Compare commits

..

66 Commits

Author SHA1 Message Date
dependabot[bot]
b0fe0e0c27 chore(deps): bump github.com/swaggo/echo-swagger from 1.4.1 to 1.5.0
Bumps [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger) from 1.4.1 to 1.5.0.
- [Release notes](https://github.com/swaggo/echo-swagger/releases)
- [Commits](https://github.com/swaggo/echo-swagger/compare/v1.4.1...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/swaggo/echo-swagger
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-18 21:22:19 +00: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
Ettore Di Giacinto
2fd026e958 fix: update moonshine API, add setuptools to voxcpm requirements (#8541)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-12 23:22:37 +01:00
LocalAI [bot]
08718b656e chore: ⬆️ Update ggml-org/llama.cpp to 338085c69e486b7155e5b03d7b5087e02c0e2528 (#8538)
⬆️ 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-12 23:21:53 +01:00
LocalAI [bot]
7121b189f7 chore(model-gallery): ⬆️ update checksum (#8540)
⬆️ 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-12 21:54:33 +01:00
Richard Palethorpe
f6c80a6987 feat(realtime): Allow sending text, image and audio conversation items" (#8524)
feat(realtime): Allow sending text and image conversation items

Signed-off-by: Richard Palethorpe <io@richiejp.com>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-12 19:33:46 +00:00
Ettore Di Giacinto
4a4d65f8e8 chore(model gallery): add vllm-omni models (#8536)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-12 18:27:20 +01:00
Ettore Di Giacinto
2858e71606 chore(model gallery): add neutts (#8535)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-12 18:17:03 +01:00
Ettore Di Giacinto
088205339c chore(model gallery): add voxcpm, whisperx, moonshine-tiny (#8534)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-12 18:13:03 +01:00
Ettore Di Giacinto
8616397d59 chore(model gallery): add nemo-asr (#8533)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-12 18:01:42 +01:00
Ettore Di Giacinto
1698f92bd0 Remove URL entry from gallery index
Signed-off-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-12 17:50:13 +01:00
Ettore Di Giacinto
02c95a57ae Add known use cases for audio processing
Signed-off-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-12 17:49:54 +01:00
rampa3
2ab6be1d0c chore(model gallery): Add npc-llm-3-8b (#8498)
Signed-off-by: rampa3 <68955305+rampa3@users.noreply.github.com>
2026-02-12 17:46:25 +01:00
Ettore Di Giacinto
9d78ec1bd8 chore(model gallery): add voxtral (which is only available in development) (#8532)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-12 17:44:24 +01:00
LocalAI [bot]
b10b85de52 chore: improve log levels verbosity (#8528)
* chore: init for PR

* feat: improve log verbosity per #8449 - demote /api/resources to DEBUG, elevate job events to INFO

---------

Co-authored-by: localai-bot <localai-bot@users.noreply.github.com>
2026-02-12 16:24:46 +01:00
Richard Palethorpe
1479bee894 fix(realtime): Sampling and websocket locking (#8521)
* fix(realtime): Use locked websocket for concurrent access

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

* fix(realtime): Use sample rate set in session

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

* fix(config): Allow pipelines to have no model parameters

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

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-02-12 13:57:34 +01:00
Austen
cff972094c feat(diffusers): add experimental support for sd_embed-style prompt embedding (#8504)
* add experimental support for sd_embed-style prompt embedding

Signed-off-by: Austen Dicken <cvpcsm@gmail.com>

* add doc equivalent to compel

Signed-off-by: Austen Dicken <cvpcsm@gmail.com>

* need to use flux1 embedding function for flux model

Signed-off-by: Austen Dicken <cvpcsm@gmail.com>

---------

Signed-off-by: Austen Dicken <cvpcsm@gmail.com>
2026-02-11 22:58:19 +01:00
LocalAI [bot]
79a25f7ae9 chore: ⬆️ Update ggml-org/llama.cpp to 4d3daf80f8834e0eb5148efc7610513f1e263653 (#8513)
⬆️ 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-11 21:48:39 +00:00
Richard Palethorpe
7270a98ce5 fix(realtime): Use user provided voice and allow pipeline models to have no backend (#8415)
* fix(realtime): Use the voice provided by the user or none at all

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

* fix(ui,config): Allow pipeline models to have no backend and use same validation in frontend

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

---------

Signed-off-by: Richard Palethorpe <io@richiejp.com>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-11 14:18:05 +01:00
LocalAI [bot]
0ee92317ec chore: ⬆️ Update ggml-org/llama.cpp to 57487a64c88c152ac72f3aea09bd1cc491b2f61e (#8499)
⬆️ 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-10 21:32:46 +00:00
LocalAI [bot]
743d2d1947 chore: ⬆️ Update ggml-org/whisper.cpp to 764482c3175d9c3bc6089c1ec84df7d1b9537d83 (#8478)
⬆️ 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-10 15:14:59 +01:00
LocalAI [bot]
df04843f34 chore: ⬆️ Update ggml-org/llama.cpp to 262364e31d1da43596fe84244fba44e94a0de64e (#8479)
⬆️ 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-10 15:14:33 +01:00
Kolega.dev
780877d1d0 security: validate URLs to prevent SSRF in content fetching endpoints (#8476)
User-supplied URLs passed to GetContentURIAsBase64() and downloadFile()
were fetched without validation, allowing SSRF attacks against internal
services. Added URL validation that blocks private IPs, loopback,
link-local, and cloud metadata endpoints before fetching.

Co-authored-by: kolega.dev <faizan@kolega.ai>
2026-02-10 15:14:14 +01:00
dependabot[bot]
08eeed61f4 chore(deps): bump github.com/openai/openai-go/v3 from 3.17.0 to 3.19.0 (#8485)
Bumps [github.com/openai/openai-go/v3](https://github.com/openai/openai-go) from 3.17.0 to 3.19.0.
- [Release notes](https://github.com/openai/openai-go/releases)
- [Changelog](https://github.com/openai/openai-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-go/compare/v3.17.0...v3.19.0)

---
updated-dependencies:
- dependency-name: github.com/openai/openai-go/v3
  dependency-version: 3.19.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-10 05:41:15 +00:00
dependabot[bot]
5207ff84dc chore(deps): bump github.com/alecthomas/kong from 1.13.0 to 1.14.0 (#8481)
Bumps [github.com/alecthomas/kong](https://github.com/alecthomas/kong) from 1.13.0 to 1.14.0.
- [Commits](https://github.com/alecthomas/kong/compare/v1.13.0...v1.14.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/kong
  dependency-version: 1.14.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-10 04:29:00 +00:00
dependabot[bot]
4ade2e61ab chore(deps): bump github.com/onsi/ginkgo/v2 from 2.28.0 to 2.28.1 (#8483)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.28.0 to 2.28.1.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.28.0...v2.28.1)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.28.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-10 03:15:46 +00:00
dependabot[bot]
818be98314 chore(deps): bump github.com/jaypipes/ghw from 0.21.2 to 0.22.0 (#8484)
Bumps [github.com/jaypipes/ghw](https://github.com/jaypipes/ghw) from 0.21.2 to 0.22.0.
- [Release notes](https://github.com/jaypipes/ghw/releases)
- [Commits](https://github.com/jaypipes/ghw/compare/v0.21.2...v0.22.0)

---
updated-dependencies:
- dependency-name: github.com/jaypipes/ghw
  dependency-version: 0.22.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-10 02:02:38 +00:00
dependabot[bot]
056c438452 chore(deps): bump github.com/anthropics/anthropic-sdk-go from 1.20.0 to 1.22.0 (#8482)
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.20.0 to 1.22.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.20.0...v1.22.0)

---
updated-dependencies:
- dependency-name: github.com/anthropics/anthropic-sdk-go
  dependency-version: 1.22.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-09 23:34:36 +00:00
LocalAI [bot]
0c040beb59 chore: ⬆️ Update antirez/voxtral.c to c9e8773a2042d67c637fc492c8a655c485354080 (#8477)
⬆️ 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-09 22:20:03 +01:00
Ettore Di Giacinto
bf5a1dd840 feat(voxtral): add voxtral backend (#8451)
* feat(voxtral): add voxtral backend

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

* simplify

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

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-09 09:12:05 +01:00
rampa3
f44200bec8 chore(model gallery): Add Ministral 3 family of models (aside from base versions) (#8467)
Signed-off-by: rampa3 <68955305+rampa3@users.noreply.github.com>
2026-02-09 09:10:37 +01:00
LocalAI [bot]
3b1b08efd6 chore: ⬆️ Update ggml-org/llama.cpp to e06088da0fa86aa444409f38dff274904931c507 (#8464)
⬆️ 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-09 09:09:32 +01:00
LocalAI [bot]
3d8791067f chore: ⬆️ Update ggml-org/whisper.cpp to 4b23ff249e7f93137cb870b28fb27818e074c255 (#8463)
⬆️ 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-09 09:08:55 +01:00
Austen
da8207b73b feat(stablediffusion-ggml): Improve legacy CPU support for stablediffusion-ggml backend (#8461)
* Port AVX logic from whisper to stablediffusion-ggml

Signed-off-by: Austen Dicken <cvpcsm@gmail.com>

* disable BMI2 on AVX builds

Signed-off-by: Austen Dicken <cvpcsm@gmail.com>

---------

Signed-off-by: Austen Dicken <cvpcsm@gmail.com>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-08 23:11:33 +00:00
Varun Chawla
aa9ca401fa docs: update model gallery documentation to reference main repository (#8452)
Fixes #8212 - Updated the note about reporting broken models to
reference the main LocalAI repository instead of the outdated
separate gallery repository reference.
2026-02-08 22:14:23 +01:00
LocalAI [bot]
e43c0c3ffc docs: ⬆️ update docs version mudler/LocalAI (#8462)
⬆️ 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-08 21:12:50 +00:00
111 changed files with 4723 additions and 1627 deletions

3
.env
View File

@@ -26,6 +26,9 @@
## Disables COMPEL (Diffusers)
# COMPEL=0
## Disables SD_EMBED (Diffusers)
# SD_EMBED=0
## Enable/Disable single backend (useful if only one GPU is available)
# LOCALAI_SINGLE_ACTIVE_BACKEND=true

View File

@@ -146,7 +146,7 @@ func getRealReadme(ctx context.Context, repository string) (string, error) {
return "", err
}
content := newFragment.LastMessage().Content
content := result.LastMessage().Content
return cleanTextContent(content), nil
}

View File

@@ -1674,6 +1674,20 @@ jobs:
dockerfile: "./backend/Dockerfile.golang"
context: "./"
ubuntu-version: '2404'
# voxtral
- build-type: ''
cuda-major-version: ""
cuda-minor-version: ""
platforms: 'linux/amd64,linux/arm64'
tag-latest: 'auto'
tag-suffix: '-cpu-voxtral'
runs-on: 'ubuntu-latest'
base-image: "ubuntu:24.04"
skip-drivers: 'false'
backend: "voxtral"
dockerfile: "./backend/Dockerfile.golang"
context: "./"
ubuntu-version: '2404'
#silero-vad
- build-type: ''
cuda-major-version: ""
@@ -1945,6 +1959,10 @@ jobs:
tag-suffix: "-metal-darwin-arm64-whisper"
build-type: "metal"
lang: "go"
- backend: "voxtral"
tag-suffix: "-metal-darwin-arm64-voxtral"
build-type: "metal"
lang: "go"
- backend: "vibevoice"
tag-suffix: "-metal-darwin-arm64-vibevoice"
build-type: "mps"

View File

@@ -30,6 +30,10 @@ jobs:
variable: "PIPER_VERSION"
branch: "master"
file: "backend/go/piper/Makefile"
- repository: "antirez/voxtral.c"
variable: "VOXTRAL_VERSION"
branch: "main"
file: "backend/go/voxtral/Makefile"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

View File

@@ -361,3 +361,34 @@ jobs:
run: |
make --jobs=5 --output-sync=target -C backend/python/voxcpm
make --jobs=5 --output-sync=target -C backend/python/voxcpm test
tests-voxtral:
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v6
with:
submodules: true
- name: Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake curl libopenblas-dev ffmpeg
- name: Setup Go
uses: actions/setup-go@v5
# You can test your matrix by printing the current Go version
- name: Display Go version
run: go version
- name: Proto Dependencies
run: |
# Install protoc
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
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.2
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@1958fcbe2ca8bd93af633f11e97d44e567e945af
PATH="$PATH:$HOME/go/bin" make protogen-go
- name: Build voxtral
run: |
make --jobs=5 --output-sync=target -C backend/go/voxtral
- name: Test voxtral
run: |
make --jobs=5 --output-sync=target -C backend/go/voxtral test

View File

@@ -1,5 +1,5 @@
# 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
.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
GOCMD=go
GOTEST=$(GOCMD) test
@@ -453,6 +453,7 @@ BACKEND_HUGGINGFACE = huggingface|golang|.|false|true
BACKEND_SILERO_VAD = silero-vad|golang|.|false|true
BACKEND_STABLEDIFFUSION_GGML = stablediffusion-ggml|golang|.|--progress=plain|true
BACKEND_WHISPER = whisper|golang|.|false|true
BACKEND_VOXTRAL = voxtral|golang|.|false|true
# Python backends with root context
BACKEND_RERANKERS = rerankers|python|.|false|true
@@ -506,6 +507,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_HUGGINGFACE)))
$(eval $(call generate-docker-build-target,$(BACKEND_SILERO_VAD)))
$(eval $(call generate-docker-build-target,$(BACKEND_STABLEDIFFUSION_GGML)))
$(eval $(call generate-docker-build-target,$(BACKEND_WHISPER)))
$(eval $(call generate-docker-build-target,$(BACKEND_VOXTRAL)))
$(eval $(call generate-docker-build-target,$(BACKEND_RERANKERS)))
$(eval $(call generate-docker-build-target,$(BACKEND_TRANSFORMERS)))
$(eval $(call generate-docker-build-target,$(BACKEND_OUTETTS)))
@@ -533,7 +535,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_ACE_STEP)))
docker-save-%: backend-images
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-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
########################################################
### Mock Backend for E2E Tests

View File

@@ -237,7 +237,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.
- 📖 [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/)
- 🔈 [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)
- 🔥 [OpenAI-alike tools API](https://localai.io/features/openai-functions/)
- ⚡ [Realtime API](https://localai.io/features/openai-realtime/) (Speech-to-speech)

View File

@@ -20,7 +20,7 @@ RUN apt-get update && \
build-essential \
git ccache \
ca-certificates \
make cmake wget \
make cmake wget libopenblas-dev \
curl unzip \
libssl-dev && \
apt-get clean && \

View File

@@ -1,5 +1,5 @@
LLAMA_VERSION?=8872ad2125336d209a9911a82101f80095a9831d
LLAMA_VERSION?=2b089c77580d347767f440205103e4da8ec33d89
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
CMAKE_ARGS?=

View File

@@ -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)
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.
for (int i = 0; i < request->options_size(); i++) {
std::string opt = request->options(i);
@@ -1255,6 +1261,42 @@ public:
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.)
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"];
}
// 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.)
SRV_DBG("[CONVERSATION DEBUG] Predict: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str());

View File

@@ -2,5 +2,5 @@ package/
sources/
.cache/
build/
libgosd.so
*.so
stablediffusion-ggml

View File

@@ -66,15 +66,18 @@ sources/stablediffusion-ggml.cpp:
git checkout $(STABLEDIFFUSION_GGML_VERSION) && \
git submodule update --init --recursive --depth 1 --single-branch
libgosd.so: sources/stablediffusion-ggml.cpp CMakeLists.txt gosd.cpp gosd.h
mkdir -p build && \
cd build && \
cmake .. $(CMAKE_ARGS) && \
cmake --build . --config Release -j$(JOBS) && \
cd .. && \
mv build/libgosd.so ./
# Detect OS
UNAME_S := $(shell uname -s)
stablediffusion-ggml: main.go gosd.go libgosd.so
# Only build CPU variants on Linux
ifeq ($(UNAME_S),Linux)
VARIANT_TARGETS = libgosd-avx.so libgosd-avx2.so libgosd-avx512.so libgosd-fallback.so
else
# On non-Linux (e.g., Darwin), build only fallback variant
VARIANT_TARGETS = libgosd-fallback.so
endif
stablediffusion-ggml: main.go gosd.go $(VARIANT_TARGETS)
CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o stablediffusion-ggml ./
package: stablediffusion-ggml
@@ -82,5 +85,46 @@ package: stablediffusion-ggml
build: package
clean:
rm -rf libgosd.so build stablediffusion-ggml package sources
clean: purge
rm -rf libgosd*.so stablediffusion-ggml package sources
purge:
rm -rf build*
# Build all variants (Linux only)
ifeq ($(UNAME_S),Linux)
libgosd-avx.so: sources/stablediffusion-ggml.cpp
$(MAKE) purge
$(info ${GREEN}I stablediffusion-ggml build info:avx${RESET})
SO_TARGET=libgosd-avx.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) libgosd-custom
rm -rfv build*
libgosd-avx2.so: sources/stablediffusion-ggml.cpp
$(MAKE) purge
$(info ${GREEN}I stablediffusion-ggml build info:avx2${RESET})
SO_TARGET=libgosd-avx2.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=on -DGGML_AVX512=off -DGGML_FMA=on -DGGML_F16C=on -DGGML_BMI2=on" $(MAKE) libgosd-custom
rm -rfv build*
libgosd-avx512.so: sources/stablediffusion-ggml.cpp
$(MAKE) purge
$(info ${GREEN}I stablediffusion-ggml build info:avx512${RESET})
SO_TARGET=libgosd-avx512.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=on -DGGML_AVX512=on -DGGML_FMA=on -DGGML_F16C=on -DGGML_BMI2=on" $(MAKE) libgosd-custom
rm -rfv build*
endif
# Build fallback variant (all platforms)
libgosd-fallback.so: sources/stablediffusion-ggml.cpp
$(MAKE) purge
$(info ${GREEN}I stablediffusion-ggml build info:fallback${RESET})
SO_TARGET=libgosd-fallback.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) libgosd-custom
rm -rfv build*
libgosd-custom: CMakeLists.txt gosd.cpp gosd.h
mkdir -p build-$(SO_TARGET) && \
cd build-$(SO_TARGET) && \
cmake .. $(CMAKE_ARGS) && \
cmake --build . --config Release -j$(JOBS) && \
cd .. && \
mv build-$(SO_TARGET)/libgosd.so ./$(SO_TARGET)
all: stablediffusion-ggml package

View File

@@ -2,6 +2,7 @@ package main
import (
"flag"
"os"
"github.com/ebitengine/purego"
grpc "github.com/mudler/LocalAI/pkg/grpc"
@@ -17,7 +18,13 @@ type LibFuncs struct {
}
func main() {
gosd, err := purego.Dlopen("./libgosd.so", purego.RTLD_NOW|purego.RTLD_GLOBAL)
// Get library name from environment variable, default to fallback
libName := os.Getenv("SD_LIBRARY")
if libName == "" {
libName = "./libgosd-fallback.so"
}
gosd, err := purego.Dlopen(libName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
panic(err)
}

View File

@@ -11,7 +11,7 @@ REPO_ROOT="${CURDIR}/../../.."
# Create lib directory
mkdir -p $CURDIR/package/lib
cp -avf $CURDIR/libgosd.so $CURDIR/package/
cp -avf $CURDIR/libgosd-*.so $CURDIR/package/
cp -avf $CURDIR/stablediffusion-ggml $CURDIR/package/
cp -fv $CURDIR/run.sh $CURDIR/package/

View File

@@ -1,14 +1,52 @@
#!/bin/bash
set -ex
# Get the absolute current dir where the script is located
CURDIR=$(dirname "$(realpath $0)")
cd /
echo "CPU info:"
if [ "$(uname)" != "Darwin" ]; then
grep -e "model\sname" /proc/cpuinfo | head -1
grep -e "flags" /proc/cpuinfo | head -1
fi
LIBRARY="$CURDIR/libgosd-fallback.so"
if [ "$(uname)" != "Darwin" ]; then
if grep -q -e "\savx\s" /proc/cpuinfo ; then
echo "CPU: AVX found OK"
if [ -e $CURDIR/libgosd-avx.so ]; then
LIBRARY="$CURDIR/libgosd-avx.so"
fi
fi
if grep -q -e "\savx2\s" /proc/cpuinfo ; then
echo "CPU: AVX2 found OK"
if [ -e $CURDIR/libgosd-avx2.so ]; then
LIBRARY="$CURDIR/libgosd-avx2.so"
fi
fi
# Check avx 512
if grep -q -e "\savx512f\s" /proc/cpuinfo ; then
echo "CPU: AVX512F found OK"
if [ -e $CURDIR/libgosd-avx512.so ]; then
LIBRARY="$CURDIR/libgosd-avx512.so"
fi
fi
fi
export LD_LIBRARY_PATH=$CURDIR/lib:$LD_LIBRARY_PATH
export SD_LIBRARY=$LIBRARY
# If there is a lib/ld.so, use it
if [ -f $CURDIR/lib/ld.so ]; then
echo "Using lib/ld.so"
echo "Using library: $LIBRARY"
exec $CURDIR/lib/ld.so $CURDIR/stablediffusion-ggml "$@"
fi
exec $CURDIR/stablediffusion-ggml "$@"
echo "Using library: $LIBRARY"
exec $CURDIR/stablediffusion-ggml "$@"

9
backend/go/voxtral/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.cache/
sources/
build/
build-*/
package/
voxtral
*.so
*.dylib
compile_commands.json

View File

@@ -0,0 +1,84 @@
cmake_minimum_required(VERSION 3.12)
if(USE_METAL)
project(govoxtral LANGUAGES C OBJC)
else()
project(govoxtral LANGUAGES C)
endif()
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Workaround: CMake + GCC linker depfile generation fails for MODULE libraries
set(CMAKE_C_LINKER_DEPFILE_SUPPORTED FALSE)
# Build voxtral.c as a library
set(VOXTRAL_SOURCES
sources/voxtral.c/voxtral.c
sources/voxtral.c/voxtral_kernels.c
sources/voxtral.c/voxtral_audio.c
sources/voxtral.c/voxtral_encoder.c
sources/voxtral.c/voxtral_decoder.c
sources/voxtral.c/voxtral_tokenizer.c
sources/voxtral.c/voxtral_safetensors.c
)
# Metal GPU acceleration (macOS arm64 only)
if(USE_METAL)
# Generate embedded shader header from .metal source via xxd
add_custom_command(
OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/sources/voxtral.c/voxtral_shaders_source.h
COMMAND xxd -i voxtral_shaders.metal > voxtral_shaders_source.h
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/sources/voxtral.c
DEPENDS sources/voxtral.c/voxtral_shaders.metal
COMMENT "Generating embedded Metal shaders header"
)
list(APPEND VOXTRAL_SOURCES sources/voxtral.c/voxtral_metal.m)
set_source_files_properties(sources/voxtral.c/voxtral_metal.m PROPERTIES
COMPILE_FLAGS "-fobjc-arc"
)
endif()
add_library(govoxtral MODULE csrc/govoxtral.c ${VOXTRAL_SOURCES})
target_include_directories(govoxtral PRIVATE sources/voxtral.c csrc)
target_compile_options(govoxtral PRIVATE -O3 -ffast-math)
if(USE_METAL)
target_compile_definitions(govoxtral PRIVATE USE_BLAS USE_METAL ACCELERATE_NEW_LAPACK)
target_link_libraries(govoxtral PRIVATE
"-framework Accelerate"
"-framework Metal"
"-framework MetalPerformanceShaders"
"-framework MetalPerformanceShadersGraph"
"-framework Foundation"
"-framework AudioToolbox"
"-framework CoreFoundation"
m
)
# Ensure the generated shader header is built before compiling
target_sources(govoxtral PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/sources/voxtral.c/voxtral_shaders_source.h
)
elseif(USE_OPENBLAS)
# Try to find OpenBLAS; use it if available, otherwise fall back to pure C
find_package(BLAS)
if(BLAS_FOUND)
target_compile_definitions(govoxtral PRIVATE USE_BLAS USE_OPENBLAS)
target_link_libraries(govoxtral PRIVATE ${BLAS_LIBRARIES} m)
target_include_directories(govoxtral PRIVATE /usr/include/openblas)
else()
message(WARNING "OpenBLAS requested but not found, building without BLAS")
target_link_libraries(govoxtral PRIVATE m)
endif()
elseif(APPLE)
# macOS without Metal: use Accelerate framework
target_compile_definitions(govoxtral PRIVATE USE_BLAS ACCELERATE_NEW_LAPACK)
target_link_libraries(govoxtral PRIVATE "-framework Accelerate" m)
else()
target_link_libraries(govoxtral PRIVATE m)
endif()
set_property(TARGET govoxtral PROPERTY C_STANDARD 11)
set_target_properties(govoxtral PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

107
backend/go/voxtral/Makefile Normal file
View File

@@ -0,0 +1,107 @@
.NOTPARALLEL:
CMAKE_ARGS?=
BUILD_TYPE?=
NATIVE?=true
GOCMD?=go
GO_TAGS?=
JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
# voxtral.c version
VOXTRAL_REPO?=https://github.com/antirez/voxtral.c
VOXTRAL_VERSION?=134d366c24d20c64b614a3dcc8bda2a6922d077d
# Detect OS
UNAME_S := $(shell uname -s)
# Shared library extension
ifeq ($(UNAME_S),Darwin)
SO_EXT=dylib
else
SO_EXT=so
endif
SO_TARGET?=libgovoxtral.$(SO_EXT)
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
ifeq ($(NATIVE),false)
ifneq ($(UNAME_S),Darwin)
CMAKE_ARGS+=-DCMAKE_C_FLAGS="-march=x86-64"
endif
endif
ifeq ($(BUILD_TYPE),cublas)
CMAKE_ARGS+=-DUSE_OPENBLAS=OFF
else ifeq ($(BUILD_TYPE),hipblas)
CMAKE_ARGS+=-DUSE_OPENBLAS=OFF
else ifeq ($(BUILD_TYPE),metal)
CMAKE_ARGS+=-DUSE_OPENBLAS=OFF -DUSE_METAL=ON
else ifeq ($(UNAME_S),Darwin)
# Default on macOS: use Accelerate (no OpenBLAS needed)
CMAKE_ARGS+=-DUSE_OPENBLAS=OFF
else
CMAKE_ARGS+=-DUSE_OPENBLAS=ON
endif
# Single library target
ifeq ($(UNAME_S),Darwin)
VARIANT_TARGETS = libgovoxtral.dylib
else
VARIANT_TARGETS = libgovoxtral.so
endif
sources/voxtral.c:
mkdir -p sources/voxtral.c
cd sources/voxtral.c && \
git init && \
git remote add origin $(VOXTRAL_REPO) && \
git fetch origin && \
git checkout $(VOXTRAL_VERSION) && \
git submodule update --init --recursive --depth 1 --single-branch
voxtral: main.go govoxtral.go $(VARIANT_TARGETS)
CGO_ENABLED=0 $(GOCMD) build -tags "$(GO_TAGS)" -o voxtral ./
package: voxtral
bash package.sh
build: package
clean: purge
rm -rf libgovoxtral.so libgovoxtral.dylib package sources/voxtral.c voxtral
purge:
rm -rf build*
# Build single library
ifeq ($(UNAME_S),Darwin)
libgovoxtral.dylib: sources/voxtral.c
$(MAKE) purge
$(info Building voxtral: darwin)
SO_TARGET=libgovoxtral.dylib NATIVE=true $(MAKE) libgovoxtral-custom
rm -rfv build*
else
libgovoxtral.so: sources/voxtral.c
$(MAKE) purge
$(info Building voxtral)
SO_TARGET=libgovoxtral.so $(MAKE) libgovoxtral-custom
rm -rfv build*
endif
libgovoxtral-custom: CMakeLists.txt csrc/govoxtral.c csrc/govoxtral.h
mkdir -p build-$(SO_TARGET) && \
cd build-$(SO_TARGET) && \
cmake .. $(CMAKE_ARGS) && \
cmake --build . --config Release -j$(JOBS) && \
cd .. && \
(mv build-$(SO_TARGET)/libgovoxtral.so ./$(SO_TARGET) 2>/dev/null || \
mv build-$(SO_TARGET)/libgovoxtral.dylib ./$(SO_TARGET) 2>/dev/null)
test: voxtral
@echo "Running voxtral tests..."
bash test.sh
@echo "voxtral tests completed."
all: voxtral package

View File

@@ -0,0 +1,62 @@
#include "govoxtral.h"
#include "voxtral.h"
#include "voxtral_audio.h"
#ifdef USE_METAL
#include "voxtral_metal.h"
#endif
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
static vox_ctx_t *ctx = NULL;
static char *last_result = NULL;
static int metal_initialized = 0;
int load_model(const char *model_dir) {
if (ctx != NULL) {
vox_free(ctx);
ctx = NULL;
}
#ifdef USE_METAL
if (!metal_initialized) {
vox_metal_init();
metal_initialized = 1;
}
#endif
ctx = vox_load(model_dir);
if (ctx == NULL) {
fprintf(stderr, "error: failed to load voxtral model from %s\n", model_dir);
return 1;
}
return 0;
}
const char *transcribe(const char *wav_path) {
if (ctx == NULL) {
fprintf(stderr, "error: model not loaded\n");
return "";
}
if (last_result != NULL) {
free(last_result);
last_result = NULL;
}
last_result = vox_transcribe(ctx, wav_path);
if (last_result == NULL) {
fprintf(stderr, "error: transcription failed for %s\n", wav_path);
return "";
}
return last_result;
}
void free_result(void) {
if (last_result != NULL) {
free(last_result);
last_result = NULL;
}
}

View File

@@ -0,0 +1,8 @@
#ifndef GOVOXTRAL_H
#define GOVOXTRAL_H
extern int load_model(const char *model_dir);
extern const char *transcribe(const char *wav_path);
extern void free_result(void);
#endif /* GOVOXTRAL_H */

View File

@@ -0,0 +1,60 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/mudler/LocalAI/pkg/grpc/base"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
"github.com/mudler/LocalAI/pkg/utils"
)
var (
CppLoadModel func(modelDir string) int
CppTranscribe func(wavPath string) string
CppFreeResult func()
)
type Voxtral struct {
base.SingleThread
}
func (v *Voxtral) Load(opts *pb.ModelOptions) error {
if ret := CppLoadModel(opts.ModelFile); ret != 0 {
return fmt.Errorf("failed to load Voxtral model from %s", opts.ModelFile)
}
return nil
}
func (v *Voxtral) AudioTranscription(opts *pb.TranscriptRequest) (pb.TranscriptResult, error) {
dir, err := os.MkdirTemp("", "voxtral")
if err != nil {
return pb.TranscriptResult{}, err
}
defer os.RemoveAll(dir)
convertedPath := dir + "/converted.wav"
if err := utils.AudioToWav(opts.Dst, convertedPath); err != nil {
return pb.TranscriptResult{}, err
}
result := strings.Clone(CppTranscribe(convertedPath))
CppFreeResult()
text := strings.TrimSpace(result)
segments := []*pb.TranscriptSegment{}
if text != "" {
segments = append(segments, &pb.TranscriptSegment{
Id: 0,
Text: text,
})
}
return pb.TranscriptResult{
Segments: segments,
Text: text,
}, nil
}

View File

@@ -0,0 +1,53 @@
package main
// Note: this is started internally by LocalAI and a server is allocated for each model
import (
"flag"
"os"
"runtime"
"github.com/ebitengine/purego"
grpc "github.com/mudler/LocalAI/pkg/grpc"
)
var (
addr = flag.String("addr", "localhost:50051", "the address to connect to")
)
type LibFuncs struct {
FuncPtr any
Name string
}
func main() {
// Get library name from environment variable, default to fallback
libName := os.Getenv("VOXTRAL_LIBRARY")
if libName == "" {
if runtime.GOOS == "darwin" {
libName = "./libgovoxtral.dylib"
} else {
libName = "./libgovoxtral.so"
}
}
gosd, err := purego.Dlopen(libName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
panic(err)
}
libFuncs := []LibFuncs{
{&CppLoadModel, "load_model"},
{&CppTranscribe, "transcribe"},
{&CppFreeResult, "free_result"},
}
for _, lf := range libFuncs {
purego.RegisterLibFunc(lf.FuncPtr, gosd, lf.Name)
}
flag.Parse()
if err := grpc.StartServer(*addr, &Voxtral{}); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Script to copy the appropriate libraries based on architecture
set -e
CURDIR=$(dirname "$(realpath $0)")
REPO_ROOT="${CURDIR}/../../.."
# Create lib directory
mkdir -p $CURDIR/package/lib
cp -avf $CURDIR/voxtral $CURDIR/package/
cp -fv $CURDIR/libgovoxtral-*.so $CURDIR/package/ 2>/dev/null || true
cp -fv $CURDIR/libgovoxtral-*.dylib $CURDIR/package/ 2>/dev/null || true
cp -fv $CURDIR/run.sh $CURDIR/package/
# Detect architecture and copy appropriate libraries
if [ -f "/lib64/ld-linux-x86-64.so.2" ]; then
# x86_64 architecture
echo "Detected x86_64 architecture, copying x86_64 libraries..."
cp -arfLv /lib64/ld-linux-x86-64.so.2 $CURDIR/package/lib/ld.so
cp -arfLv /lib/x86_64-linux-gnu/libc.so.6 $CURDIR/package/lib/libc.so.6
cp -arfLv /lib/x86_64-linux-gnu/libgcc_s.so.1 $CURDIR/package/lib/libgcc_s.so.1
cp -arfLv /lib/x86_64-linux-gnu/libstdc++.so.6 $CURDIR/package/lib/libstdc++.so.6
cp -arfLv /lib/x86_64-linux-gnu/libm.so.6 $CURDIR/package/lib/libm.so.6
cp -arfLv /lib/x86_64-linux-gnu/libgomp.so.1 $CURDIR/package/lib/libgomp.so.1
cp -arfLv /lib/x86_64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
cp -arfLv /lib/x86_64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
cp -arfLv /lib/x86_64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
# OpenBLAS if available
if [ -f /usr/lib/x86_64-linux-gnu/libopenblas.so.0 ]; then
cp -arfLv /usr/lib/x86_64-linux-gnu/libopenblas.so.0 $CURDIR/package/lib/
fi
elif [ -f "/lib/ld-linux-aarch64.so.1" ]; then
# ARM64 architecture
echo "Detected ARM64 architecture, copying ARM64 libraries..."
cp -arfLv /lib/ld-linux-aarch64.so.1 $CURDIR/package/lib/ld.so
cp -arfLv /lib/aarch64-linux-gnu/libc.so.6 $CURDIR/package/lib/libc.so.6
cp -arfLv /lib/aarch64-linux-gnu/libgcc_s.so.1 $CURDIR/package/lib/libgcc_s.so.1
cp -arfLv /lib/aarch64-linux-gnu/libstdc++.so.6 $CURDIR/package/lib/libstdc++.so.6
cp -arfLv /lib/aarch64-linux-gnu/libm.so.6 $CURDIR/package/lib/libm.so.6
cp -arfLv /lib/aarch64-linux-gnu/libgomp.so.1 $CURDIR/package/lib/libgomp.so.1
cp -arfLv /lib/aarch64-linux-gnu/libdl.so.2 $CURDIR/package/lib/libdl.so.2
cp -arfLv /lib/aarch64-linux-gnu/librt.so.1 $CURDIR/package/lib/librt.so.1
cp -arfLv /lib/aarch64-linux-gnu/libpthread.so.0 $CURDIR/package/lib/libpthread.so.0
# OpenBLAS if available
if [ -f /usr/lib/aarch64-linux-gnu/libopenblas.so.0 ]; then
cp -arfLv /usr/lib/aarch64-linux-gnu/libopenblas.so.0 $CURDIR/package/lib/
fi
elif [ $(uname -s) = "Darwin" ]; then
echo "Detected Darwin — system frameworks linked dynamically, no bundled libs needed"
else
echo "Error: Could not detect architecture"
exit 1
fi
# Package GPU libraries based on BUILD_TYPE
GPU_LIB_SCRIPT="${REPO_ROOT}/scripts/build/package-gpu-libs.sh"
if [ -f "$GPU_LIB_SCRIPT" ]; then
echo "Packaging GPU libraries for BUILD_TYPE=${BUILD_TYPE:-cpu}..."
source "$GPU_LIB_SCRIPT" "$CURDIR/package/lib"
package_gpu_libs
fi
echo "Packaging completed successfully"
ls -liah $CURDIR/package/
ls -liah $CURDIR/package/lib/

49
backend/go/voxtral/run.sh Normal file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
set -ex
# Get the absolute current dir where the script is located
CURDIR=$(dirname "$(realpath $0)")
cd /
echo "CPU info:"
if [ "$(uname)" != "Darwin" ]; then
grep -e "model\sname" /proc/cpuinfo | head -1
grep -e "flags" /proc/cpuinfo | head -1
fi
if [ "$(uname)" = "Darwin" ]; then
# macOS: single dylib variant (Metal or Accelerate)
LIBRARY="$CURDIR/libgovoxtral-fallback.dylib"
export DYLD_LIBRARY_PATH=$CURDIR/lib:$DYLD_LIBRARY_PATH
else
LIBRARY="$CURDIR/libgovoxtral-fallback.so"
if grep -q -e "\savx\s" /proc/cpuinfo ; then
echo "CPU: AVX found OK"
if [ -e $CURDIR/libgovoxtral-avx.so ]; then
LIBRARY="$CURDIR/libgovoxtral-avx.so"
fi
fi
if grep -q -e "\savx2\s" /proc/cpuinfo ; then
echo "CPU: AVX2 found OK"
if [ -e $CURDIR/libgovoxtral-avx2.so ]; then
LIBRARY="$CURDIR/libgovoxtral-avx2.so"
fi
fi
export LD_LIBRARY_PATH=$CURDIR/lib:$LD_LIBRARY_PATH
fi
export VOXTRAL_LIBRARY=$LIBRARY
# If there is a lib/ld.so, use it (Linux only)
if [ -f $CURDIR/lib/ld.so ]; then
echo "Using lib/ld.so"
echo "Using library: $LIBRARY"
exec $CURDIR/lib/ld.so $CURDIR/voxtral "$@"
fi
echo "Using library: $LIBRARY"
exec $CURDIR/voxtral "$@"

View File

@@ -0,0 +1,48 @@
#!/bin/bash
set -e
CURDIR=$(dirname "$(realpath $0)")
echo "Running voxtral backend tests..."
# The test requires:
# - VOXTRAL_MODEL_DIR: path to directory containing consolidated.safetensors + tekken.json
# - VOXTRAL_BINARY: path to the voxtral binary (defaults to ./voxtral)
#
# Tests that require the model will be skipped if VOXTRAL_MODEL_DIR is not set.
cd "$CURDIR"
export VOXTRAL_MODEL_DIR="${VOXTRAL_MODEL_DIR:-./voxtral-model}"
if [ ! -d "$VOXTRAL_MODEL_DIR" ]; then
echo "Creating voxtral-model directory for tests..."
mkdir -p "$VOXTRAL_MODEL_DIR"
MODEL_ID="mistralai/Voxtral-Mini-4B-Realtime-2602"
echo "Model: ${MODEL_ID}"
echo ""
# Files to download
FILES=(
"consolidated.safetensors"
"params.json"
"tekken.json"
)
BASE_URL="https://huggingface.co/${MODEL_ID}/resolve/main"
for file in "${FILES[@]}"; do
dest="${VOXTRAL_MODEL_DIR}/${file}"
if [ -f "${dest}" ]; then
echo " [skip] ${file} (already exists)"
else
echo " [download] ${file}..."
curl -L -o "${dest}" "${BASE_URL}/${file}" --progress-bar
echo " [done] ${file}"
fi
done
fi
# Run Go tests
go test -v -timeout 300s ./...
echo "All voxtral tests passed."

View File

@@ -0,0 +1,201 @@
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
testAddr = "localhost:50051"
sampleAudio = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_en.wav"
startupWait = 5 * time.Second
)
func skipIfNoModel(t *testing.T) string {
t.Helper()
modelDir := os.Getenv("VOXTRAL_MODEL_DIR")
if modelDir == "" {
t.Skip("VOXTRAL_MODEL_DIR not set, skipping test (set to voxtral model directory)")
}
if _, err := os.Stat(filepath.Join(modelDir, "consolidated.safetensors")); os.IsNotExist(err) {
t.Skipf("Model file not found in %s, skipping", modelDir)
}
return modelDir
}
func startServer(t *testing.T) *exec.Cmd {
t.Helper()
binary := os.Getenv("VOXTRAL_BINARY")
if binary == "" {
binary = "./voxtral"
}
if _, err := os.Stat(binary); os.IsNotExist(err) {
t.Skipf("Backend binary not found at %s, skipping", binary)
}
cmd := exec.Command(binary, "--addr", testAddr)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatalf("Failed to start server: %v", err)
}
time.Sleep(startupWait)
return cmd
}
func stopServer(cmd *exec.Cmd) {
if cmd != nil && cmd.Process != nil {
cmd.Process.Kill()
cmd.Wait()
}
}
func dialGRPC(t *testing.T) *grpc.ClientConn {
t.Helper()
conn, err := grpc.Dial(testAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(50*1024*1024),
grpc.MaxCallSendMsgSize(50*1024*1024),
),
)
if err != nil {
t.Fatalf("Failed to dial gRPC: %v", err)
}
return conn
}
func downloadFile(url, dest string) error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("HTTP GET failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
f, err := os.Create(dest)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
return err
}
func TestServerHealth(t *testing.T) {
cmd := startServer(t)
defer stopServer(cmd)
conn := dialGRPC(t)
defer conn.Close()
client := pb.NewBackendClient(conn)
resp, err := client.Health(context.Background(), &pb.HealthMessage{})
if err != nil {
t.Fatalf("Health check failed: %v", err)
}
if string(resp.Message) != "OK" {
t.Fatalf("Expected OK, got %s", string(resp.Message))
}
}
func TestLoadModel(t *testing.T) {
modelDir := skipIfNoModel(t)
cmd := startServer(t)
defer stopServer(cmd)
conn := dialGRPC(t)
defer conn.Close()
client := pb.NewBackendClient(conn)
resp, err := client.LoadModel(context.Background(), &pb.ModelOptions{
ModelFile: modelDir,
})
if err != nil {
t.Fatalf("LoadModel failed: %v", err)
}
if !resp.Success {
t.Fatalf("LoadModel returned failure: %s", resp.Message)
}
}
func TestAudioTranscription(t *testing.T) {
modelDir := skipIfNoModel(t)
tmpDir, err := os.MkdirTemp("", "voxtral-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Download sample audio — JFK "ask not what your country can do for you" clip
audioFile := filepath.Join(tmpDir, "sample.wav")
t.Log("Downloading sample audio...")
if err := downloadFile(sampleAudio, audioFile); err != nil {
t.Fatalf("Failed to download sample audio: %v", err)
}
cmd := startServer(t)
defer stopServer(cmd)
conn := dialGRPC(t)
defer conn.Close()
client := pb.NewBackendClient(conn)
// Load model
loadResp, err := client.LoadModel(context.Background(), &pb.ModelOptions{
ModelFile: modelDir,
})
if err != nil {
t.Fatalf("LoadModel failed: %v", err)
}
if !loadResp.Success {
t.Fatalf("LoadModel returned failure: %s", loadResp.Message)
}
// Transcribe
transcriptResp, err := client.AudioTranscription(context.Background(), &pb.TranscriptRequest{
Dst: audioFile,
})
if err != nil {
t.Fatalf("AudioTranscription failed: %v", err)
}
if transcriptResp == nil {
t.Fatal("AudioTranscription returned nil")
}
t.Logf("Transcribed text: %s", transcriptResp.Text)
t.Logf("Number of segments: %d", len(transcriptResp.Segments))
if transcriptResp.Text == "" {
t.Fatal("Transcription returned empty text")
}
allText := strings.ToLower(transcriptResp.Text)
for _, seg := range transcriptResp.Segments {
allText += " " + strings.ToLower(seg.Text)
}
t.Logf("All text: %s", allText)
if !strings.Contains(allText, "big") {
t.Errorf("Expected 'big' in transcription, got: %s", allText)
}
// The sample audio should contain recognizable speech
if len(allText) < 10 {
t.Errorf("Transcription too short: %q", allText)
}
}

View File

@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
# whisper.cpp version
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
WHISPER_CPP_VERSION?=941bdabbe4561bc6de68981aea01bc5ab05781c5
WHISPER_CPP_VERSION?=364c77f4ca2737e3287652e0e8a8c6dce3231bba
SO_TARGET?=libgowhisper.so
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
@@ -88,19 +88,19 @@ ifeq ($(UNAME_S),Linux)
libgowhisper-avx.so: sources/whisper.cpp
$(MAKE) purge
$(info ${GREEN}I whisper build info:avx${RESET})
SO_TARGET=libgowhisper-avx.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off" $(MAKE) libgowhisper-custom
SO_TARGET=libgowhisper-avx.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) libgowhisper-custom
rm -rfv build*
libgowhisper-avx2.so: sources/whisper.cpp
$(MAKE) purge
$(info ${GREEN}I whisper build info:avx2${RESET})
SO_TARGET=libgowhisper-avx2.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=on -DGGML_AVX512=off -DGGML_FMA=on -DGGML_F16C=on" $(MAKE) libgowhisper-custom
SO_TARGET=libgowhisper-avx2.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=on -DGGML_AVX512=off -DGGML_FMA=on -DGGML_F16C=on -DGGML_BMI2=on" $(MAKE) libgowhisper-custom
rm -rfv build*
libgowhisper-avx512.so: sources/whisper.cpp
$(MAKE) purge
$(info ${GREEN}I whisper build info:avx512${RESET})
SO_TARGET=libgowhisper-avx512.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=off -DGGML_AVX512=on -DGGML_FMA=on -DGGML_F16C=on" $(MAKE) libgowhisper-custom
SO_TARGET=libgowhisper-avx512.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=on -DGGML_AVX2=on -DGGML_AVX512=on -DGGML_FMA=on -DGGML_F16C=on -DGGML_BMI2=on" $(MAKE) libgowhisper-custom
rm -rfv build*
endif
@@ -108,7 +108,7 @@ endif
libgowhisper-fallback.so: sources/whisper.cpp
$(MAKE) purge
$(info ${GREEN}I whisper build info:fallback${RESET})
SO_TARGET=libgowhisper-fallback.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off" $(MAKE) libgowhisper-custom
SO_TARGET=libgowhisper-fallback.so CMAKE_ARGS="$(CMAKE_ARGS) -DGGML_AVX=off -DGGML_AVX2=off -DGGML_AVX512=off -DGGML_FMA=off -DGGML_F16C=off -DGGML_BMI2=off" $(MAKE) libgowhisper-custom
rm -rfv build*
libgowhisper-custom: CMakeLists.txt gowhisper.cpp gowhisper.h

View File

@@ -56,6 +56,21 @@
nvidia-cuda-12: "cuda12-whisper"
nvidia-l4t-cuda-12: "nvidia-l4t-arm64-whisper"
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-whisper"
- &voxtral
name: "voxtral"
alias: "voxtral"
license: mit
description: |
Voxtral Realtime 4B Pure C speech-to-text inference engine
urls:
- https://github.com/mudler/voxtral.c
tags:
- audio-transcription
- CPU
- Metal
capabilities:
default: "cpu-voxtral"
metal-darwin-arm64: "metal-voxtral"
- &stablediffusionggml
name: "stablediffusion-ggml"
alias: "stablediffusion-ggml"
@@ -2594,3 +2609,24 @@
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-pocket-tts"
mirrors:
- localai/localai-backends:master-metal-darwin-arm64-pocket-tts
## voxtral
- !!merge <<: *voxtral
name: "cpu-voxtral"
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-voxtral"
mirrors:
- localai/localai-backends:latest-cpu-voxtral
- !!merge <<: *voxtral
name: "cpu-voxtral-development"
uri: "quay.io/go-skynet/local-ai-backends:master-cpu-voxtral"
mirrors:
- localai/localai-backends:master-cpu-voxtral
- !!merge <<: *voxtral
name: "metal-voxtral"
uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-voxtral"
mirrors:
- localai/localai-backends:latest-metal-darwin-arm64-voxtral
- !!merge <<: *voxtral
name: "metal-voxtral-development"
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-voxtral"
mirrors:
- localai/localai-backends:master-metal-darwin-arm64-voxtral

View File

@@ -115,6 +115,7 @@ Available pipelines: AnimateDiffPipeline, AnimateDiffVideoToVideoPipeline, ...
| Variable | Default | Description |
|----------|---------|-------------|
| `COMPEL` | `0` | Enable Compel for prompt weighting |
| `SD_EMBED` | `0` | Enable sd_embed for prompt weighting |
| `XPU` | `0` | Enable Intel XPU support |
| `CLIPSKIP` | `1` | Enable CLIP skip support |
| `SAFETENSORS` | `1` | Use safetensors format |

View File

@@ -40,6 +40,7 @@ from compel import Compel, ReturnedEmbeddingsType
from optimum.quanto import freeze, qfloat8, quantize
from transformers import T5EncoderModel
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
# Import LTX-2 specific utilities
from diffusers.pipelines.ltx2.export_utils import encode_video as ltx2_encode_video
@@ -47,6 +48,7 @@ from diffusers import LTX2VideoTransformer3DModel, GGUFQuantizationConfig
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
COMPEL = os.environ.get("COMPEL", "0") == "1"
SD_EMBED = os.environ.get("SD_EMBED", "0") == "1"
XPU = os.environ.get("XPU", "0") == "1"
CLIPSKIP = os.environ.get("CLIPSKIP", "1") == "1"
SAFETENSORS = os.environ.get("SAFETENSORS", "1") == "1"
@@ -737,6 +739,51 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
kwargs["prompt_embeds"] = conditioning
kwargs["pooled_prompt_embeds"] = pooled
# pass the kwargs dictionary to the self.pipe method
image = self.pipe(
guidance_scale=self.cfg_scale,
**kwargs
).images[0]
elif SD_EMBED:
if self.PipelineType == "StableDiffusionPipeline":
(
kwargs["prompt_embeds"],
kwargs["negative_prompt_embeds"],
) = get_weighted_text_embeddings_sd15(
pipe = self.pipe,
prompt = prompt,
neg_prompt = request.negative_prompt if hasattr(request, 'negative_prompt') else None,
)
if self.PipelineType == "StableDiffusionXLPipeline":
(
kwargs["prompt_embeds"],
kwargs["negative_prompt_embeds"],
kwargs["pooled_prompt_embeds"],
kwargs["negative_pooled_prompt_embeds"],
) = get_weighted_text_embeddings_sdxl(
pipe = self.pipe,
prompt = prompt,
neg_prompt = request.negative_prompt if hasattr(request, 'negative_prompt') else None
)
if self.PipelineType == "StableDiffusion3Pipeline":
(
kwargs["prompt_embeds"],
kwargs["negative_prompt_embeds"],
kwargs["pooled_prompt_embeds"],
kwargs["negative_pooled_prompt_embeds"],
) = get_weighted_text_embeddings_sd3(
pipe = self.pipe,
prompt = prompt,
neg_prompt = request.negative_prompt if hasattr(request, 'negative_prompt') else None
)
if self.PipelineType == "FluxTransformer2DModel":
(
kwargs["prompt_embeds"],
kwargs["pooled_prompt_embeds"],
) = get_weighted_text_embeddings_flux1(
pipe = self.pipe,
prompt = prompt,
)
image = self.pipe(
guidance_scale=self.cfg_scale,
**kwargs

View File

@@ -5,6 +5,7 @@ transformers
torchvision==0.22.1
accelerate
compel
git+https://github.com/xhinker/sd_embed
peft
sentencepiece
torch==2.7.1

View File

@@ -5,6 +5,7 @@ transformers
torchvision
accelerate
compel
git+https://github.com/xhinker/sd_embed
peft
sentencepiece
torch

View File

@@ -5,6 +5,7 @@ transformers
torchvision
accelerate
compel
git+https://github.com/xhinker/sd_embed
peft
sentencepiece
torch

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,11 @@ import sys
import os
import backend_pb2
import backend_pb2_grpc
import moonshine_onnx
from moonshine_voice import (
Transcriber,
get_model_for_language,
load_wav_file,
)
import grpc
@@ -25,16 +29,49 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
"""
BackendServicer is the class that implements the gRPC service
"""
def __init__(self):
self.transcriber = None
self.model_name = None
def Health(self, request, context):
return backend_pb2.Reply(message=bytes("OK", 'utf-8'))
def LoadModel(self, request, context):
try:
print("Preparing models, please wait", file=sys.stderr)
# Store the model name for use in transcription
# Model name format: e.g., "moonshine/tiny"
self.model_name = request.Model
print(f"Model name set to: {self.model_name}", file=sys.stderr)
# Default values
language = "en"
model_arch = None
# Parse options from request
options = request.Options
self.options = {}
# The options are a list of strings in this form optname:optvalue
for opt in options:
if ":" not in opt:
continue
key, value = opt.split(":", 1)
self.options[key] = value
print(f"Options: {self.options}", file=sys.stderr)
# Extract language and model_arch from options
if "language" in self.options:
language = self.options["language"]
if "model_arch" in self.options:
model_arch = self.options["model_arch"]
# Get the model path and architecture
model_path, model_arch = get_model_for_language(language, model_arch)
print(f"Loading model: {model_path} with architecture: {model_arch} for language: {language}", file=sys.stderr)
# Initialize the transcriber
self.transcriber = Transcriber(model_path=model_path, model_arch=model_arch)
print("Model loaded successfully", file=sys.stderr)
except Exception as err:
return backend_pb2.Result(success=False, message=f"Unexpected {err=}, {type(err)=}")
return backend_pb2.Result(message="Model loaded successfully", success=True)
@@ -43,33 +80,44 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
resultSegments = []
text = ""
try:
# moonshine_onnx.transcribe returns a list of strings
transcriptions = moonshine_onnx.transcribe(request.dst, self.model_name)
if self.transcriber is None:
raise Exception("Model not loaded. Call LoadModel first.")
# Load the audio file
audio_data, sample_rate = load_wav_file(request.dst)
print(f"Loaded audio file: {request.dst} with sample rate: {sample_rate}", file=sys.stderr)
# Transcribe without streaming
transcript = self.transcriber.transcribe_without_streaming(
audio_data, sample_rate=sample_rate, flags=0
)
# Process transcript lines
full_text_parts = []
for idx, line in enumerate(transcript.lines):
line_text = line.text.strip()
full_text_parts.append(line_text)
# Create segment with timing information
start_ms = int(line.start_time * 1000)
end_ms = int((line.start_time + line.duration) * 1000)
resultSegments.append(backend_pb2.TranscriptSegment(
id=idx,
start=start_ms,
end=end_ms,
text=line_text
))
print(f"Segment {idx}: [{line.start_time:.2f}s - {line.start_time + line.duration:.2f}s] {line_text}", file=sys.stderr)
# Combine all transcriptions into a single text
if isinstance(transcriptions, list):
text = " ".join(transcriptions)
# Create segments for each transcription in the list
for id, trans in enumerate(transcriptions):
# Since moonshine doesn't provide timing info, we'll create a single segment
# with id and text, using approximate timing
resultSegments.append(backend_pb2.TranscriptSegment(
id=id,
start=0,
end=0,
text=trans
))
else:
# Handle case where it's not a list (shouldn't happen, but be safe)
text = str(transcriptions)
resultSegments.append(backend_pb2.TranscriptSegment(
id=0,
start=0,
end=0,
text=text
))
text = " ".join(full_text_parts)
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}", file=sys.stderr)
import traceback
traceback.print_exc()
return backend_pb2.TranscriptResult(segments=[], text="")
return backend_pb2.TranscriptResult(segments=resultSegments, text=text)

View File

@@ -1,4 +1,4 @@
grpcio==1.71.0
protobuf
grpcio-tools
useful-moonshine-onnx@git+https://git@github.com/moonshine-ai/moonshine.git#subdirectory=moonshine-onnx
moonshine-voice

View File

@@ -1,4 +1,4 @@
grpcio==1.71.0
protobuf
grpcio-tools
useful-moonshine-onnx@git+https://git@github.com/moonshine-ai/moonshine.git#subdirectory=moonshine-onnx
moonshine-voice

View File

@@ -112,7 +112,7 @@ class TestBackendServicer(unittest.TestCase):
self.assertGreaterEqual(len(transcript_response.segments), 0)
# Verify the transcription contains the expected text
expected_text = "This is the micro machine man presenting the most midget miniature"
expected_text = "This is the micro machine man"
self.assertIn(
expected_text.lower(),
transcript_response.text.lower(),

View File

@@ -32,7 +32,14 @@ if [ "x${BUILD_PROFILE}" == "xl4t12" ]; then
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 ./

View File

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

View File

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

View File

@@ -9,7 +9,12 @@ else
fi
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
# This fixes the "Dimension out of range" error in scaled_dot_product_attention
# by changing .contiguous() to .unsqueeze(0) in the attention module

View File

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

View File

@@ -319,6 +319,29 @@ func loadRuntimeSettingsFromFile(options *config.ApplicationConfig) {
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 {
// Only apply if current value is default (0), suggesting it wasn't set from env var
if options.AgentJobRetentionDays == 0 {

View File

@@ -1,8 +1,6 @@
package application
import (
"time"
"github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/xlog"
)
@@ -37,11 +35,15 @@ func (a *Application) startWatchdog() error {
model.WithMemoryReclaimer(appConfig.MemoryReclaimerEnabled, appConfig.MemoryReclaimerThreshold),
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)
// Set the watchdog on the model loader
a.modelLoader.SetWatchDog(wd)
// Start watchdog goroutine if any periodic checks are enabled
// LRU eviction doesn't need the Run() loop - it's triggered on model load
// But memory reclaimer needs the Run() loop for periodic checking
@@ -49,15 +51,19 @@ func (a *Application) startWatchdog() error {
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() {
select {
case <-a.watchdogStop:
xlog.Debug("Watchdog stop signal received")
wd.Shutdown()
wdForShutdown.Shutdown()
case <-appConfig.Context.Done():
xlog.Debug("Context canceled, shutting down watchdog")
wd.Shutdown()
wdForShutdown.Shutdown()
}
}()
@@ -82,20 +88,41 @@ func (a *Application) RestartWatchdog() error {
a.watchdogMutex.Lock()
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 {
close(a.watchdogStop)
a.watchdogStop = nil
}
// Shutdown existing watchdog if running
currentWD := a.modelLoader.GetWatchDog()
if currentWD != nil {
currentWD.Shutdown()
// Wait a bit for shutdown to complete
time.Sleep(100 * time.Millisecond)
// Shutdown existing watchdog - this triggers the stop signal
if oldWD != nil {
oldWD.Shutdown()
// Wait for the old watchdog's Run() goroutine to fully shut down
oldWD.WaitDone()
}
// 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

@@ -83,7 +83,7 @@ type RunCMD struct {
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"`
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
}

View File

@@ -99,6 +99,10 @@ type AgentConfig struct {
EnablePlanning bool `yaml:"enable_planning,omitempty" json:"enable_planning,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"`
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) {
@@ -704,7 +708,7 @@ func (c *ModelConfig) BuildCogitoOptions() []cogito.Option {
// Apply agent configuration options
if c.Agent.EnableReasoning {
cogitoOpts = append(cogitoOpts, cogito.EnableToolReasoner)
cogitoOpts = append(cogitoOpts, cogito.WithForceReasoning())
}
if c.Agent.EnablePlanning {
@@ -727,5 +731,21 @@ func (c *ModelConfig) BuildCogitoOptions() []cogito.Option {
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
}

View File

@@ -76,42 +76,35 @@ func (lo *LoadOptions) Apply(options ...ConfigLoaderOption) {
}
}
// TODO: either in the next PR or the next commit, I want to merge these down into a single function that looks at the first few characters of the file to determine if we need to deserialize to []BackendConfig or BackendConfig
func readMultipleModelConfigsFromFile(file string, opts ...ConfigLoaderOption) ([]*ModelConfig, error) {
c := &[]*ModelConfig{}
// readModelConfigsFromFile reads a config file that may contain either a single
// ModelConfig or an array of ModelConfigs. It tries to unmarshal as an array first,
// then falls back to a single config if that fails.
func readModelConfigsFromFile(file string, opts ...ConfigLoaderOption) ([]*ModelConfig, error) {
f, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("readMultipleModelConfigsFromFile cannot read config file %q: %w", file, err)
}
if err := yaml.Unmarshal(f, c); err != nil {
return nil, fmt.Errorf("readMultipleModelConfigsFromFile cannot unmarshal config file %q: %w", file, err)
return nil, fmt.Errorf("readModelConfigsFromFile cannot read config file %q: %w", file, err)
}
for _, cc := range *c {
cc.modelConfigFile = file
cc.SetDefaults(opts...)
// Try to unmarshal as array first
var configs []*ModelConfig
if err := yaml.Unmarshal(f, &configs); err == nil && len(configs) > 0 {
for _, cc := range configs {
cc.modelConfigFile = file
cc.SetDefaults(opts...)
}
return configs, nil
}
return *c, nil
}
func readModelConfigFromFile(file string, opts ...ConfigLoaderOption) (*ModelConfig, error) {
lo := &LoadOptions{}
lo.Apply(opts...)
// Fall back to single config
c := &ModelConfig{}
f, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("readModelConfigFromFile cannot read config file %q: %w", file, err)
}
if err := yaml.Unmarshal(f, c); err != nil {
return nil, fmt.Errorf("readModelConfigFromFile cannot unmarshal config file %q: %w", file, err)
return nil, fmt.Errorf("readModelConfigsFromFile cannot unmarshal config file %q: %w", file, err)
}
c.SetDefaults(opts...)
c.modelConfigFile = file
return c, nil
c.SetDefaults(opts...)
return []*ModelConfig{c}, nil
}
// Load a config file for a model
@@ -163,7 +156,7 @@ func (bcl *ModelConfigLoader) LoadModelConfigFileByNameDefaultOptions(modelName
func (bcl *ModelConfigLoader) LoadMultipleModelConfigsSingleFile(file string, opts ...ConfigLoaderOption) error {
bcl.Lock()
defer bcl.Unlock()
c, err := readMultipleModelConfigsFromFile(file, opts...)
c, err := readModelConfigsFromFile(file, opts...)
if err != nil {
return fmt.Errorf("cannot load config file: %w", err)
}
@@ -181,11 +174,18 @@ func (bcl *ModelConfigLoader) LoadMultipleModelConfigsSingleFile(file string, op
func (bcl *ModelConfigLoader) ReadModelConfig(file string, opts ...ConfigLoaderOption) error {
bcl.Lock()
defer bcl.Unlock()
c, err := readModelConfigFromFile(file, opts...)
configs, err := readModelConfigsFromFile(file, opts...)
if err != nil {
return fmt.Errorf("ReadModelConfig cannot read config file %q: %w", file, err)
}
if len(configs) == 0 {
return fmt.Errorf("ReadModelConfig: no configs found in file %q", file)
}
if len(configs) > 1 {
xlog.Warn("ReadModelConig: read more than one config from file, only using first", "file", file, "configs", len(configs))
}
c := configs[0]
if valid, err := c.Validate(); valid {
bcl.configs[c.Name] = *c
} else {
@@ -375,15 +375,23 @@ func (bcl *ModelConfigLoader) LoadModelConfigsFromPath(path string, opts ...Conf
strings.HasPrefix(file.Name(), ".") {
continue
}
c, err := readModelConfigFromFile(filepath.Join(path, file.Name()), opts...)
filePath := filepath.Join(path, file.Name())
// Read config(s) - handles both single and array formats
configs, err := readModelConfigsFromFile(filePath, opts...)
if err != nil {
xlog.Error("LoadModelConfigsFromPath cannot read config file", "error", err, "File Name", file.Name())
continue
}
if valid, validationErr := c.Validate(); valid {
bcl.configs[c.Name] = *c
} else {
xlog.Error("config is not valid", "error", validationErr, "Name", c.Name)
// Validate and store each config
for _, c := range configs {
if valid, validationErr := c.Validate(); valid {
bcl.configs[c.Name] = *c
} else {
xlog.Error("config is not valid", "error", validationErr, "Name", c.Name)
}
}
}

View File

@@ -25,7 +25,8 @@ known_usecases:
- COMPLETION
`)
Expect(err).ToNot(HaveOccurred())
config, err := readModelConfigFromFile(tmp.Name())
configs, err := readModelConfigsFromFile(tmp.Name())
config := configs[0]
Expect(err).To(BeNil())
Expect(config).ToNot(BeNil())
valid, err := config.Validate()
@@ -43,7 +44,8 @@ backend: "foo-bar"
parameters:
model: "foo-bar"`)
Expect(err).ToNot(HaveOccurred())
config, err := readModelConfigFromFile(tmp.Name())
configs, err := readModelConfigsFromFile(tmp.Name())
config := configs[0]
Expect(err).To(BeNil())
Expect(config).ToNot(BeNil())
// two configs in config.yaml
@@ -62,7 +64,8 @@ parameters:
defer os.Remove(tmp.Name())
_, err = io.Copy(tmp, resp.Body)
Expect(err).To(BeNil())
config, err = readModelConfigFromFile(tmp.Name())
configs, err = readModelConfigsFromFile(tmp.Name())
config = configs[0]
Expect(err).To(BeNil())
Expect(config).ToNot(BeNil())
// two configs in config.yaml
@@ -188,7 +191,8 @@ mcp:
}
}`)
Expect(err).ToNot(HaveOccurred())
config, err := readModelConfigFromFile(tmp.Name())
configs, err := readModelConfigsFromFile(tmp.Name())
config := configs[0]
Expect(err).To(BeNil())
Expect(config).ToNot(BeNil())
valid, err := config.Validate()
@@ -218,7 +222,8 @@ mcp:
}
}`)
Expect(err).ToNot(HaveOccurred())
config, err := readModelConfigFromFile(tmp.Name())
configs, err := readModelConfigsFromFile(tmp.Name())
config := configs[0]
Expect(err).To(BeNil())
Expect(config).ToNot(BeNil())
valid, err := config.Validate()

View File

@@ -16,7 +16,7 @@ var _ = Describe("Test cases for config related functions", func() {
Context("Test Read configuration functions", func() {
configFile = os.Getenv("CONFIG_FILE")
It("Test readConfigFile", func() {
config, err := readMultipleModelConfigsFromFile(configFile)
config, err := readModelConfigsFromFile(configFile)
Expect(err).To(BeNil())
Expect(config).ToNot(BeNil())
// two configs in config.yaml

View File

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

View File

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

View File

@@ -4,11 +4,12 @@ import (
"os"
"path/filepath"
"dario.cat/mergo"
"github.com/mudler/LocalAI/core/config"
. "github.com/mudler/LocalAI/core/gallery"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
var _ = Describe("Gallery", func() {
@@ -462,4 +463,60 @@ var _ = Describe("Gallery", func() {
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

@@ -109,10 +109,10 @@ func API(application *application.Application) (*echo.Echo, error) {
res := c.Response()
err := next(c)
// Fix for #7989: Reduce log verbosity of Web UI polling
// If the path is /api/operations and the request was successful (200),
// Fix for #7989: Reduce log verbosity of Web UI polling and resources API
// If the path is /api/operations or /api/resources and the request was successful (200),
// we log it at DEBUG level (hidden by default) instead of INFO.
if req.URL.Path == "/api/operations" && res.Status == 200 {
if (req.URL.Path == "/api/operations" || req.URL.Path == "/api/resources") && res.Status == 200 {
xlog.Debug("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status)
} else {
xlog.Info("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status)

View File

@@ -336,6 +336,7 @@ var _ = Describe("API test", func() {
Name: "bert",
URL: bertEmbeddingsURL,
},
Overrides: map[string]interface{}{"backend": "llama-cpp"},
},
{
Metadata: gallery.Metadata{
@@ -953,7 +954,8 @@ parameters:
It("returns the models list", func() {
models, err := client.ListModels(context.TODO())
Expect(err).ToNot(HaveOccurred())
Expect(len(models.Models)).To(Equal(7)) // If "config.yaml" should be included, this should be 8?
// A model called "bert" can be present in the model directory depending on the order of the tests
Expect(len(models.Models)).To(BeNumerically(">=", 8))
})
It("can generate completions via ggml", func() {
if runtime.GOOS != "linux" {

View File

@@ -102,7 +102,7 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
// Build fragment from messages
fragment := cogito.NewEmptyFragment()
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)
@@ -162,11 +162,6 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
return err
}
f, err = defaultLLM.Ask(ctxWithCancellation, f)
if err != nil {
return err
}
resp := &schema.OpenAIResponse{
ID: id,
Created: created,
@@ -252,17 +247,6 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
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
content := f.LastMessage().Content
events <- MCPAssistantEvent{

View File

@@ -23,10 +23,15 @@ import (
"github.com/mudler/LocalAI/core/backend"
model "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/utils"
"github.com/mudler/xlog"
)
func downloadFile(url string) (string, error) {
if err := utils.ValidateExternalURL(url); err != nil {
return "", fmt.Errorf("URL validation failed: %w", err)
}
// Get the data
resp, err := http.Get(url)
if err != nil {

View File

@@ -27,18 +27,36 @@ import (
model "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/reasoning"
"github.com/mudler/LocalAI/pkg/sound"
"github.com/mudler/LocalAI/pkg/utils"
"github.com/mudler/xlog"
)
const (
localSampleRate = 16000
remoteSampleRate = 24000
// 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
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
// If the model support instead audio-to-audio, we will use the specific gRPC calls instead
// LockedWebsocket wraps a websocket connection with a mutex for safe concurrent writes
type LockedWebsocket struct {
*websocket.Conn
sync.Mutex
}
func (l *LockedWebsocket) WriteMessage(messageType int, data []byte) error {
l.Lock()
defer l.Unlock()
return l.Conn.WriteMessage(messageType, data)
}
// Session represents a single WebSocket connection and its state
type Session struct {
ID string
@@ -58,7 +76,9 @@ type Session struct {
DefaultConversationID string
ModelInterface Model
// The pipeline model config or the config for an any-to-any model
ModelConfig *config.ModelConfig
ModelConfig *config.ModelConfig
InputSampleRate int
MaxOutputTokens types.IntOrInf
}
func (s *Session) FromClient(session *types.SessionUnion) {
@@ -80,12 +100,13 @@ func (s *Session) ToServer() types.SessionUnion {
} else {
return types.SessionUnion{
Realtime: &types.RealtimeSession{
ID: s.ID,
Object: "realtime.session",
Model: s.Model,
Instructions: s.Instructions,
Tools: s.Tools,
ToolChoice: s.ToolChoice,
ID: s.ID,
Object: "realtime.session",
Model: s.Model,
Instructions: s.Instructions,
Tools: s.Tools,
ToolChoice: s.ToolChoice,
MaxOutputTokens: s.MaxOutputTokens,
Audio: &types.RealtimeSessionAudio{
Input: &types.SessionAudioInput{
TurnDetection: s.TurnDetection,
@@ -153,6 +174,9 @@ func Realtime(application *application.Application) echo.HandlerFunc {
}
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
model := c.QueryParam("model")
@@ -162,7 +186,8 @@ func Realtime(application *application.Application) echo.HandlerFunc {
}
func registerRealtime(application *application.Application, model string) func(c *websocket.Conn) {
return func(c *websocket.Conn) {
return func(conn *websocket.Conn) {
c := &LockedWebsocket{Conn: conn}
evaluator := application.TemplatesEvaluator()
xlog.Debug("Realtime WebSocket connection established", "address", c.RemoteAddr().String(), "model", model)
@@ -183,14 +208,13 @@ func registerRealtime(application *application.Application, model string) func(c
}
sttModel := cfg.Pipeline.Transcription
ttsModel := cfg.Pipeline.TTS
sessionID := generateSessionID()
session := &Session{
ID: sessionID,
TranscriptionOnly: false,
Model: model,
Voice: ttsModel,
Voice: cfg.TTSConfig.Voice,
ModelConfig: cfg,
TurnDetection: &types.TurnDetectionUnion{
ServerVad: &types.ServerVad{
@@ -203,7 +227,8 @@ func registerRealtime(application *application.Application, model string) func(c
InputAudioTranscription: &types.AudioTranscription{
Model: sttModel,
},
Conversations: make(map[string]*Conversation),
Conversations: make(map[string]*Conversation),
InputSampleRate: defaultRemoteSampleRate,
}
// Create a default conversation
@@ -355,8 +380,17 @@ func registerRealtime(application *application.Application, model string) func(c
continue
}
// Append to InputAudioBuffer
// Check buffer size limits before appending
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.AudioBufferLock.Unlock()
@@ -383,7 +417,36 @@ func registerRealtime(application *application.Application, model string) func(c
case types.ConversationItemCreateEvent:
xlog.Debug("recv", "message", string(msg))
sendNotImplemented(c, "conversation.item.create")
// Add the item to the conversation
item := e.Item
// Ensure IDs are present
if item.User != nil && item.User.ID == "" {
item.User.ID = generateItemID()
}
if item.Assistant != nil && item.Assistant.ID == "" {
item.Assistant.ID = generateItemID()
}
if item.System != nil && item.System.ID == "" {
item.System.ID = generateItemID()
}
if item.FunctionCall != nil && item.FunctionCall.ID == "" {
item.FunctionCall.ID = generateItemID()
}
if item.FunctionCallOutput != nil && item.FunctionCallOutput.ID == "" {
item.FunctionCallOutput.ID = generateItemID()
}
conversation.Lock.Lock()
conversation.Items = append(conversation.Items, &item)
conversation.Lock.Unlock()
sendEvent(c, types.ConversationItemAddedEvent{
ServerEventBase: types.ServerEventBase{
EventID: e.EventID,
},
PreviousItemID: e.PreviousItemID,
Item: item,
})
case types.ConversationItemDeleteEvent:
sendError(c, "not_implemented", "Deleting items not implemented", "", "event_TODO")
@@ -429,7 +492,34 @@ func registerRealtime(application *application.Application, model string) func(c
case types.ResponseCreateEvent:
xlog.Debug("recv", "message", string(msg))
sendNotImplemented(c, "response.create")
// Handle optional items to add to context
if len(e.Response.Input) > 0 {
conversation.Lock.Lock()
for _, item := range e.Response.Input {
// Ensure IDs are present
if item.User != nil && item.User.ID == "" {
item.User.ID = generateItemID()
}
if item.Assistant != nil && item.Assistant.ID == "" {
item.Assistant.ID = generateItemID()
}
if item.System != nil && item.System.ID == "" {
item.System.ID = generateItemID()
}
if item.FunctionCall != nil && item.FunctionCall.ID == "" {
item.FunctionCall.ID = generateItemID()
}
if item.FunctionCallOutput != nil && item.FunctionCallOutput.ID == "" {
item.FunctionCallOutput.ID = generateItemID()
}
conversation.Items = append(conversation.Items, &item)
}
conversation.Lock.Unlock()
}
go triggerResponse(session, conversation, c, &e.Response)
case types.ResponseCancelEvent:
xlog.Debug("recv", "message", string(msg))
@@ -456,7 +546,7 @@ func registerRealtime(application *application.Application, model string) func(c
}
// Helper function to send events to the client
func sendEvent(c *websocket.Conn, event types.ServerEvent) {
func sendEvent(c *LockedWebsocket, event types.ServerEvent) {
eventBytes, err := json.Marshal(event)
if err != nil {
xlog.Error("failed to marshal event", "error", err)
@@ -468,7 +558,7 @@ func sendEvent(c *websocket.Conn, event types.ServerEvent) {
}
// Helper function to send errors to the client
func sendError(c *websocket.Conn, code, message, param, eventID string) {
func sendError(c *LockedWebsocket, code, message, param, eventID string) {
errorEvent := types.ErrorEvent{
ServerEventBase: types.ServerEventBase{
EventID: eventID,
@@ -485,7 +575,7 @@ func sendError(c *websocket.Conn, code, message, param, eventID string) {
sendEvent(c, errorEvent)
}
func sendNotImplemented(c *websocket.Conn, message string) {
func sendNotImplemented(c *LockedWebsocket, message string) {
sendError(c, "not_implemented", message, "", "event_TODO")
}
@@ -530,6 +620,12 @@ func updateTransSession(session *Session, update *types.SessionUnion, cl *config
session.TurnDetection = update.Transcription.Audio.Input.TurnDetection
}
if update.Transcription.Audio.Input.Format != nil && update.Transcription.Audio.Input.Format.PCM != nil {
if update.Transcription.Audio.Input.Format.PCM.Rate > 0 {
session.InputSampleRate = update.Transcription.Audio.Input.Format.PCM.Rate
}
}
return nil
}
@@ -557,13 +653,13 @@ func updateSession(session *Session, update *types.SessionUnion, cl *config.Mode
session.InputAudioTranscription = &types.AudioTranscription{}
}
session.InputAudioTranscription.Model = cfg.Pipeline.Transcription
session.Voice = cfg.Pipeline.TTS
session.Voice = cfg.TTSConfig.Voice
session.Model = rt.Model
session.ModelConfig = cfg
}
if rt.Audio != nil && rt.Audio.Output != nil && rt.Audio.Output.Voice != "" {
xlog.Warn("Ignoring voice setting; not implemented", "voice", rt.Audio.Output.Voice)
session.Voice = string(rt.Audio.Output.Voice)
}
if rt.Audio != nil && rt.Audio.Input != nil && rt.Audio.Input.Transcription != nil {
@@ -583,6 +679,12 @@ func updateSession(session *Session, update *types.SessionUnion, cl *config.Mode
session.TurnDetection = rt.Audio.Input.TurnDetection
}
if rt.Audio != nil && rt.Audio.Input != nil && rt.Audio.Input.Format != nil && rt.Audio.Input.Format.PCM != nil {
if rt.Audio.Input.Format.PCM.Rate > 0 {
session.InputSampleRate = rt.Audio.Input.Format.PCM.Rate
}
}
if rt.Instructions != "" {
session.Instructions = rt.Instructions
}
@@ -594,12 +696,16 @@ func updateSession(session *Session, update *types.SessionUnion, cl *config.Mode
session.ToolChoice = rt.ToolChoice
}
if rt.MaxOutputTokens != 0 {
session.MaxOutputTokens = rt.MaxOutputTokens
}
return nil
}
// handleVAD is a goroutine that listens for audio data from the client,
// runs VAD on the audio data, and commits utterances to the conversation
func handleVAD(session *Session, conv *Conversation, c *websocket.Conn, done chan struct{}) {
func handleVAD(session *Session, conv *Conversation, c *LockedWebsocket, done chan struct{}) {
vadContext, cancel := context.WithCancel(context.Background())
go func() {
<-done
@@ -628,12 +734,12 @@ func handleVAD(session *Session, conv *Conversation, c *websocket.Conn, done cha
session.AudioBufferLock.Unlock()
aints := sound.BytesToInt16sLE(allAudio)
if len(aints) == 0 || len(aints) < int(silenceThreshold)*remoteSampleRate {
if len(aints) == 0 || len(aints) < int(silenceThreshold)*session.InputSampleRate {
continue
}
// Resample from 24kHz to 16kHz
aints = sound.ResampleInt16(aints, remoteSampleRate, localSampleRate)
// Resample from InputSampleRate to 16kHz
aints = sound.ResampleInt16(aints, session.InputSampleRate, localSampleRate)
segments, err := runVAD(vadContext, session, aints)
if err != nil {
@@ -649,18 +755,18 @@ func handleVAD(session *Session, conv *Conversation, c *websocket.Conn, done cha
audioLength := float64(len(aints)) / localSampleRate
// 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 {
session.AudioBufferLock.Lock()
session.InputAudioBuffer = nil
session.AudioBufferLock.Unlock()
xlog.Debug("Detected silence for a while, clearing audio buffer")
sendEvent(c, types.InputAudioBufferClearedEvent{
ServerEventBase: types.ServerEventBase{
EventID: "event_TODO",
},
})
// NOTE: OpenAI doesn't send this message unless the client requests it
// xlog.Debug("Detected silence for a while, clearing audio buffer")
// sendEvent(c, types.InputAudioBufferClearedEvent{
// ServerEventBase: types.ServerEventBase{
// EventID: "event_TODO",
// },
// })
continue
} else if len(segments) == 0 {
@@ -713,7 +819,7 @@ func handleVAD(session *Session, conv *Conversation, c *websocket.Conn, done cha
}
}
func commitUtterance(ctx context.Context, utt []byte, session *Session, conv *Conversation, c *websocket.Conn) {
func commitUtterance(ctx context.Context, utt []byte, session *Session, conv *Conversation, c *LockedWebsocket) {
if len(utt) == 0 {
return
}
@@ -746,6 +852,10 @@ func commitUtterance(ctx context.Context, utt []byte, session *Session, conv *Co
tr, err := session.ModelInterface.Transcribe(ctx, f.Name(), session.InputAudioTranscription.Language, false, false, session.InputAudioTranscription.Prompt)
if err != nil {
sendError(c, "transcription_failed", err.Error(), "", "event_TODO")
return
} else if tr == nil {
sendError(c, "transcription_failed", "trancribe result is nil", "", "event_TODO")
return
}
transcript = tr.Text
@@ -791,11 +901,10 @@ func runVAD(ctx context.Context, session *Session, adata []int16) ([]schema.VADS
}
// Function to generate a response based on the conversation
func generateResponse(session *Session, utt []byte, transcript string, conv *Conversation, c *websocket.Conn, mt int) {
func generateResponse(session *Session, utt []byte, transcript string, conv *Conversation, c *LockedWebsocket, mt int) {
xlog.Debug("Generating realtime response...")
config := session.ModelInterface.PredictConfig()
// Create user message item
item := types.MessageItemUnion{
User: &types.MessageItemUser{
ID: generateItemID(),
@@ -817,33 +926,100 @@ func generateResponse(session *Session, utt []byte, transcript string, conv *Con
Item: item,
})
triggerResponse(session, conv, c, nil)
}
func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, overrides *types.ResponseCreateParams) {
config := session.ModelInterface.PredictConfig()
// Default values
tools := session.Tools
toolChoice := session.ToolChoice
instructions := session.Instructions
maxOutputTokens := session.MaxOutputTokens
// Overrides
if overrides != nil {
if overrides.Tools != nil {
tools = overrides.Tools
}
if overrides.ToolChoice != nil {
toolChoice = overrides.ToolChoice
}
if 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
conversationHistory = append(conversationHistory, schema.Message{
Role: string(types.MessageRoleSystem),
StringContent: session.Instructions,
Content: session.Instructions,
StringContent: instructions,
Content: instructions,
})
imgIndex := 0
conv.Lock.Lock()
for _, item := range conv.Items {
if item.User != nil {
msg := schema.Message{
Role: string(types.MessageRoleUser),
}
textContent := ""
nrOfImgsInMessage := 0
for _, content := range item.User.Content {
switch content.Type {
case types.MessageContentTypeInputText:
conversationHistory = append(conversationHistory, schema.Message{
Role: string(types.MessageRoleUser),
StringContent: content.Text,
Content: content.Text,
})
textContent += content.Text
case types.MessageContentTypeInputAudio:
conversationHistory = append(conversationHistory, schema.Message{
Role: string(types.MessageRoleUser),
StringContent: content.Transcript,
Content: content.Transcript,
StringAudios: []string{content.Audio},
})
textContent += content.Transcript
case types.MessageContentTypeInputImage:
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++
nrOfImgsInMessage++
}
}
if nrOfImgsInMessage > 0 {
templated, err := templates.TemplateMultiModal(config.TemplateConfig.Multimodal, templates.MultiModalOptions{
TotalImages: imgIndex,
ImagesInMessage: nrOfImgsInMessage,
}, textContent)
if err != nil {
xlog.Warn("Failed to apply multimodal template", "error", err)
templated = textContent
}
msg.StringContent = templated
msg.Content = templated
} else {
msg.StringContent = textContent
msg.Content = textContent
}
conversationHistory = append(conversationHistory, msg)
} else if item.Assistant != nil {
for _, content := range item.Assistant.Content {
switch content.Type {
@@ -874,6 +1050,11 @@ func generateResponse(session *Session, utt []byte, transcript string, conv *Con
}
conv.Lock.Unlock()
var images []string
for _, m := range conversationHistory {
images = append(images, m.StringImages...)
}
responseID := generateUniqueID()
sendEvent(c, types.ResponseCreatedEvent{
ServerEventBase: types.ServerEventBase{},
@@ -884,26 +1065,47 @@ func generateResponse(session *Session, utt []byte, transcript string, conv *Con
},
})
predFunc, err := session.ModelInterface.Predict(context.TODO(), conversationHistory, nil, nil, nil, nil, session.Tools, session.ToolChoice, nil, nil, nil)
predFunc, err := session.ModelInterface.Predict(context.TODO(), conversationHistory, images, nil, nil, nil, tools, toolChoice, nil, nil, nil)
if err != nil {
sendError(c, "inference_failed", fmt.Sprintf("backend error: %v", err), "", item.Assistant.ID)
sendError(c, "inference_failed", fmt.Sprintf("backend error: %v", err), "", "") // item.Assistant.ID is unknown here
return
}
pred, err := predFunc()
if err != nil {
sendError(c, "prediction_failed", fmt.Sprintf("backend error: %v", err), "", item.Assistant.ID)
sendError(c, "prediction_failed", fmt.Sprintf("backend error: %v", err), "", "")
return
}
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
if config.TemplateConfig.ReplyPrefix != "" {
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)
textContent := functions.ParseTextContent(responseWithoutReasoning, config.FunctionsConfig)
@@ -1006,7 +1208,16 @@ func generateResponse(session *Session, utt []byte, transcript string, conv *Con
sendError(c, "tts_error", fmt.Sprintf("Failed to read TTS audio: %v", err), "", item.Assistant.ID)
return
}
audioString := base64.StdEncoding.EncodeToString(audioBytes)
// Strip WAV header (44 bytes) to get raw PCM data
// The OpenAI Realtime API expects raw PCM, not WAV files
const wavHeaderSize = 44
pcmData := audioBytes
if len(audioBytes) > wavHeaderSize {
pcmData = audioBytes[wavHeaderSize:]
}
audioString := base64.StdEncoding.EncodeToString(pcmData)
sendEvent(c, types.ResponseOutputAudioTranscriptDeltaEvent{
ServerEventBase: types.ServerEventBase{},
@@ -1131,7 +1342,6 @@ func generateResponse(session *Session, utt []byte, transcript string, conv *Con
Status: types.ResponseStatusCompleted,
},
})
}
// Helper functions to generate unique IDs

View File

@@ -194,7 +194,40 @@ func (m *wrappedModel) Predict(ctx context.Context, messages schema.Messages, im
var toolsJSON string
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)
}

View File

@@ -26,6 +26,7 @@ const (
MessageContentTypeTranscript MessageContentType = "transcript"
MessageContentTypeInputText MessageContentType = "input_text"
MessageContentTypeInputAudio MessageContentType = "input_audio"
MessageContentTypeInputImage MessageContentType = "input_image"
MessageContentTypeOutputText MessageContentType = "output_text"
MessageContentTypeOutputAudio MessageContentType = "output_audio"
)

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).
Description string `json:"description"`
// The type of the tool, i.e. function.
Parameters any `json:"parameters"`
// The jsonschema representing the parameters
Parameters any `json:"parameters,omitempty"`
}
func (t ToolFunction) ToolType() ToolType {

View File

@@ -927,7 +927,7 @@ func handleBackgroundMCPResponse(ctx context.Context, store *ResponseStore, resp
// Build fragment from messages
fragment := cogito.NewEmptyFragment()
for _, message := range openAIReq.Messages {
fragment = fragment.AddMessage(message.Role, message.StringContent)
fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
}
fragmentPtr := &fragment
@@ -1004,12 +1004,6 @@ func handleBackgroundMCPNonStream(ctx context.Context, store *ResponseStore, res
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
fPtr := &f
outputItems := convertCogitoFragmentToORItems(fPtr)
@@ -1186,21 +1180,6 @@ func handleBackgroundMCPStream(ctx context.Context, store *ResponseStore, respon
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
content := f.LastMessage().Content
messageID := fmt.Sprintf("msg_%s", uuid.New().String())
@@ -2505,7 +2484,7 @@ func handleMCPResponse(c echo.Context, responseID string, createdAt int64, input
// Build fragment from messages
fragment := cogito.NewEmptyFragment()
for _, message := range openAIReq.Messages {
fragment = fragment.AddMessage(message.Role, message.StringContent)
fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
}
fragmentPtr := &fragment
@@ -2580,12 +2559,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), "")
}
// 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
fPtr := &f
outputItems := convertCogitoFragmentToORItems(fPtr)
@@ -2730,17 +2703,6 @@ func handleMCPStream(c echo.Context, responseID string, createdAt int64, input *
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
content := f.LastMessage().Content
messageID := fmt.Sprintf("msg_%s", uuid.New().String())

View File

@@ -418,6 +418,337 @@ textarea.input-success {
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
============================================ */
@@ -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,71 @@
/* Layout Structure */
html {
height: 100%;
}
body {
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);
}
/* 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-messages { overflow-y: auto; flex-grow: 1; }
.htmx-indicator{

View File

@@ -1,12 +1,18 @@
/* LocalAI Theme - CSS Variables System */
/* Based on logo color palette: cyan, teal, navy, purple */
:root {
/* Base Colors */
--color-bg-primary: #0F172A; /* Deep navy background */
--color-bg-secondary: #1E293B; /* Elevated surfaces */
--color-bg-tertiary: #1E293B; /* Cards, panels */
--color-bg-overlay: rgba(15, 23, 42, 0.8); /* Modals, overlays */
/* Dark Theme (Default) - Charcoal Gray Style */
:root,
[data-theme="dark"],
.dark {
/* Base Colors - Charcoal Gray */
--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 */
--color-primary: #38BDF8; /* Cyan - primary actions */
@@ -32,16 +38,16 @@
--color-text-secondary: #94A3B8; /* Secondary text */
--color-text-muted: #64748B; /* Tertiary 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 */
--color-border-subtle: rgba(148, 163, 184, 0.08); /* Minimal borders */
--color-border-default: rgba(148, 163, 184, 0.12); /* Default borders */
--color-border-strong: rgba(56, 189, 248, 0.2); /* Focus borders */
--color-border-divider: rgba(148, 163, 184, 0.06); /* Section dividers */
--color-border-primary: rgba(56, 189, 248, 0.15); /* Primary borders (reduced opacity) */
--color-border-secondary: rgba(148, 163, 184, 0.1);
--color-border-focus: rgba(56, 189, 248, 0.3); /* Focus borders (reduced) */
/* Border Colors - Visible on charcoal */
--color-border-subtle: rgba(255, 255, 255, 0.08); /* Minimal borders */
--color-border-default: rgba(255, 255, 255, 0.12); /* Default borders */
--color-border-strong: rgba(56, 189, 248, 0.3); /* Focus borders */
--color-border-divider: rgba(255, 255, 255, 0.05); /* Section dividers */
--color-border-primary: rgba(56, 189, 248, 0.2); /* Primary borders */
--color-border-secondary: rgba(255, 255, 255, 0.1);
--color-border-focus: rgba(56, 189, 248, 0.4); /* Focus borders */
/* Status Colors */
--color-success: #14B8A6; /* Use teal for success (aligned with logo) */
@@ -55,17 +61,18 @@
/* Gradient Definitions */
--gradient-primary: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
--gradient-hero: linear-gradient(135deg, #0F172A 0%, #1E293B 50%, #0F172A 100%);
--gradient-card: linear-gradient(135deg, rgba(56, 189, 248, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%);
--gradient-hero: linear-gradient(135deg, #121212 0%, #1A1A1A 50%, #121212 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%);
/* Shadows - Minimal System */
/* Shadows - Charcoal theme */
--shadow-none: none;
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.1);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-glow: 0 0 0 1px rgba(56, 189, 248, 0.1), 0 0 8px rgba(56, 189, 248, 0.15); /* Minimal glow */
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.35);
--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 */
--duration-instant: 100ms;
@@ -109,5 +116,83 @@
--width-5xl: 64rem; /* 1024px */
--width-6xl: 72rem; /* 1152px */
--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,30 +2,31 @@
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow">
<!-- Error Section -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-10">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8 mb-10">
<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>
</div>
<h1 class="hero-title mb-4">
404 - Page Not Found
</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">
<a href="./"
class="inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] font-semibold py-3 px-6 rounded-lg transition-colors">
class="btn-primary">
<i class="fas fa-home mr-2"></i>
<span>Return Home</span>
</a>
<a href="browse/"
class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white font-semibold py-3 px-6 rounded-lg transition-colors">
class="btn-secondary">
<i class="fas fa-images mr-2"></i>
<span>Browse Gallery</span>
</a>
@@ -33,19 +34,20 @@
</div>
</div>
<!-- Additional Information -->
<div class="bg-[#1E293B] border border-[#1E293B] rounded-xl p-8">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8">
<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">
<i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i>
<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-[var(--color-warning)] text-2xl fa-solid fa-triangle-exclamation"></i>
</div>
<h2 class="text-2xl md:text-3xl font-semibold text-[#E5E7EB] 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>
<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-[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>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>

View File

@@ -2,10 +2,12 @@
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen" x-data="jobDetails()" x-init="init()">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{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">
<!-- Header -->
@@ -17,7 +19,7 @@
</h1>
<p class="hero-subtitle">Live job status, reasoning traces, and execution details</p>
</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
</a>
</div>
@@ -26,7 +28,7 @@
<!-- Job Status Card -->
<div class="card p-8 mb-8">
<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">
<span :class="{
'bg-yellow-500': job.status === 'pending',
@@ -48,64 +50,64 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="text-[#94A3B8] text-sm">Job ID</label>
<div class="font-mono text-[#E5E7EB] mt-1" x-text="job.id || '-'"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Job ID</label>
<div class="font-mono text-[var(--color-text-primary)] mt-1" x-text="job.id || '-'"></div>
</div>
<div>
<label class="text-[#94A3B8] text-sm">Task</label>
<div class="text-[#E5E7EB] mt-1" x-text="task ? task.name : (job.task_id || '-')"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Task</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="task ? task.name : (job.task_id || '-')"></div>
</div>
<div>
<label class="text-[#94A3B8] text-sm">Created</label>
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.created_at)"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Created</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.created_at)"></div>
</div>
<div>
<label class="text-[#94A3B8] text-sm">Started</label>
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.started_at)"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Started</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.started_at)"></div>
</div>
<div>
<label class="text-[#94A3B8] text-sm">Completed</label>
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.completed_at)"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Completed</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.completed_at)"></div>
</div>
<div>
<label class="text-[#94A3B8] text-sm">Triggered By</label>
<div class="text-[#E5E7EB] mt-1" x-text="job.triggered_by || '-'"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Triggered By</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="job.triggered_by || '-'"></div>
</div>
</div>
</div>
<!-- Agent Prompt Template -->
<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>
<p class="text-sm text-[#94A3B8] 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>
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Agent Prompt Template</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">The original prompt template from the task definition.</p>
<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>
<!-- 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">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Parameters</h2>
<p class="text-sm text-[#94A3B8] 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>
<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-[var(--color-text-primary)] mb-6">Cron Parameters</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">Parameters configured for cron-triggered executions of this task.</p>
<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>
<!-- 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">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Job Parameters</h2>
<p class="text-sm text-[#94A3B8] 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>
<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-[var(--color-text-primary)] mb-6">Job Parameters</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">Parameters used for this specific job execution.</p>
<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>
<!-- Rendered Job 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>
<p class="text-sm text-[#94A3B8] 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>
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Rendered Job Prompt</h2>
<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-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap" x-text="getRenderedPrompt()"></div>
</div>
<!-- Result -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.result">
<h2 class="text-2xl font-semibold text-[#E5E7EB] 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-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.result">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Result</h2>
<div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap" x-text="job.result"></div>
</div>
<!-- Error -->
@@ -115,18 +117,18 @@
</div>
<!-- Reasoning Traces & Actions -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Execution Traces</h2>
<div x-show="!traces || traces.length === 0" class="text-[#94A3B8] text-center py-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-[var(--color-text-primary)] mb-6">Execution Traces</h2>
<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>
<p>No execution traces available yet. Traces will appear here as the job executes.</p>
</div>
<div x-show="traces && traces.length > 0" class="space-y-4">
<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 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"
:class="{
'bg-blue-500/20 text-blue-400': trace.type === 'reasoning',
@@ -136,14 +138,14 @@
}"
x-text="trace.type"></span>
</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 class="text-[#E5E7EB] text-sm" x-text="trace.content"></div>
<div x-show="trace.tool_name" class="mt-2 text-xs text-[#94A3B8]">
<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-[var(--color-text-secondary)]">
<span class="font-semibold">Tool:</span> <span x-text="trace.tool_name"></span>
</div>
<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>
</template>
@@ -151,16 +153,16 @@
</div>
<!-- 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">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhook Status</h2>
<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-[var(--color-text-primary)] mb-6">Webhook Status</h2>
<div class="space-y-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>
</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>
<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 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">
@@ -320,7 +322,9 @@
}
}
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}}
<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()">
{{template "views/partials/navbar" .}}
<div class="app-layout">
{{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">
<!-- Header -->
@@ -140,13 +142,13 @@
</h3>
<div class="space-y-3">
<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">
<i class="fas fa-cube text-[var(--color-primary)]"></i>
<span class="text-[var(--color-text-primary)] font-medium" x-text="model.name"></span>
</div>
<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>
Configure MCP
</a>
@@ -759,7 +761,9 @@
}
}
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -2,20 +2,22 @@
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen" x-data="taskDetails()" x-init="init()">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{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">
<!-- 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>
<h1 class="hero-title">
<span x-text="isNewTask ? 'Create Task' : (isEditMode ? 'Edit Task' : 'Task Details')"></span>
</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 class="flex space-x-3">
<template x-if="!isNewTask && !isEditMode">
@@ -37,7 +39,7 @@
<template x-if="isEditMode || isNewTask">
<div class="flex space-x-3">
<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
</button>
<button @click="saveTask()"
@@ -46,7 +48,7 @@
</button>
</div>
</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
</a>
</div>
@@ -57,62 +59,62 @@
<template x-if="isEditMode || isNewTask">
<form @submit.prevent="saveTask()" class="space-y-8">
<!-- Basic Information -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Basic Information</h2>
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Basic Information</h2>
<div class="space-y-6">
<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
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>
<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"
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>
<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
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>
{{ range .ModelsConfig }}
{{ $cfg := . }}
{{ $hasMCP := or (ne $cfg.MCP.Servers "") (ne $cfg.MCP.Stdio "") }}
{{ 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 }}
</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>
<label class="flex items-center">
<input type="checkbox" x-model="taskForm.enabled"
class="mr-2">
<span class="text-[#E5E7EB]">Enabled</span>
<span class="text-[var(--color-text-primary)]">Enabled</span>
</label>
</div>
</div>
</div>
<!-- Prompt Template -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Prompt Template</h2>
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Prompt Template</h2>
<div>
<label class="block text-[#E5E7EB] mb-2">Prompt *</label>
<p class="text-sm text-[#94A3B8] 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.
<label class="block text-[var(--color-text-primary)] mb-2">Prompt *</label>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
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.
</p>
<!-- Example Prompt -->
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4 mb-4">
<p class="text-xs text-[#94A3B8] 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{{"}}"}}.
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4 mb-4">
<p class="text-xs text-[var(--color-text-secondary)] mb-2 font-semibold">Example Prompt:</p>
<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{{"}}"}}
@@ -121,8 +123,8 @@ Provide a detailed response that addresses their specific needs.</pre>
<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."
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>
<p class="text-xs text-[#94A3B8] mt-2">
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-[var(--color-text-secondary)] mt-2">
<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.
</p>
@@ -130,25 +132,25 @@ Provide a detailed response that addresses their specific needs.</pre>
</div>
<!-- Cron Schedule -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Schedule (Optional)</h2>
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Cron Schedule (Optional)</h2>
<div class="space-y-6">
<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"
x-model="taskForm.cron"
@blur="validateCron(taskForm.cron)"
@input="cronError = ''"
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'">
<p class="text-sm text-[#94A3B8] mt-1">Standard 5-field cron format (minute hour day month weekday)</p>
: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-[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>
</div>
<!-- Cron Parameters -->
<div>
<label class="block text-[#E5E7EB] mb-2">Cron Parameters (Optional)</label>
<p class="text-sm text-[#94A3B8] mb-3">
<label class="block text-[var(--color-text-primary)] mb-2">Cron Parameters (Optional)</label>
<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.
Enter as key-value pairs (one per line, format: key=value).
</p>
@@ -156,27 +158,27 @@ Provide a detailed response that addresses their specific needs.</pre>
@input="updateCronParameters()"
rows="6"
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>
<p class="text-xs text-[#94A3B8] mt-1">
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-[var(--color-text-secondary)] mt-1">
<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>
</div>
</div>
</div>
<!-- Multimedia Sources Configuration -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Multimedia Sources (Optional)</h2>
<p class="text-sm text-[#94A3B8] mb-4">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Multimedia Sources (Optional)</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
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.
</p>
<div class="space-y-4">
<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">
<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)"
class="text-red-400 hover:text-red-300">
<i class="fas fa-trash"></i>
@@ -184,9 +186,9 @@ Provide a detailed response that addresses their specific needs.</pre>
</div>
<div class="space-y-4">
<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
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="image">Image</option>
<option value="video">Video</option>
@@ -195,40 +197,40 @@ Provide a detailed response that addresses their specific needs.</pre>
</select>
</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
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">
<p class="text-xs text-[#94A3B8] mt-1">URL where multimedia content will be fetched from</p>
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-[var(--color-text-secondary)] mt-1">URL where multimedia content will be fetched from</p>
</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"
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>
<p class="text-xs text-[#94A3B8] mt-1">Custom headers for the HTTP request (e.g., Authorization)</p>
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-[var(--color-text-secondary)] mt-1">Custom headers for the HTTP request (e.g., Authorization)</p>
</div>
</div>
</div>
</template>
<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
</button>
</div>
</div>
<!-- Webhook Configuration -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhooks (Optional)</h2>
<p class="text-sm text-[#94A3B8] mb-4">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhooks (Optional)</h2>
<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.
</p>
<div class="space-y-4">
<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">
<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)"
class="text-red-400 hover:text-red-300">
<i class="fas fa-trash"></i>
@@ -236,35 +238,35 @@ Provide a detailed response that addresses their specific needs.</pre>
</div>
<div class="space-y-4">
<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
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">
<p class="text-xs text-[#94A3B8] mt-1">URL where webhook notifications will be sent</p>
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-[var(--color-text-secondary)] mt-1">URL where webhook notifications will be sent</p>
</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"
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="PUT">PUT</option>
<option value="PATCH">PATCH</option>
</select>
</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"
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>
<p class="text-xs text-[#94A3B8] mt-1">Custom headers for the webhook request (e.g., Authorization)</p>
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-[var(--color-text-secondary)] mt-1">Custom headers for the webhook request (e.g., Authorization)</p>
</div>
<div>
<label class="block text-[#E5E7EB] 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-[#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>
<div class="bg-[#0A0E1A] border border-[#38BDF8]/10 rounded-lg p-3 mb-2">
<p class="text-xs text-[#94A3B8] mb-1 font-semibold">Example (Slack with error handling):</p>
<pre class="text-xs text-[#E5E7EB] font-mono whitespace-pre-wrap">{
<label class="block text-[var(--color-text-primary)] mb-2">Custom Payload Template (Optional)</label>
<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-[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-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/10 rounded-lg p-3 mb-2">
<p class="text-xs text-[var(--color-text-secondary)] mb-1 font-semibold">Example (Slack with error handling):</p>
<pre class="text-xs text-[var(--color-text-primary)] font-mono whitespace-pre-wrap">{
"text": "Job {{.Job.ID}} {{if .Error}}failed{{else}}completed{{end}}",
"blocks": [
{
@@ -279,13 +281,13 @@ Provide a detailed response that addresses their specific needs.</pre>
</div>
<textarea x-model="webhook.payload_template" rows="5"
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>
</template>
<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
</button>
</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) -->
<div x-show="!isEditMode && !isNewTask" x-cloak>
<!-- Task Information -->
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Task Information</h2>
<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-[var(--color-text-primary)] mb-6">Task Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="text-[#94A3B8] text-sm">Name</label>
<div class="text-[#E5E7EB] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Name</label>
<div class="text-[var(--color-text-primary)] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></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">
<span :class="task && task.enabled ? 'bg-green-500' : 'bg-gray-500'"
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>
<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">
<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>
<a :href="task ? '/models/edit/' + task.model : '#'"
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>
<label class="text-[#94A3B8] text-sm">Cron Schedule</label>
<div class="text-[#E5E7EB] mt-1 font-mono text-sm" x-text="task && task.cron ? task.cron : '-'"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Cron Schedule</label>
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm" x-text="task && task.cron ? task.cron : '-'"></div>
</div>
<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">
<template x-for="(value, key) in task.cron_parameters" :key="key">
<div class="text-[#E5E7EB] text-sm mb-1">
<span class="font-semibold text-[#38BDF8]" x-text="key + ':'"></span>
<div class="text-[var(--color-text-primary)] text-sm mb-1">
<span class="font-semibold text-[var(--color-primary)]" x-text="key + ':'"></span>
<span x-text="value"></span>
</div>
</template>
</div>
</div>
<div class="md:col-span-2">
<label class="text-[#94A3B8] text-sm">Description</label>
<div class="text-[#E5E7EB] mt-1" x-text="task && task.description ? task.description : 'No description'"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Description</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="task && task.description ? task.description : 'No description'"></div>
</div>
<div class="md:col-span-2">
<label class="text-[#94A3B8] 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>
<label class="text-[var(--color-text-secondary)] text-sm">Prompt Template</label>
<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>
<!-- API Usage Examples -->
<div class="bg-[#1E293B] border border-[#38BDF8]/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>
<p class="text-sm text-[#94A3B8] mb-4">
<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-[var(--color-text-primary)] mb-6">API Usage Examples</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
Use these curl commands to interact with this task programmatically.
</p>
<div class="space-y-6">
<!-- Execute Task by ID -->
<div>
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
<i class="fas fa-play text-[#38BDF8] mr-2"></i>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
<i class="fas fa-play text-[var(--color-primary)] mr-2"></i>
Execute Task by ID
</h3>
<div class="bg-[#101827] border border-[#38BDF8]/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 \
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<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 "Authorization: Bearer YOUR_API_KEY" \
-d '{
@@ -382,12 +384,12 @@ Provide a detailed response that addresses their specific needs.</pre>
<!-- Execute Task by Name -->
<div>
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
<i class="fas fa-code text-[#38BDF8] mr-2"></i>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
<i class="fas fa-code text-[var(--color-primary)] mr-2"></i>
Execute Task by Name
</h3>
<div class="bg-[#101827] border border-[#38BDF8]/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 \
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<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 "Authorization: Bearer YOUR_API_KEY" \
-d '{
@@ -396,7 +398,7 @@ Provide a detailed response that addresses their specific needs.</pre>
"task_description": "Analyze sales data"
}'</code></pre>
</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>
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.
@@ -405,12 +407,12 @@ Provide a detailed response that addresses their specific needs.</pre>
<!-- Execute Task with Multimedia -->
<div>
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
<i class="fas fa-images text-[#38BDF8] mr-2"></i>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
<i class="fas fa-images text-[var(--color-primary)] mr-2"></i>
Execute Task with Multimedia (Images)
</h3>
<div class="bg-[#101827] border border-[#38BDF8]/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 \
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<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 "Authorization: Bearer YOUR_API_KEY" \
-d '{
@@ -425,53 +427,53 @@ Provide a detailed response that addresses their specific needs.</pre>
]
}'</code></pre>
</div>
<p class="text-xs text-[#94A3B8] 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>.
<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-[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>
</div>
<!-- Check Job Status -->
<div>
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
<i class="fas fa-info-circle text-[#38BDF8] mr-2"></i>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
<i class="fas fa-info-circle text-[var(--color-primary)] mr-2"></i>
Check Job Status
</h3>
<div class="bg-[#101827] border border-[#38BDF8]/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 \
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
<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>
</div>
<p class="text-xs text-[#94A3B8] 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.
<p class="text-xs text-[var(--color-text-secondary)] mt-2">
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>
</div>
</div>
</div>
<!-- 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">
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhook Configuration</h2>
<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-[var(--color-text-primary)] mb-6">Webhook Configuration</h2>
<div class="space-y-4">
<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">
<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 class="space-y-3">
<div>
<label class="text-[#94A3B8] text-sm">URL</label>
<div class="text-[#E5E7EB] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div>
<label class="text-[var(--color-text-secondary)] text-sm">URL</label>
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div>
</div>
<div>
<label class="text-[#94A3B8] text-sm">Method</label>
<div class="text-[#E5E7EB] mt-1 font-mono text-sm" x-text="webhook.method || 'POST'"></div>
<label class="text-[var(--color-text-secondary)] text-sm">Method</label>
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm" x-text="webhook.method || 'POST'"></div>
</div>
<div x-show="webhook.headers && Object.keys(webhook.headers).length > 0">
<label class="text-[#94A3B8] 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>
<label class="text-[var(--color-text-secondary)] text-sm">Headers</label>
<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 x-show="webhook.payload_template">
<label class="text-[#94A3B8] 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>
<label class="text-[var(--color-text-secondary)] text-sm">Payload Template</label>
<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>
@@ -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) -->
<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">
<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">
<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="pending">Pending</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">
<table class="w-full">
<thead>
<tr class="border-b border-[#38BDF8]/20">
<th class="text-left py-3 px-4 text-[#94A3B8]">Job ID</th>
<th class="text-left py-3 px-4 text-[#94A3B8]">Status</th>
<th class="text-left py-3 px-4 text-[#94A3B8]">Created</th>
<th class="text-left py-3 px-4 text-[#94A3B8]">Triggered By</th>
<th class="text-left py-3 px-4 text-[#94A3B8]">Actions</th>
<tr class="border-b border-[var(--color-primary)]/20">
<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-[var(--color-text-secondary)]">Status</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-[var(--color-text-secondary)]">Triggered By</th>
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Actions</th>
</tr>
</thead>
<tbody>
<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">
<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) + '...'"
:title="job.id"></a>
</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"
x-text="job.status"></span>
</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-[#94A3B8] text-sm" x-text="job.triggered_by || '-'"></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-[var(--color-text-secondary)] text-sm" x-text="job.triggered_by || '-'"></td>
<td class="py-3 px-4">
<button x-show="job.status === 'pending' || job.status === 'running'"
@click="cancelJob(job.id)"
@@ -546,7 +548,7 @@ Provide a detailed response that addresses their specific needs.</pre>
</tr>
</template>
<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>
</tbody>
</table>
@@ -560,11 +562,11 @@ Provide a detailed response that addresses their specific needs.</pre>
x-cloak
@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">
<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="flex justify-between items-center p-8 pb-6 border-b border-[#38BDF8]/20">
<h3 class="text-2xl font-semibold text-[#E5E7EB]">Execute Task</h3>
<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-[var(--color-primary)]/20">
<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'"
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>
</button>
</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-1 overflow-y-auto px-8 py-6 space-y-4">
<div>
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Task</label>
<div class="text-[#94A3B8]" x-text="task.name"></div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Task</label>
<div class="text-[var(--color-text-secondary)]" x-text="task.name"></div>
</div>
<!-- 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">
<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">
Parameters
</button>
<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">
Multimedia
</button>
@@ -594,75 +596,75 @@ Provide a detailed response that addresses their specific needs.</pre>
<!-- Parameters Tab -->
<div x-show="executeModalTab === 'parameters'">
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Parameters</label>
<p class="text-xs text-[#94A3B8] mb-3">
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Parameters</label>
<p class="text-xs text-[var(--color-text-secondary)] mb-3">
Enter parameters as key-value pairs (one per line, format: key=value).
These will be used to template the prompt.
</p>
<textarea x-model="executionParametersText"
rows="6"
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>
<p class="text-xs text-[#94A3B8] mt-1">
Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">user_name=Alice</code>
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-[var(--color-text-secondary)] mt-1">
Example: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">user_name=Alice</code>
</p>
</div>
<!-- Multimedia Tab -->
<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.
</p>
<!-- Images -->
<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"
rows="3"
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
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>
<!-- Videos -->
<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"
rows="3"
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
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>
<!-- Audios -->
<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"
rows="3"
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
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>
<!-- Files -->
<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"
rows="3"
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
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 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'"
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
</button>
<button @click="executeTaskWithParameters()"
@@ -1135,6 +1137,9 @@ Provide a detailed response that addresses their specific needs.</pre>
}
}
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -2,10 +2,12 @@
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen" x-data="backendsGallery()">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="backendsGallery()">
<!-- Notifications -->
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
@@ -44,20 +46,20 @@
Discover and install AI backends to power your models
</p>
<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="w-2 h-2 bg-emerald-400 rounded-full mr-2"></div>
<span class="font-semibold text-emerald-300" x-text="availableBackends"></span>
<span class="text-[#94A3B8] ml-1">backends available</span>
<div class="flex items-center bg-[var(--color-bg-primary)] rounded-lg px-4 py-2">
<div class="w-2 h-2 bg-[var(--color-success)] rounded-full mr-2"></div>
<span class="font-semibold text-[var(--color-success)]" x-text="availableBackends"></span>
<span class="text-[var(--color-text-secondary)] ml-1">backends available</span>
</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">
<div class="w-2 h-2 bg-cyan-400 rounded-full mr-2"></div>
<span class="font-semibold text-cyan-300" x-text="installedBackends"></span>
<span class="text-[#94A3B8] ml-1">installed</span>
<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-[var(--color-primary)] rounded-full mr-2"></div>
<span class="font-semibold text-[var(--color-primary)]" x-text="installedBackends"></span>
<span class="text-[var(--color-text-secondary)] ml-1">installed</span>
</a>
<div class="flex items-center bg-[#101827] rounded-lg px-4 py-2 border border-[#38BDF8]/30">
<i class="fas fa-microchip text-[#38BDF8] mr-2"></i>
<span class="text-[#94A3B8] mr-1">Capability:</span>
<span class="font-semibold text-[#38BDF8]" x-text="systemCapability"></span>
<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-[var(--color-primary)] mr-2"></i>
<span class="text-[var(--color-text-secondary)] mr-1">Capability:</span>
<span class="font-semibold text-[var(--color-primary)]" x-text="systemCapability"></span>
</div>
<a href="https://localai.io/backends/" target="_blank" class="btn-primary">
<i class="fas fa-info-circle mr-2"></i>
@@ -77,41 +79,41 @@
class="w-full flex items-center justify-between text-left"
>
<div class="flex items-center gap-2">
<i class="fas fa-plus-circle text-[#38BDF8] text-lg"></i>
<h3 class="text-lg font-semibold text-[#E5E7EB]">Install Backend Manually</h3>
<i class="fas fa-plus-circle text-[var(--color-primary)] text-lg"></i>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Install Backend Manually</h3>
</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>
<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>
<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
type="text"
x-model="externalBackend.uri"
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>
<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
type="text"
x-model="externalBackend.name"
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>
<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
type="text"
x-model="externalBackend.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>
@@ -120,12 +122,12 @@
<button
@click="installExternalBackend()"
: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="btn-primary"
>
<i class="mr-2" :class="installingExternal ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
<span x-text="installingExternal ? 'Installing...' : 'Install Backend'"></span>
</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>
@@ -135,13 +137,13 @@
<div>
<!-- Search Input -->
<div class="mb-8">
<h3 class="text-xl font-semibold text-[#E5E7EB] mb-4 flex items-center">
<i class="fas fa-search mr-3 text-[#8B5CF6]"></i>
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
<i class="fas fa-search mr-3 text-[var(--color-accent)]"></i>
Find Backend Components
</h3>
<div class="relative">
<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>
<input
x-model="searchTerm"
@@ -151,7 +153,7 @@
type="search"
placeholder="Search backends by name, description or type...">
<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>
<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>
@@ -161,33 +163,33 @@
<!-- Filter by Type -->
<div>
<h3 class="text-lg font-semibold text-white mb-4 flex items-center">
<i class="fas fa-filter mr-3 text-teal-400"></i>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
<i class="fas fa-filter mr-3 text-[var(--color-secondary)]"></i>
Filter by Backend Type
</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
<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>
<span>LLM</span>
</button>
<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>
<span>Diffusion</span>
</button>
<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>
<span>TTS</span>
</button>
<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>
<span>Whisper</span>
</button>
<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>
<span>Vision</span>
</button>
@@ -199,97 +201,97 @@
<!-- Results Section -->
<div id="search-results" class="transition-all duration-300">
<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>
<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>
<p class="text-gray-400">Loading backends...</p>
<p class="text-[var(--color-text-secondary)]">Loading backends...</p>
</div>
<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>
<p class="text-gray-400">No backends found matching your criteria</p>
<i class="fas fa-search text-[var(--color-text-muted)] text-4xl mb-4"></i>
<p class="text-[var(--color-text-secondary)]">No backends found matching your criteria</p>
</div>
<!-- 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">
<table class="w-full">
<thead>
<tr class="bg-gradient-to-r from-[#38BDF8]/20 to-[#8B5CF6]/20 border-b border-[#38BDF8]/30">
<th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Icon</th>
<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-[var(--color-text-primary)] uppercase tracking-wider">Icon</th>
<th @click="setSort('name')"
:class="sortBy === 'name' ? 'bg-[#38BDF8]/20' : ''"
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="sortBy === 'name' ? 'bg-[var(--color-primary-light)]' : ''"
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">
<span>Backend Name</span>
<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>
</div>
</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')"
:class="sortBy === 'repository' ? 'bg-[#38BDF8]/20' : ''"
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="sortBy === 'repository' ? 'bg-[var(--color-primary-light)]' : ''"
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">
<span>Repository</span>
<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>
</div>
</th>
<th @click="setSort('license')"
:class="sortBy === 'license' ? 'bg-[#38BDF8]/20' : ''"
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="sortBy === 'license' ? 'bg-[var(--color-primary-light)]' : ''"
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">
<span>License</span>
<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>
</div>
</th>
<th @click="setSort('status')"
:class="sortBy === 'status' ? 'bg-[#38BDF8]/20' : ''"
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="sortBy === 'status' ? 'bg-[var(--color-primary-light)]' : ''"
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">
<span>Status</span>
<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>
</div>
</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>
</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">
<tr class="hover:bg-[#38BDF8]/10 transition-colors duration-200">
<tr class="hover:bg-[var(--color-bg-primary)] transition-colors duration-200">
<!-- Icon -->
<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"
:src="backend.icon"
class="w-full h-full object-cover rounded-lg"
loading="lazy"
: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>
</td>
<!-- Backend Name -->
<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>
<!-- Description -->
<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>
<!-- Repository -->
<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>
<span x-text="backend.gallery"></span>
</span>
@@ -297,21 +299,21 @@
<!-- License -->
<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>
<span x-text="backend.license"></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>
<!-- Status -->
<td class="px-6 py-4">
<!-- Processing State -->
<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>
</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
</div>
<div class="progress-table mt-1">
@@ -321,7 +323,7 @@
<!-- Installed State -->
<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>
Installed
</span>
@@ -329,7 +331,7 @@
<!-- Not Installed State -->
<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>
Not Installed
</span>
@@ -341,7 +343,7 @@
<div class="flex items-center justify-end gap-2">
<!-- Info Button -->
<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">
<i class="fas fa-info-circle"></i>
</button>
@@ -350,12 +352,12 @@
<template x-if="!backend.processing && backend.installed">
<div class="flex gap-2">
<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">
<i class="fa-solid fa-arrow-rotate-right"></i>
</button>
<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">
<i class="fa-solid fa-trash"></i>
</button>
@@ -365,7 +367,7 @@
<!-- Not Installed State Actions -->
<template x-if="!backend.processing && !backend.installed">
<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">
<i class="fa-solid fa-download"></i>
</button>
@@ -383,15 +385,15 @@
<div x-show="selectedBackend"
x-transition
@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;">
<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 -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white" x-text="selectedBackend?.name"></h3>
<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-[var(--color-text-primary)]" x-text="selectedBackend?.name"></h3>
<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">
<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>
@@ -401,21 +403,21 @@
<!-- Modal Body -->
<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="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"
:src="selectedBackend?.icon"
class="rounded-lg max-h-48 max-w-96 object-cover"
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 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">
<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">
<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>
<span x-text="tag"></span>
</span>
@@ -425,11 +427,11 @@
</template>
<template x-if="selectedBackend?.urls && selectedBackend.urls.length > 0">
<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>
<template x-for="url in selectedBackend.urls" :key="url">
<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>
<span x-text="url"></span>
</a>
@@ -440,9 +442,9 @@
</template>
</div>
<!-- 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()"
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
</button>
</div>
@@ -453,30 +455,29 @@
<!-- Pagination -->
<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)"
:disabled="currentPage <= 1"
: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>
</button>
<div class="text-gray-300 text-sm font-medium px-4">
<span class="text-gray-400">Page</span>
<span class="text-white font-bold text-lg mx-2" x-text="currentPage"></span>
<span class="text-gray-400">of</span>
<span class="text-white font-bold text-lg mx-2" x-text="totalPages"></span>
<div class="text-[var(--color-text-primary)] text-sm font-medium px-4">
<span class="text-[var(--color-text-secondary)]">Page</span>
<span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="currentPage"></span>
<span class="text-[var(--color-text-secondary)]">of</span>
<span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="totalPages"></span>
</div>
<button @click="goToPage(currentPage + 1)"
:disabled="currentPage >= totalPages"
: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>
</button>
</div>
</div>
</div>
{{template "views/partials/footer" .}}
</div>
<style>
@@ -516,16 +517,16 @@
/* Table progress bar styling */
.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: 1px solid rgba(56, 189, 248, 0.3);
border: 1px solid var(--color-primary-border);
height: 6px;
overflow: hidden;
width: 100%;
}
.progress-bar-table-backend {
background: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 100%);
background: var(--gradient-primary);
height: 100%;
transition: width 0.3s ease;
}
@@ -534,6 +535,7 @@
table {
border-collapse: separate;
border-spacing: 0;
background: var(--color-bg-secondary);
}
tbody tr:last-child td:first-child {
@@ -905,5 +907,10 @@ function backendsGallery() {
}
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -587,19 +587,25 @@ SOFTWARE.
<script defer src="static/chat.js"></script>
{{ $allGalleryConfigs:=.GalleryConfig }}
{{ $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 }">
{{template "views/partials/navbar" .}}
<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 })">
<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 -->
<div class="flex flex-1 overflow-hidden relative">
<!-- Sidebar -->
<!-- Main container with settings panel -->
<div class="flex flex-1 min-h-0 relative">
<!-- 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
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="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
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="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">
<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
href="https://localai.io/features/text-generation/"
target="_blank"
@@ -609,10 +615,10 @@ SOFTWARE.
</a>
</div>
<button
@click="sidebarOpen = false"
@click="settingsPanelOpen = false"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none text-xs"
title="Hide sidebar">
<i class="fa-solid fa-chevron-left"></i>
title="Hide settings">
<i class="fa-solid fa-chevron-right"></i>
</button>
</div>
@@ -1099,61 +1105,57 @@ SOFTWARE.
</div>
</div>
<!-- Main chat container (shifts with sidebar) -->
<!-- Main chat container (shifts with settings panel on desktop only; on mobile panel overlays) -->
<div
class="flex-1 flex flex-col transition-all duration-300 ease-in-out"
:class="sidebarOpen ? 'ml-56' : 'ml-0'">
class="flex-1 flex flex-col min-h-0 transition-all duration-300 ease-in-out"
:class="settingsPanelOpen ? 'md:mr-56' : 'mr-0'">
<!-- Chat header with toggle button -->
<div class="border-b border-[var(--color-bg-secondary)] p-4 flex items-center justify-between">
<div class="flex items-center">
<!-- Sidebar toggle button moved to be the first element in the header and with clear styling -->
<button
@click="sidebarOpen = !sidebarOpen"
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"
style="min-width: 36px;"
title="Toggle settings">
<i class="fa-solid" :class="sidebarOpen ? 'fa-chevron-left' : 'fa-bars'"></i>
</button>
<div class="flex items-center">
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)]"></i>
<!-- Model icon - reactive to active chat -->
<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">
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2">
<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 min-w-0 flex-1">
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)] flex-shrink-0"></i>
<!-- Model icon - reactive to active chat -->
<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">
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2 flex-shrink-0">
</template>
<!-- Fallback icon for initial model from server (when no active chat yet) -->
<template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon">
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2 flex-shrink-0">
</template>
<h1 class="text-lg font-semibold text-[var(--color-text-primary)] truncate min-w-0">
Chat
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model">
<span x-text="' with ' + $store.chat.activeChat().model"></span>
</template>
<!-- Fallback icon for initial model from server (when no active chat yet) -->
<template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon">
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2">
<template x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model">
{{ if .Model }}<span> with {{.Model}}</span>{{ end }}
</template>
<h1 class="text-lg font-semibold text-[var(--color-text-primary)]">
Chat
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model">
<span x-text="' with ' + $store.chat.activeChat().model"></span>
</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>
</h1>
<!-- Loading indicator next to model name -->
<div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)] flex-shrink-0" style="display: none;">
<i class="fas fa-spinner fa-spin text-sm"></i>
</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 flex-shrink-0">
<button
@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"
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">
<i class="fa-solid fa-broom"></i>
</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>
<!-- Clear Chat Alert -->
<!-- Clear Chat Alert (bottom on mobile to avoid covering header) -->
<div x-show="showClearAlert"
x-transition:enter="transition ease-out duration-300"
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-start="opacity-100"
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="flex items-center gap-2">
<i class="fa-solid fa-check-circle text-[var(--color-primary)]"></i>
@@ -1172,7 +1174,8 @@ SOFTWARE.
</div>
<!-- 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)]">
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">
@@ -1367,11 +1370,11 @@ SOFTWARE.
</div>
</template>
</div>
</div>
</div>
<!-- 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">
<!-- 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">
@@ -1391,38 +1394,38 @@ SOFTWARE.
</template>
</div>
<!-- Token Usage and Context Window - Compact above input -->
<div class="mb-3 flex items-center justify-between gap-4 text-xs">
<!-- Token Usage -->
<div class="flex items-center gap-3 text-[var(--color-text-secondary)]">
<div class="flex items-center gap-1">
<!-- Token Usage and Context Window - responsive: two rows on mobile -->
<div class="mb-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3 text-xs">
<!-- Token Usage (wraps on mobile) -->
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-[var(--color-text-secondary)]">
<div class="flex items-center gap-1 max-md:hidden">
<i class="fas fa-chart-line text-[var(--color-primary)]"></i>
<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>
</div>
<div class="flex items-center gap-1">
<div class="flex items-center gap-1 max-md:hidden">
<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>
</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-text-primary)] font-bold" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
</div>
<!-- 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>
<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>
</div>
</div>
<!-- Context Window -->
<!-- Context Window (second row on mobile) -->
<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>
<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>
<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>
</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">
<textarea
id="input"
name="input"
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..."
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
@keydown.shift="shiftPressed = true"
@keyup.shift="shiftPressed = false"
@keydown.enter.prevent="if (!shiftPressed) { submitPrompt($event); }"
rows="2"
></textarea>
<button
type="button"
onclick="document.getElementById('input_image').click()"
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"
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>
<!-- Attachment buttons (desktop only - inside input) -->
<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>
<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>
<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>
<!-- Send button and stop button in the same position -->
<div class="absolute right-3 top-3 flex items-center">
@@ -1483,7 +1484,7 @@ SOFTWARE.
id="stop-button"
type="button"
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;"
title="Stop request"
>
@@ -1494,7 +1495,7 @@ SOFTWARE.
<button
id="send-button"
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)"
>
<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 -->
<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 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 -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 id="model-info-modal-title" class="text-xl font-semibold text-gray-900 dark:text-white">{{ 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(); }">
<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-[var(--color-text-primary)]">{{ if $model }}{{ $model }}{{ end }}</h3>
<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">
<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>
@@ -1553,16 +1554,16 @@ SOFTWARE.
<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"/>
</div>
<div id="model-info-description" class="text-base leading-relaxed text-gray-500 dark:text-gray-400 break-words max-w-full"></div>
<hr>
<p class="text-sm font-semibold text-gray-900 dark:text-white">Links</p>
<div id="model-info-description" class="text-base leading-relaxed text-[var(--color-text-secondary)] break-words max-w-full"></div>
<hr class="border-[var(--color-border-subtle)]">
<p class="text-sm font-semibold text-[var(--color-text-primary)]">Links</p>
<ul id="model-info-links">
</ul>
</div>
<!-- Footer -->
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
<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(); }">
<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-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
</button>
</div>
@@ -1874,7 +1875,7 @@ SOFTWARE.
let backdrop = document.querySelector('.modal-backdrop');
if (!backdrop) {
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);
backdrop.addEventListener('click', () => {
closeModelInfoModal();
@@ -1962,7 +1963,7 @@ SOFTWARE.
let backdrop = document.querySelector('.modal-backdrop');
if (!backdrop) {
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);
backdrop.addEventListener('click', () => {
window.closeModelInfoModal();
@@ -2065,8 +2066,8 @@ SOFTWARE.
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre;
background: #101827 !important;
border: 1px solid #1E293B;
background: var(--color-bg-primary) !important;
border: 1px solid var(--color-border-subtle);
border-radius: 6px;
padding: 12px;
margin: 0;
@@ -2079,7 +2080,7 @@ SOFTWARE.
overflow-wrap: break-word;
white-space: pre;
background: transparent !important;
color: #E5E7EB;
color: var(--color-text-primary);
font-family: 'ui-monospace', 'Monaco', 'Consolas', monospace;
font-size: 0.875rem;
line-height: 1.5;
@@ -2182,13 +2183,13 @@ SOFTWARE.
height: 6px;
}
.sidebar::-webkit-scrollbar-track,
.chat-settings-panel::-webkit-scrollbar-track,
#chat::-webkit-scrollbar-track,
#messages::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb,
.chat-settings-panel::-webkit-scrollbar-thumb,
#chat::-webkit-scrollbar-thumb,
#messages::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.2);
@@ -2196,14 +2197,14 @@ SOFTWARE.
transition: background 0.2s ease;
}
.sidebar::-webkit-scrollbar-thumb:hover,
.chat-settings-panel::-webkit-scrollbar-thumb:hover,
#chat::-webkit-scrollbar-thumb:hover,
#messages::-webkit-scrollbar-thumb:hover {
background: rgba(148, 163, 184, 0.4);
}
/* Firefox - Minimal */
.sidebar,
.chat-settings-panel,
#chat,
#messages {
scrollbar-width: thin;
@@ -2234,5 +2235,8 @@ SOFTWARE.
scrollbar-color: rgba(148, 163, 184, 0.15) transparent;
}
</style>
</div>
</main>
</div>
</body>
</html>

View File

@@ -2,30 +2,31 @@
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow">
<!-- Error Section -->
<div class="bg-[#1E293B] border border-red-500/20 rounded-xl p-8 mb-10">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-error)]/20 rounded-xl p-8 mb-10">
<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>
</div>
<h1 class="hero-title mb-4" style="color: var(--color-error);">
{{if .ErrorCode}}{{.ErrorCode}}{{else}}Error{{end}}
</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">
<a href="./"
class="inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] font-semibold py-3 px-6 rounded-lg transition-colors">
class="btn-primary">
<i class="fas fa-home mr-2"></i>
<span>Return Home</span>
</a>
<a href="browse/"
class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white font-semibold py-3 px-6 rounded-lg transition-colors">
class="btn-secondary">
<i class="fas fa-images mr-2"></i>
<span>Browse Gallery</span>
</a>
@@ -33,20 +34,21 @@
</div>
</div>
<!-- Additional Information -->
<div class="bg-[#1E293B] border border-[#1E293B] rounded-xl p-8">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8">
<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">
<i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i>
<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-[var(--color-warning)] text-2xl fa-solid fa-triangle-exclamation"></i>
</div>
<h2 class="text-2xl md:text-3xl font-semibold text-[#E5E7EB] 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>
<h2 class="text-2xl md:text-3xl font-semibold text-[var(--color-text-primary)] mb-4">Need help?</h2>
<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>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -5,8 +5,8 @@
<style>
body {
background-color: #101827;
color: #E5E7EB;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.token {
@@ -19,51 +19,53 @@
position: relative;
}
.network-card {
background-color: #2d3748;
background-color: var(--color-bg-secondary);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid var(--color-border-subtle);
transition: background-color 0.2s ease;
}
.network-card:hover {
background-color: #374151;
background-color: var(--color-bg-tertiary);
}
.network-title {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
color: #63b3ed;
color: var(--color-primary);
}
.network-token {
font-size: 14px;
font-style: italic;
color: #cbd5e0;
color: var(--color-text-secondary);
margin-bottom: 10px;
word-break: break-word; /* Breaks words to prevent overflow */
overflow-wrap: break-word; /* Ensures long strings break */
white-space: pre-wrap; /* Preserves whitespace for breaking */
word-break: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
}
.cluster {
margin-top: 10px;
background-color: #4a5568;
background-color: var(--color-bg-tertiary);
padding: 10px;
border-radius: 6px;
border: 1px solid var(--color-border-subtle);
transition: background-color 0.3s ease;
}
.cluster:hover {
background-color: #5a6b78;
background-color: var(--color-bg-secondary);
}
.cluster-title {
font-size: 18px;
font-weight: bold;
color: #e2e8f0;
color: var(--color-text-primary);
}
.form-container {
background-color: #2d3748;
background-color: var(--color-bg-secondary);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid var(--color-border-subtle);
}
.form-control {
margin-bottom: 15px;
@@ -72,47 +74,50 @@
display: block;
margin-bottom: 5px;
font-weight: bold;
color: var(--color-text-primary);
}
input[type="text"],
textarea {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid #4a5568;
background-color: #3a4250;
color: #e2e8f0;
border: 1px solid var(--color-border-subtle);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
transition: border-color 0.3s ease, background-color 0.3s ease;
}
input[type="text"]:focus,
textarea:focus {
border-color: #63b3ed;
background-color: #4a5568;
border-color: var(--color-primary);
background-color: var(--color-bg-tertiary);
}
button {
background-color: #3182ce;
color: #e2e8f0;
background-color: var(--color-primary);
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: var(--color-primary-hover);
}
.error {
color: #e53e3e;
color: var(--color-error);
margin-top: 5px;
}
.success {
color: #38a169;
color: var(--color-success);
margin-top: 5px;
}
/* Spinner Styles */
.spinner {
display: inline-block;
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.2);
border: 5px solid var(--color-border-subtle);
border-radius: 50%;
border-top-color: #3182ce;
border-top-color: var(--color-primary);
animation: spin 1s linear infinite;
margin: 0 auto;
}
@@ -121,43 +126,46 @@
to { transform: rotate(360deg); }
}
/* Center the loading text and spinner */
.loading-container {
text-align: center;
padding: 50px;
}
.warning-box {
border-radius: 5px;
border-radius: 5px;
}
.warning-box i {
margin-right: 10px;
}
.token-box {
background-color: #4a5568;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
position: relative;
cursor: pointer;
}
.token-box:hover {
background-color: #5a6b7e;
}
.token-text {
overflow-wrap: break-word;
font-family: monospace;
}
.copy-icon {
position: absolute;
top: 10px;
right: 10px;
color: #e2e8f0;
}
background-color: var(--color-bg-tertiary);
padding: 10px;
border-radius: 4px;
margin-top: 10px;
position: relative;
cursor: pointer;
border: 1px solid var(--color-border-subtle);
}
.token-box:hover {
background-color: var(--color-bg-secondary);
}
.token-text {
overflow-wrap: break-word;
font-family: monospace;
}
.copy-icon {
position: absolute;
top: 10px;
right: 10px;
color: var(--color-text-primary);
}
</style>
<body class="bg-gray-900 text-gray-200">
<div class="flex flex-col min-h-screen" x-data="networkClusters()" x-init="init()">
{{template "views/partials/navbar_explorer" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{template "views/partials/navbar_explorer" .}}
<main class="main-content">
<div class="main-content-inner" x-data="networkClusters()" x-init="init()">
<div class="animation-container">
<canvas id="networkCanvas"></canvas>
<div class="text-overlay">
@@ -178,8 +186,8 @@
<div class="container mx-auto px-4 flex-grow">
<!-- Warning Box -->
<div class="warning-box bg-yellow-100 text-gray-800 mb-20 pt-5 pb-5 pr-5 pl-5 text-lg">
<i class="fa-solid fa-triangle-exclamation"></i><i class="fa-solid fa-flask"></i>
<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 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.
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.
@@ -259,19 +267,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>
<!-- 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'" >
<p class="text-lg font-bold mb-4 mt-1">
<i class="fa-solid fa-copy copy-icon float-right"></i>
Command to connect (click to copy):
</p>
<code class="block bg-gray-700 text-yellow-300 p-4 rounded-lg break-words" @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
</code>
or via CLI:
<code class="block bg-gray-700 text-yellow-300 p-4 rounded-lg break-words" @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
</code>
</span>
<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">
<i class="fa-solid fa-copy copy-icon float-right"></i>
Command to connect (click to copy):
</p>
<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
</code>
or via CLI:
<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
</code>
</span>
</div>
</template>
</div>
@@ -371,6 +379,8 @@
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>

View File

@@ -3,10 +3,12 @@
{{template "views/partials/head" .}}
<script defer src="static/image.js"></script>
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen">
<div class="flex flex-col flex-1 overflow-hidden">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{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">
<!-- 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">
@@ -237,6 +239,8 @@
</div>
</div>
</div>
</main>
</div>
<script>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen">
{{template "views/partials/navbar" .}}
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<!-- Main Content - ChatGPT-style minimal interface -->
<div class="flex-1 flex flex-col items-center justify-center px-4 py-12">
@@ -524,6 +526,8 @@
</div>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
<script>

View File

@@ -2,14 +2,14 @@
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
<main class="main-content">
<div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow flex items-center justify-center">
<!-- 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="text-overlay">
<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">
Authorization Required
</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>
<form id="login-form" class="space-y-6" onsubmit="login(); return false;">
<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="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>
<input
type="password"
id="token"
name="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;"
required
/>
@@ -46,7 +46,7 @@
<div>
<button
type="submit"
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"
class="btn-primary w-full"
>
<i class="fas fa-sign-in-alt mr-2"></i>
<span>Login</span>
@@ -54,9 +54,9 @@
</div>
</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">
<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>
</div>
<p>Current time (UTC): <span id="current-time">{{.CurrentDate}}</span></p>
@@ -66,6 +66,8 @@
</div>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
<script>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="indexDashboard()">
{{template "views/partials/navbar" .}}
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="indexDashboard()">
<!-- Notifications -->
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
@@ -527,7 +529,6 @@
</div>
</div>
{{template "views/partials/footer" .}}
</div>
<script>
@@ -869,6 +870,10 @@ document.addEventListener('DOMContentLoaded', function() {
});
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>

View File

@@ -2,10 +2,12 @@
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen" x-data="importModel()" x-init="init()">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="importModel()" x-init="init()">
{{template "views/partials/inprogress" .}}
<div class="container mx-auto px-4 py-8 flex-grow">
@@ -64,9 +66,9 @@
x-transition:enter-end="opacity-100"
class="card p-8">
<div class="space-y-6">
<h2 class="text-2xl font-semibold text-[#E5E7EB] flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center">
<i class="fas fa-link text-green-400"></i>
<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-[var(--color-success-light)] flex items-center justify-center">
<i class="fas fa-link text-[var(--color-success)]"></i>
</div>
Import from URI
</h2>
@@ -74,20 +76,20 @@
<!-- URI Input -->
<div>
<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
</label>
<div class="flex gap-2">
<a href="https://huggingface.co/models?search=gguf&sort=trending"
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>
<span>Search GGUF Models on Hugging Face</span>
<i class="fas fa-external-link-alt text-xs"></i>
</a>
<a href="https://huggingface.co/models?sort=trending"
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>
<span>Browse All Models on Hugging Face</span>
<i class="fas fa-external-link-alt text-xs"></i>
@@ -100,14 +102,14 @@
placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf"
class="input w-full"
: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
</p>
<!-- URI Format Guide -->
<div class="mt-4" x-data="{ showGuide: false }">
<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 fa-info-circle"></i>
<span>Supported URI Formats</span>
@@ -117,34 +119,34 @@
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
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 -->
<div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
<i class="fab fa-huggingface text-purple-400"></i>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fab fa-huggingface text-[var(--color-accent)]"></i>
HuggingFace
</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">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#10B981]">huggingface://</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[#6B7280] mt-0.5">Standard HuggingFace format</p>
<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-[var(--color-text-muted)] mt-0.5">Standard HuggingFace format</p>
</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#10B981]">hf://</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[#6B7280] mt-0.5">Short HuggingFace format</p>
<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-[var(--color-text-muted)] mt-0.5">Short HuggingFace format</p>
</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#10B981]">https://huggingface.co/</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[#6B7280] mt-0.5">Full HuggingFace URL</p>
<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-[var(--color-text-muted)] mt-0.5">Full HuggingFace URL</p>
</div>
</div>
</div>
@@ -152,16 +154,16 @@
<!-- HTTP/HTTPS -->
<div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
<i class="fas fa-globe text-blue-400"></i>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fas fa-globe text-[var(--color-primary)]"></i>
HTTP/HTTPS URLs
</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">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#10B981]">https://</code><span class="text-[#94A3B8]">example.com/model.gguf</span>
<p class="text-[#6B7280] mt-0.5">Direct download from any HTTPS URL</p>
<code class="text-[var(--color-success)]">https://</code><span class="text-[var(--color-text-secondary)]">example.com/model.gguf</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Direct download from any HTTPS URL</p>
</div>
</div>
</div>
@@ -169,23 +171,23 @@
<!-- Local Files -->
<div>
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
<i class="fas fa-file text-yellow-400"></i>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
<i class="fas fa-file text-[var(--color-warning)]"></i>
Local Files
</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">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#10B981]">file://</code><span class="text-[#94A3B8]">/path/to/model.gguf</span>
<p class="text-[#6B7280] mt-0.5">Local file path (absolute)</p>
<code class="text-[var(--color-success)]">file://</code><span class="text-[var(--color-text-secondary)]">/path/to/model.gguf</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Local file path (absolute)</p>
</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#94A3B8]">/path/to/model.yaml</code>
<p class="text-[#6B7280] mt-0.5">Direct local YAML config file</p>
<span class="text-[var(--color-text-secondary)]">/path/to/model.yaml</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Direct local YAML config file</p>
</div>
</div>
</div>
@@ -193,23 +195,23 @@
<!-- OCI -->
<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>
OCI Registry
</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">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#10B981]">oci://</code><span class="text-[#94A3B8]">registry.example.com/model:tag</span>
<p class="text-[#6B7280] mt-0.5">OCI container registry</p>
<code class="text-[var(--color-success)]">oci://</code><span class="text-[var(--color-text-secondary)]">registry.example.com/model:tag</span>
<p class="text-[var(--color-text-muted)] mt-0.5">OCI container registry</p>
</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#10B981]">ocifile://</code><span class="text-[#94A3B8]">/path/to/image.tar</span>
<p class="text-[#6B7280] mt-0.5">Local OCI tarball file</p>
<code class="text-[var(--color-success)]">ocifile://</code><span class="text-[var(--color-text-secondary)]">/path/to/image.tar</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Local OCI tarball file</p>
</div>
</div>
</div>
@@ -217,16 +219,16 @@
<!-- Ollama -->
<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>
Ollama
</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">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#10B981]">ollama://</code><span class="text-[#94A3B8]">llama2:7b</span>
<p class="text-[#6B7280] mt-0.5">Ollama model format</p>
<code class="text-[var(--color-success)]">ollama://</code><span class="text-[var(--color-text-secondary)]">llama2:7b</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Ollama model format</p>
</div>
</div>
</div>
@@ -234,31 +236,31 @@
<!-- YAML Config Files -->
<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>
YAML Configuration Files
</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">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#94A3B8]">https://example.com/model.yaml</code>
<p class="text-[#6B7280] mt-0.5">Remote YAML config file</p>
<span class="text-[var(--color-text-secondary)]">https://example.com/model.yaml</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Remote YAML config file</p>
</div>
</div>
<div class="flex items-start gap-2">
<span class="text-green-400"></span>
<span class="text-[var(--color-success)]"></span>
<div>
<code class="text-[#94A3B8]">file:///path/to/config.yaml</code>
<p class="text-[#6B7280] mt-0.5">Local YAML config file</p>
<span class="text-[var(--color-text-secondary)]">file:///path/to/config.yaml</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Local YAML config file</p>
</div>
</div>
</div>
</div>
<div class="pt-2 mt-3 border-t border-[#1E293B]">
<p class="text-xs text-[#6B7280] italic">
<i class="fas fa-lightbulb mr-1.5 text-yellow-400"></i>
<div class="pt-2 mt-3 border-t border-[var(--color-border-subtle)]">
<p class="text-xs text-[var(--color-text-muted)] italic">
<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.
</p>
</div>
@@ -269,25 +271,25 @@
<!-- Preferences Section -->
<div>
<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)
</label>
</div>
<!-- Common Preferences -->
<div class="space-y-4 mb-6 p-4 bg-gray-900/50 rounded-xl border border-gray-700/50">
<h3 class="text-sm font-semibold text-gray-300 mb-3 flex items-center">
<i class="fas fa-star mr-2 text-yellow-400"></i>Common Preferences
<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-[var(--color-text-secondary)] mb-3 flex items-center">
<i class="fas fa-star mr-2 text-[var(--color-warning)]"></i>Common Preferences
</h3>
<!-- Backend Selection -->
<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
</label>
<select
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">
<option value="">Auto-detect (based on URI)</option>
<option value="llama-cpp">llama-cpp</option>
@@ -297,30 +299,30 @@
<option value="vllm">vllm</option>
<option value="diffusers">diffusers</option>
</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.
</p>
</div>
<!-- Model Name -->
<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
</label>
<input
x-model="commonPreferences.name"
type="text"
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">
<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.
</p>
</div>
<!-- Description -->
<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
</label>
<textarea
@@ -329,39 +331,39 @@
placeholder="Leave empty to use default description"
class="input w-full resize-none"
: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.
</p>
</div>
<!-- Quantizations -->
<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
</label>
<input
x-model="commonPreferences.quantizations"
type="text"
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">
<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).
</p>
</div>
<!-- MMProj Quantizations -->
<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
</label>
<input
x-model="commonPreferences.mmproj_quantizations"
type="text"
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">
<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).
</p>
</div>
@@ -372,77 +374,77 @@
<input
x-model="commonPreferences.embeddings"
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">
<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
</span>
</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.
</p>
</div>
<!-- Model Type -->
<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
</label>
<input
x-model="commonPreferences.type"
type="text"
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">
<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).
</p>
</div>
<!-- Pipeline Type (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
</label>
<input
x-model="commonPreferences.pipeline_type"
type="text"
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">
<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).
</p>
</div>
<!-- Scheduler Type (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
</label>
<input
x-model="commonPreferences.scheduler_type"
type="text"
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">
<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.
</p>
</div>
<!-- Enable Parameters (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
</label>
<input
x-model="commonPreferences.enable_parameters"
type="text"
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">
<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).
</p>
</div>
@@ -453,13 +455,13 @@
<input
x-model="commonPreferences.cuda"
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">
<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
</span>
</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.
</p>
</div>
@@ -468,12 +470,12 @@
<!-- Custom Preferences -->
<div class="space-y-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
</label>
<button @click="addPreference()"
: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
</button>
</div>
@@ -485,24 +487,24 @@
x-model="pref.key"
type="text"
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">
<span class="text-gray-400">:</span>
<span class="text-[var(--color-text-secondary)]">:</span>
<input
x-model="pref.value"
type="text"
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">
<button @click="removePreference(index)"
: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>
</button>
</div>
</template>
</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
</p>
</div>
@@ -515,19 +517,19 @@
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
class="bg-[#1E293B] border border-[#8B5CF6]/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">
<h2 class="text-xl font-semibold text-[#E5E7EB] flex items-center gap-3">
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-[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-[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">
<i class="fas fa-code text-fuchsia-400"></i>
</div>
YAML Configuration Editor
</h2>
<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
</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
</button>
</div>
@@ -553,8 +555,8 @@
<style>
/* Enhanced CodeMirror styling */
.CodeMirror {
background: linear-gradient(135deg, #111827 0%, #1f2937 100%) !important;
color: #e5e7eb !important;
background: var(--color-bg-primary) !important;
color: var(--color-text-primary) !important;
border: none !important;
height: 100% !important;
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important;
@@ -564,7 +566,7 @@
}
.CodeMirror-cursor {
border-left: 2px solid #a78bfa !important;
border-left: 2px solid var(--color-accent) !important;
animation: blink 1s infinite;
}
@@ -574,20 +576,20 @@
}
.CodeMirror-gutters {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%) !important;
border-right: 1px solid rgba(75, 85, 99, 0.5) !important;
color: #9ca3af !important;
background: var(--color-bg-secondary) !important;
border-right: 1px solid var(--color-border-subtle) !important;
color: var(--color-text-secondary) !important;
padding-right: 8px !important;
}
.CodeMirror-linenumber {
color: #6b7280 !important;
color: var(--color-text-muted) !important;
padding: 0 8px 0 4px !important;
font-size: 12px !important;
}
.CodeMirror-activeline-background {
background: rgba(139, 92, 246, 0.1) !important;
background: var(--color-accent-light) !important;
}
.CodeMirror-selected {
@@ -614,27 +616,27 @@
.cm-keyword { color: #8b5cf6 !important; font-weight: 600 !important; }
.cm-string { color: #10b981 !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-operator { color: #ef4444 !important; }
.cm-variable { color: #06b6d4 !important; }
.cm-tag { color: #8b5cf6 !important; font-weight: 600 !important; }
.cm-attribute { color: #f59e0b !important; }
.cm-def { color: #ec4899 !important; font-weight: 600 !important; }
.cm-bracket { color: #d1d5db !important; }
.cm-punctuation { color: #d1d5db !important; }
.cm-bracket { color: var(--color-text-secondary) !important; }
.cm-punctuation { color: var(--color-text-secondary) !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-atom { color: #f59e0b !important; }
/* Enhanced scrollbar styling */
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background: #1f2937 !important;
background: var(--color-bg-secondary) !important;
}
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
background: #1f2937 !important;
background: var(--color-bg-secondary) !important;
}
.CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar {
@@ -643,17 +645,17 @@
}
.CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track {
background: #1f2937;
background: var(--color-bg-secondary);
border-radius: 4px;
}
.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;
}
.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 */
@@ -682,27 +684,27 @@
}
.alert-success {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
border-color: rgba(16, 185, 129, 0.3);
color: #10b981;
background: var(--color-success-light);
border-color: var(--color-success);
color: var(--color-success);
}
.alert-error {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%);
border-color: rgba(239, 68, 68, 0.3);
color: #ef4444;
background: var(--color-error-light);
border-color: var(--color-error);
color: var(--color-error);
}
.alert-warning {
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%);
border-color: rgba(245, 158, 11, 0.3);
color: #f59e0b;
background: var(--color-warning-light);
border-color: var(--color-warning);
color: var(--color-warning);
}
.alert-info {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%);
border-color: rgba(59, 130, 246, 0.3);
color: #3b82f6;
background: var(--color-info-light);
border-color: var(--color-info);
color: var(--color-info);
}
</style>
@@ -1026,10 +1028,11 @@ parameters:
if (!config.name) {
throw new Error('Model name is required');
}
if (!config.backend) {
const isPipeline = config.pipeline && (config.pipeline.vad || config.pipeline.transcription || config.pipeline.tts || config.pipeline.llm);
if (!isPipeline && !config.backend) {
throw new Error('Backend is required');
}
if (!config.parameters || !config.parameters.model) {
if (!isPipeline && (!config.parameters || !config.parameters.model)) {
throw new Error('Model file/path is required in parameters.model');
}
@@ -1041,7 +1044,6 @@ parameters:
async saveConfig() {
try {
// Validate before saving
const yamlContent = this.yamlEditor.getValue();
const config = jsyaml.load(yamlContent);
@@ -1052,13 +1054,13 @@ parameters:
if (!config.name) {
throw new Error('Model name is required');
}
if (!config.backend) {
const isPipeline = config.pipeline && (config.pipeline.vad || config.pipeline.transcription || config.pipeline.tts || config.pipeline.llm);
if (!isPipeline && !config.backend) {
throw new Error('Backend is required');
}
if (!config.parameters || !config.parameters.model) {
if (!isPipeline && (!config.parameters || !config.parameters.model)) {
throw new Error('Model file/path is required in parameters.model');
}
const endpoint = this.isEditMode ? `/models/edit/{{.ModelName}}` : '/models/import';
const response = await fetch(endpoint, {
@@ -1180,5 +1182,10 @@ parameters:
}
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="modelsGallery()">
{{template "views/partials/navbar" .}}
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="modelsGallery()">
<!-- Notifications -->
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
@@ -190,75 +192,75 @@
</div>
<!-- Table View -->
<div x-show="models.length > 0" class="bg-[#1E293B] rounded-2xl border border-[#38BDF8]/20 overflow-hidden shadow-xl backdrop-blur-sm">
<div x-show="models.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">
<table class="w-full">
<thead>
<tr class="bg-gradient-to-r from-[#38BDF8]/20 to-[#8B5CF6]/20 border-b border-[#38BDF8]/30">
<th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Icon</th>
<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-[var(--color-text-primary)] uppercase tracking-wider">Icon</th>
<th @click="setSort('name')"
:class="sortBy === 'name' ? 'bg-[#38BDF8]/20' : ''"
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="sortBy === 'name' ? 'bg-[var(--color-primary-light)]' : ''"
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">
<span>Model Name</span>
<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>
</div>
</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')"
:class="sortBy === 'repository' ? 'bg-[#38BDF8]/20' : ''"
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="sortBy === 'repository' ? 'bg-[var(--color-primary-light)]' : ''"
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">
<span>Repository</span>
<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>
</div>
</th>
<th @click="setSort('license')"
:class="sortBy === 'license' ? 'bg-[#38BDF8]/20' : ''"
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="sortBy === 'license' ? 'bg-[var(--color-primary-light)]' : ''"
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">
<span>License</span>
<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>
</div>
</th>
<th @click="setSort('status')"
:class="sortBy === 'status' ? 'bg-[#38BDF8]/20' : ''"
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="sortBy === 'status' ? 'bg-[var(--color-primary-light)]' : ''"
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">
<span>Status</span>
<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>
</div>
</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>
</thead>
<tbody class="divide-y divide-[#38BDF8]/20">
<tbody class="divide-y divide-[var(--color-border-subtle)]">
<template x-for="model in models" :key="model.id">
<tr class="hover:bg-[#38BDF8]/10 transition-colors duration-200">
<tr class="hover:bg-[var(--color-bg-primary)] transition-colors duration-200">
<!-- Icon -->
<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="model.icon"
:src="model.icon"
class="w-full h-full object-cover rounded-lg"
loading="lazy"
:alt="model.name">
<i x-show="!model.icon" class="fas fa-brain text-xl text-[#8B5CF6]"></i>
<i x-show="!model.icon" class="fas fa-brain text-xl text-[var(--color-accent)]"></i>
</div>
</td>
<!-- Model Name -->
<td class="px-6 py-4">
<div class="flex flex-col">
<span class="text-sm font-semibold text-[#E5E7EB]" x-text="model.name"></span>
<span class="text-sm font-semibold text-[var(--color-text-primary)]" x-text="model.name"></span>
<div x-show="model.trustRemoteCode" class="mt-1">
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-red-500/20 text-red-300 border border-red-500/30">
<i class="fa-solid fa-circle-exclamation mr-1"></i>
@@ -270,12 +272,12 @@
<!-- Description -->
<td class="px-6 py-4">
<div class="text-sm text-[#94A3B8] max-w-xs truncate" x-text="model.description" :title="model.description"></div>
<div class="text-sm text-[var(--color-text-secondary)] max-w-xs truncate" x-text="model.description" :title="model.description"></div>
</td>
<!-- Repository -->
<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>
<span x-text="model.gallery"></span>
</span>
@@ -283,21 +285,21 @@
<!-- License -->
<td class="px-6 py-4">
<span x-show="model.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="model.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>
<span x-text="model.license"></span>
</span>
<span x-show="!model.license" class="text-xs text-[#94A3B8]">-</span>
<span x-show="!model.license" class="text-xs text-[var(--color-text-secondary)]">-</span>
</td>
<!-- Status -->
<td class="px-6 py-4">
<!-- Processing State -->
<div x-show="model.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="model.isDeletion ? 'Deleting...' : 'Installing...'"></span>
</div>
<div x-show="(jobProgress[model.jobID] || 0) === 0" class="text-xs text-[#38BDF8]">
<div x-show="(jobProgress[model.jobID] || 0) === 0" class="text-xs text-[var(--color-primary)]">
<i class="fas fa-clock mr-1"></i>Queued
</div>
<div class="progress-table mt-1">
@@ -307,7 +309,7 @@
<!-- Installed State -->
<div x-show="!model.processing && model.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>
Installed
</span>
@@ -315,7 +317,7 @@
<!-- Not Installed State -->
<div x-show="!model.processing && !model.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>
Not Installed
</span>
@@ -327,7 +329,7 @@
<div class="flex items-center justify-end gap-2">
<!-- Info Button -->
<button @click="openModal(model)"
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">
<i class="fas fa-info-circle"></i>
</button>
@@ -336,12 +338,12 @@
<template x-if="!model.processing && model.installed">
<div class="flex gap-2">
<button @click="reinstallModel(model.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">
<i class="fa-solid fa-arrow-rotate-right"></i>
</button>
<button @click="deleteModel(model.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">
<i class="fa-solid fa-trash"></i>
</button>
@@ -352,12 +354,12 @@
<template x-if="!model.processing && !model.installed">
<div class="flex gap-2">
<button @click="getConfig(model.id)"
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-accent)]/20 hover:bg-[var(--color-accent)]/40 text-xs font-medium text-[var(--color-text-primary)] transition duration-200 border border-[var(--color-accent-border)]/30"
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-xs font-medium text-[var(--color-text-primary)] transition duration-200 border border-[var(--color-accent)]/30"
title="Get config">
<i class="fa-solid fa-file-code"></i>
</button>
<button @click="installModel(model.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">
<i class="fa-solid fa-download"></i>
</button>
@@ -376,15 +378,15 @@
<div x-show="selectedModel"
x-transition
@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;">
<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 -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white" x-text="selectedModel?.name"></h3>
<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-[var(--color-text-primary)]" x-text="selectedModel?.name"></h3>
<button @click="closeModal()"
class="text-[var(--color-text-secondary)] 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">
<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>
@@ -394,23 +396,23 @@
<!-- Modal Body -->
<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="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="selectedModel?.icon"
:src="selectedModel?.icon"
class="rounded-lg max-h-48 max-w-96 object-cover"
loading="lazy">
<i x-show="!selectedModel?.icon" class="fas fa-brain text-6xl text-[var(--color-text-secondary)] dark:text-[var(--color-text-muted)]"></i>
<i x-show="!selectedModel?.icon" class="fas fa-brain text-6xl text-[var(--color-text-muted)]"></i>
</div>
</div>
<div class="text-base leading-relaxed text-[var(--color-text-muted)] dark:text-[var(--color-text-secondary)] break-words max-w-full markdown-content" x-html="renderMarkdown(selectedModel?.description)"></div>
<div class="text-base leading-relaxed text-[var(--color-text-secondary)] break-words max-w-full markdown-content" x-html="renderMarkdown(selectedModel?.description)"></div>
<hr>
<template x-if="selectedModel?.urls && selectedModel.urls.length > 0">
<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>
<template x-for="url in selectedModel.urls" :key="url">
<li>
<a :href="url" target="_blank" class="text-base leading-relaxed text-[var(--color-text-muted)] dark:text-[var(--color-text-secondary)] hover:text-[var(--color-primary)]">
<a :href="url" target="_blank" class="text-base leading-relaxed text-[var(--color-text-secondary)] hover:text-[var(--color-primary)]">
<i class="fas fa-link pr-2"></i>
<span x-text="url"></span>
</a>
@@ -421,11 +423,11 @@
</template>
<template x-if="selectedModel?.additionalFiles && selectedModel.additionalFiles.length > 0">
<div>
<p class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Files</p>
<p class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Files</p>
<ul>
<template x-for="file in selectedModel.additionalFiles" :key="file">
<li class="mb-0">
<p class="text-base leading-tight text-[var(--color-text-muted)] dark:text-[var(--color-text-secondary)]">
<p class="text-base leading-tight text-[var(--color-text-secondary)]">
<i class="fas fa-file pr-2"></i>
<span x-text="file.filename"></span>
</p>
@@ -436,11 +438,11 @@
</template>
<template x-if="selectedModel?.tags && selectedModel.tags.length > 0">
<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-row flex-wrap content-center">
<template x-for="tag in selectedModel.tags" :key="tag">
<a :href="'browse?term=' + tag"
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 hover:bg-gray-600 hover:text-gray-100 transition duration-200 ease-in-out mr-2 mb-2">
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)] hover:bg-[var(--color-primary-light)] hover:text-[var(--color-text-primary)] transition duration-200 ease-in-out mr-2 mb-2">
<i class="fas fa-tag pr-2"></i>
<span x-text="tag"></span>
</a>
@@ -450,9 +452,9 @@
</template>
</div>
<!-- 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()"
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-[var(--color-text-secondary)] dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
class="btn-secondary">
Close
</button>
</div>
@@ -463,30 +465,29 @@
<!-- Pagination -->
<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)"
:disabled="currentPage <= 1"
:class="currentPage <= 1 ? 'opacity-50 cursor-not-allowed' : ''"
class="flex items-center justify-center h-12 w-12 bg-[var(--color-bg-secondary)] hover:bg-indigo-600 text-[var(--color-text-secondary)] 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-primary)] text-[var(--color-text-secondary)] hover:text-white rounded-lg transition-colors">
<i class="fas fa-chevron-left"></i>
</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-[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-[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>
<button @click="goToPage(currentPage + 1)"
:disabled="currentPage >= totalPages"
: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-indigo-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-primary)] 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>
</button>
</div>
</div>
</div>
{{template "views/partials/footer" .}}
</div>
<style>
@@ -526,16 +527,16 @@
/* Table progress bar styling */
.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: 1px solid rgba(56, 189, 248, 0.3);
border: 1px solid var(--color-primary-border);
height: 6px;
overflow: hidden;
width: 100%;
}
.progress-bar-table {
background: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 100%);
background: var(--gradient-primary);
height: 100%;
transition: width 0.3s ease;
}
@@ -544,6 +545,7 @@
table {
border-collapse: separate;
border-spacing: 0;
background: var(--color-bg-secondary);
}
tbody tr:last-child td:first-child {
@@ -831,5 +833,10 @@ function modelsGallery() {
}
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>

View File

@@ -2,10 +2,12 @@
<html lang="en">
{{template "views/partials/head" .}}
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen" x-data="p2pNetwork()">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="p2pNetwork()">
{{template "views/partials/inprogress" .}}
@@ -25,66 +27,66 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-10">
<div class="card card-animate">
<div class="w-10 h-10 bg-blue-500/10 rounded-lg flex items-center justify-center mx-auto mb-3">
<i class="fas fa-network-wired text-[#38BDF8] text-xl"></i>
<i class="fas fa-network-wired text-[var(--color-primary)] text-xl"></i>
</div>
<h3 class="text-sm font-semibold text-[#E5E7EB] mb-2">Instance Federation</h3>
<p class="text-xs text-[#94A3B8]">Load balance across multiple instances</p>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Instance Federation</h3>
<p class="text-xs text-[var(--color-text-secondary)]">Load balance across multiple instances</p>
</div>
<div class="card card-animate">
<div class="w-10 h-10 bg-purple-500/10 rounded-lg flex items-center justify-center mx-auto mb-3">
<i class="fas fa-puzzle-piece text-[#8B5CF6] text-xl"></i>
<i class="fas fa-puzzle-piece text-[var(--color-accent)] text-xl"></i>
</div>
<h3 class="text-sm font-semibold text-[#E5E7EB] mb-2">Model Sharding</h3>
<p class="text-xs text-[#94A3B8]">Split large models across workers</p>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Model Sharding</h3>
<p class="text-xs text-[var(--color-text-secondary)]">Split large models across workers</p>
</div>
<div class="card card-animate">
<div class="w-10 h-10 bg-green-500/10 rounded-lg flex items-center justify-center mx-auto mb-3">
<i class="fas fa-share-alt text-green-400 text-xl"></i>
<i class="fas fa-share-alt text-[var(--color-success)] text-xl"></i>
</div>
<h3 class="text-sm font-semibold text-[#E5E7EB] mb-2">Resource Sharing</h3>
<p class="text-xs text-[#94A3B8]">Pool resources from multiple devices</p>
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Resource Sharing</h3>
<p class="text-xs text-[var(--color-text-secondary)]">Pool resources from multiple devices</p>
</div>
</div>
<!-- Setup Instructions -->
<div class="card mb-8 text-left">
<h3 class="text-lg font-bold text-[#E5E7EB] mb-4 flex items-center">
<i class="fas fa-rocket text-[#8B5CF6] mr-2"></i>
<h3 class="text-lg font-bold text-[var(--color-text-primary)] mb-4 flex items-center">
<i class="fas fa-rocket text-[var(--color-accent)] mr-2"></i>
How to Enable P2P
</h3>
<div class="space-y-4">
<div class="flex items-start">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[#8B5CF6]/20 flex items-center justify-center mr-3 mt-0.5">
<span class="text-[#8B5CF6] font-bold text-sm">1</span>
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--color-accent-light)] flex items-center justify-center mr-3 mt-0.5">
<span class="text-[var(--color-accent)] font-bold text-sm">1</span>
</div>
<div class="flex-1">
<p class="text-[#E5E7EB] font-medium mb-2">Start LocalAI with P2P enabled</p>
<code class="block bg-[#1E293B] text-[#38BDF8] p-3 rounded-lg text-sm border border-[#38BDF8]/20">
<p class="text-[var(--color-text-primary)] font-medium mb-2">Start LocalAI with P2P enabled</p>
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-primary)] p-3 rounded-lg text-sm border border-[var(--color-primary-border)]/20">
local-ai run --p2p
</code>
<p class="text-[#94A3B8] text-sm mt-2">This will automatically generate a network token for you.</p>
<p class="text-[var(--color-text-secondary)] text-sm mt-2">This will automatically generate a network token for you.</p>
</div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[#8B5CF6]/20 flex items-center justify-center mr-3 mt-0.5">
<span class="text-[#8B5CF6] font-bold text-sm">2</span>
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--color-accent-light)] flex items-center justify-center mr-3 mt-0.5">
<span class="text-[var(--color-accent)] font-bold text-sm">2</span>
</div>
<div class="flex-1">
<p class="text-[#E5E7EB] font-medium mb-2">Or use an existing token</p>
<code class="block bg-[#1E293B] text-[#38BDF8] p-3 rounded-lg text-sm border border-[#38BDF8]/20">
<p class="text-[var(--color-text-primary)] font-medium mb-2">Or use an existing token</p>
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-primary)] p-3 rounded-lg text-sm border border-[var(--color-primary-border)]/20">
export TOKEN="your-token-here"<br>
local-ai run --p2p
</code>
<p class="text-[#94A3B8] text-sm mt-2">If you already have a token from another instance, you can reuse it.</p>
<p class="text-[var(--color-text-secondary)] text-sm mt-2">If you already have a token from another instance, you can reuse it.</p>
</div>
</div>
<div class="flex items-start">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[#8B5CF6]/20 flex items-center justify-center mr-3 mt-0.5">
<span class="text-[#8B5CF6] font-bold text-sm">3</span>
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--color-accent-light)] flex items-center justify-center mr-3 mt-0.5">
<span class="text-[var(--color-accent)] font-bold text-sm">3</span>
</div>
<div class="flex-1">
<p class="text-[#E5E7EB] font-medium mb-2">Access the P2P dashboard</p>
<p class="text-[#94A3B8] text-sm">Once enabled, refresh this page to see your network token and start connecting nodes.</p>
<p class="text-[var(--color-text-primary)] font-medium mb-2">Access the P2P dashboard</p>
<p class="text-[var(--color-text-secondary)] text-sm">Once enabled, refresh this page to see your network token and start connecting nodes.</p>
</div>
</div>
</div>
@@ -92,13 +94,13 @@
<div class="flex flex-wrap justify-center gap-4">
<a href="https://localai.io/features/distribute/" target="_blank"
class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white py-3 px-6 rounded-lg font-semibold transition-colors">
class="inline-flex items-center bg-[var(--color-accent)] hover:bg-[var(--color-accent)]/90 text-white py-3 px-6 rounded-lg font-semibold transition-colors">
<i class="fas fa-book mr-2"></i>
Documentation
<i class="fas fa-external-link-alt ml-2 text-sm"></i>
</a>
<a href="https://localai.io/basics/getting_started/" target="_blank"
class="inline-flex items-center bg-[#1E293B] hover:bg-[#1E293B]/80 border border-[#8B5CF6]/20 text-[#E5E7EB] py-3 px-6 rounded-lg font-semibold transition-colors">
class="inline-flex items-center bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 text-[var(--color-text-primary)] py-3 px-6 rounded-lg font-semibold transition-colors">
<i class="fas fa-graduation-cap mr-2"></i>
Getting Started
<i class="fas fa-external-link-alt ml-2 text-sm"></i>
@@ -118,7 +120,7 @@
</h1>
<p class="hero-subtitle">
Scale your AI workloads across multiple devices with peer-to-peer distribution
<a href="https://localai.io/features/distribute/" target="_blank" class="text-[#38BDF8] hover:text-[#8B5CF6] transition-colors">
<a href="https://localai.io/features/distribute/" target="_blank" class="text-[var(--color-primary)] hover:text-[var(--color-accent)] transition-colors">
<i class="fas fa-circle-info ml-2"></i>
</a>
</p>
@@ -132,7 +134,7 @@
<h2 class="h2 mb-4">
How P2P Distribution Works
</h2>
<p class="text-lg text-[#94A3B8] max-w-3xl mx-auto">
<p class="text-lg text-[var(--color-text-secondary)] max-w-3xl mx-auto">
LocalAI leverages cutting-edge peer-to-peer technologies to distribute AI workloads intelligently across your network
</p>
</div>
@@ -140,34 +142,34 @@
<!-- Key Features Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Federation -->
<div class="bg-[#101827] rounded-xl p-6 border border-[#38BDF8]/20 transition-colors">
<div class="bg-[var(--color-bg-primary)] rounded-xl p-6 border border-[var(--color-primary-border)]/20 transition-colors">
<div class="w-12 h-12 bg-blue-500/10 rounded-lg flex items-center justify-center mb-4">
<i class="fas fa-network-wired text-blue-400 text-xl"></i>
<i class="fas fa-network-wired text-[var(--color-primary)] text-xl"></i>
</div>
<h3 class="text-xl font-bold text-[#E5E7EB] mb-3">Instance Federation</h3>
<p class="text-[#94A3B8] text-sm leading-relaxed">
<h3 class="text-xl font-bold text-[var(--color-text-primary)] mb-3">Instance Federation</h3>
<p class="text-[var(--color-text-secondary)] text-sm leading-relaxed">
Share complete LocalAI instances across your network for load balancing and redundancy. Perfect for scaling across multiple devices.
</p>
</div>
<!-- Model Sharding -->
<div class="bg-[#101827] rounded-xl p-6 border border-[#8B5CF6]/20 transition-colors">
<div class="bg-[var(--color-bg-primary)] rounded-xl p-6 border border-[var(--color-accent)]/20 transition-colors">
<div class="w-12 h-12 bg-purple-500/10 rounded-lg flex items-center justify-center mb-4">
<i class="fas fa-puzzle-piece text-purple-400 text-xl"></i>
<i class="fas fa-puzzle-piece text-[var(--color-accent)] text-xl"></i>
</div>
<h3 class="text-xl font-bold text-[#E5E7EB] mb-3">Model Sharding</h3>
<p class="text-[#94A3B8] text-sm leading-relaxed">
<h3 class="text-xl font-bold text-[var(--color-text-primary)] mb-3">Model Sharding</h3>
<p class="text-[var(--color-text-secondary)] text-sm leading-relaxed">
Split large model weights across multiple workers. Currently supported with llama.cpp backends for efficient memory usage.
</p>
</div>
<!-- Resource Sharing -->
<div class="bg-[#101827] rounded-xl p-6 border border-green-500/20 transition-colors">
<div class="bg-[var(--color-bg-primary)] rounded-xl p-6 border border-[var(--color-success)]/20 transition-colors">
<div class="w-12 h-12 bg-green-500/10 rounded-lg flex items-center justify-center mb-4">
<i class="fas fa-share-alt text-green-400 text-xl"></i>
<i class="fas fa-share-alt text-[var(--color-success)] text-xl"></i>
</div>
<h3 class="text-xl font-bold text-[#E5E7EB] mb-3">Resource Sharing</h3>
<p class="text-[#94A3B8] text-sm leading-relaxed">
<h3 class="text-xl font-bold text-[var(--color-text-primary)] mb-3">Resource Sharing</h3>
<p class="text-[var(--color-text-secondary)] text-sm leading-relaxed">
Pool computational resources from multiple devices, including your friends' machines, to handle larger workloads collaboratively.
</p>
</div>
@@ -176,44 +178,44 @@
<!-- Benefits -->
<div class="mt-10 grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="text-center">
<div class="text-2xl font-bold text-blue-400 mb-1">
<div class="text-2xl font-bold text-[var(--color-primary)] mb-1">
<i class="fas fa-tachometer-alt mr-2"></i>Faster
</div>
<p class="text-gray-400 text-sm">Parallel processing</p>
<p class="text-[var(--color-text-secondary)] text-sm">Parallel processing</p>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-purple-400 mb-1">
<div class="text-2xl font-bold text-[var(--color-accent)] mb-1">
<i class="fas fa-expand-arrows-alt mr-2"></i>Scalable
</div>
<p class="text-gray-400 text-sm">Add more nodes</p>
<p class="text-[var(--color-text-secondary)] text-sm">Add more nodes</p>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-400 mb-1">
<div class="text-2xl font-bold text-[var(--color-success)] mb-1">
<i class="fas fa-shield-alt mr-2"></i>Resilient
</div>
<p class="text-gray-400 text-sm">Fault tolerant</p>
<p class="text-[var(--color-text-secondary)] text-sm">Fault tolerant</p>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-yellow-400 mb-1">
<div class="text-2xl font-bold text-[var(--color-warning)] mb-1">
<i class="fas fa-coins mr-2"></i>Efficient
</div>
<p class="text-gray-400 text-sm">Resource optimization</p>
<p class="text-[var(--color-text-secondary)] text-sm">Resource optimization</p>
</div>
</div>
</div>
</div>
<!-- Network Token Card -->
<div class="bg-[#1E293B] border border-[#8B5CF6]/20 rounded-xl mb-10 p-6">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 rounded-xl mb-10 p-6">
<div class="flex items-center mb-4">
<i class="fas fa-key text-yellow-400 text-xl mr-3"></i>
<h3 class="text-xl font-bold text-white">Network Token</h3>
<button onclick="copyClipboard('{{.P2PToken}}')" class="ml-auto bg-gray-700 hover:bg-gray-600 text-gray-300 p-2 rounded-lg transition-colors duration-200">
<i class="fas fa-key text-[var(--color-warning)] text-xl mr-3"></i>
<h3 class="text-xl font-bold text-[var(--color-text-primary)]">Network Token</h3>
<button onclick="copyClipboard('{{.P2PToken}}')" class="ml-auto bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] p-2 rounded-lg transition-colors duration-200 border border-[var(--color-border-subtle)]">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<code class="block bg-gray-900/80 text-yellow-300 p-4 rounded-lg break-words mb-4 border border-gray-700/50 cursor-pointer hover:bg-gray-900" @click="copyClipboard($el.textContent.trim())">{{.P2PToken}}</code>
<p class="text-gray-300">
<code class="block bg-[var(--color-bg-primary)]/80 text-[var(--color-warning)] p-4 rounded-lg break-words mb-4 border border-[var(--color-border-subtle)]/50 cursor-pointer hover:bg-[var(--color-bg-primary)]" @click="copyClipboard($el.textContent.trim())">{{.P2PToken}}</code>
<p class="text-[var(--color-text-secondary)]">
The network token can be used to either share the instance or join a federation or a worker network. Below you will find examples on how to start a new instance or a worker with this token.
</p>
</div>
@@ -221,74 +223,74 @@
<!-- Network Status Overview -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-10">
<!-- Federation Status -->
<div class="bg-[#1E293B] border border-blue-500/20 rounded-xl p-6">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-12 h-12 bg-blue-500/20 rounded-xl flex items-center justify-center mr-3">
<i class="fas fa-network-wired text-blue-400 text-xl"></i>
<div class="w-12 h-12 bg-[var(--color-primary-light)] rounded-xl flex items-center justify-center mr-3">
<i class="fas fa-network-wired text-[var(--color-primary)] text-xl"></i>
</div>
<div>
<h3 class="text-lg font-bold text-white">Federation</h3>
<p class="text-blue-300 text-sm">Instance sharing</p>
<h3 class="text-lg font-bold text-[var(--color-text-primary)]">Federation</h3>
<p class="text-[var(--color-primary)] text-sm">Instance sharing</p>
</div>
</div>
<div class="text-right">
<div class="text-2xl font-bold">
<span :class="stats.federated.online > 0 ? 'text-green-400' : 'text-red-400'" x-text="stats.federated.online"></span>
<span class="text-gray-300 text-xl">/<span x-text="stats.federated.total"></span></span>
<span :class="stats.federated.online > 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'" x-text="stats.federated.online"></span>
<span class="text-[var(--color-text-secondary)] text-xl">/<span x-text="stats.federated.total"></span></span>
</div>
<p class="text-blue-300 text-sm">nodes</p>
<p class="text-[var(--color-primary)] text-sm">nodes</p>
</div>
</div>
<div class="flex items-center text-sm text-blue-200">
<div class="flex items-center text-sm text-[var(--color-primary)]/80">
<i class="fas fa-info-circle mr-2"></i>
<span>Load balanced instances</span>
</div>
</div>
<!-- Workers Status -->
<div class="bg-[#1E293B] border border-purple-500/20 rounded-xl p-6">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-12 h-12 bg-purple-500/20 rounded-xl flex items-center justify-center mr-3">
<i class="fas fa-puzzle-piece text-purple-400 text-xl"></i>
<div class="w-12 h-12 bg-[var(--color-accent-light)] rounded-xl flex items-center justify-center mr-3">
<i class="fas fa-puzzle-piece text-[var(--color-accent)] text-xl"></i>
</div>
<div>
<h3 class="text-lg font-bold text-white">Workers</h3>
<p class="text-purple-300 text-sm">Model sharding</p>
<h3 class="text-lg font-bold text-[var(--color-text-primary)]">Workers</h3>
<p class="text-[var(--color-accent)] text-sm">Model sharding</p>
</div>
</div>
<div class="text-right">
<div class="text-2xl font-bold">
<span :class="stats.workers.online > 0 ? 'text-green-400' : 'text-red-400'" x-text="stats.workers.online"></span>
<span class="text-gray-300 text-xl">/<span x-text="stats.workers.total"></span></span>
<span :class="stats.workers.online > 0 ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'" x-text="stats.workers.online"></span>
<span class="text-[var(--color-text-secondary)] text-xl">/<span x-text="stats.workers.total"></span></span>
</div>
<p class="text-purple-300 text-sm">workers</p>
<p class="text-[var(--color-accent)] text-sm">workers</p>
</div>
</div>
<div class="flex items-center text-sm text-purple-200">
<div class="flex items-center text-sm text-[var(--color-accent)]/80">
<i class="fas fa-info-circle mr-2"></i>
<span>Distributed computation</span>
</div>
</div>
<!-- Network Token -->
<div class="bg-[#1E293B] border border-yellow-500/20 rounded-xl p-6">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-warning)]/20 rounded-xl p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-12 h-12 bg-yellow-500/20 rounded-xl flex items-center justify-center mr-3">
<i class="fas fa-key text-yellow-400 text-xl"></i>
<div class="w-12 h-12 bg-[var(--color-warning-light)] rounded-xl flex items-center justify-center mr-3">
<i class="fas fa-key text-[var(--color-warning)] text-xl"></i>
</div>
<div>
<h3 class="text-lg font-bold text-white">Network</h3>
<p class="text-yellow-300 text-sm">Connection token</p>
<h3 class="text-lg font-bold text-[var(--color-text-primary)]">Network</h3>
<p class="text-[var(--color-warning)] text-sm">Connection token</p>
</div>
</div>
<button onclick="copyClipboard('{{.P2PToken}}')" class="bg-yellow-600/30 hover:bg-yellow-600/50 text-yellow-300 p-2 rounded-lg transition-colors duration-200">
<button onclick="copyClipboard('{{.P2PToken}}')" class="bg-[var(--color-warning-light)] hover:bg-[var(--color-warning)]/30 text-[var(--color-warning)] p-2 rounded-lg transition-colors duration-200">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<div class="flex items-center text-sm text-yellow-200">
<div class="flex items-center text-sm text-[var(--color-warning)]/80">
<i class="fas fa-info-circle mr-2"></i>
<span>Ready to connect</span>
</div>
@@ -296,30 +298,30 @@
</div>
<!-- Federation Box -->
<div class="bg-[#1E293B] border border-[#8B5CF6]/20 rounded-xl mb-10">
<div class="p-8 border-b border-gray-700/50">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 rounded-xl mb-10">
<div class="p-8 border-b border-[var(--color-border-subtle)]">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center">
<div class="w-14 h-14 bg-blue-500/20 rounded-2xl flex items-center justify-center mr-4">
<i class="text-blue-400 fa-solid fa-circle-nodes text-2xl fa-spin-pulse"></i>
<div class="w-14 h-14 bg-[var(--color-primary-light)] rounded-2xl flex items-center justify-center mr-4">
<i class="text-[var(--color-primary)] fa-solid fa-circle-nodes text-2xl fa-spin-pulse"></i>
</div>
<div>
<h2 class="text-2xl font-bold text-white">Federation Network</h2>
<p class="text-blue-300 text-sm">Instance load balancing and sharing</p>
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Federation Network</h2>
<p class="text-[var(--color-primary)] text-sm">Instance load balancing and sharing</p>
</div>
</div>
<div class="text-right">
<div class="text-sm text-gray-400 mb-1">Active Nodes</div>
<div class="text-sm text-[var(--color-text-secondary)] mb-1">Active Nodes</div>
<div class="text-3xl font-bold">
<span :class="stats.federated.online > 0 ? 'text-blue-400' : 'text-red-400'" x-text="stats.federated.online"></span>
<span class="text-gray-400 text-xl">/<span x-text="stats.federated.total"></span></span>
<span :class="stats.federated.online > 0 ? 'text-[var(--color-primary)]' : 'text-[var(--color-error)]'" x-text="stats.federated.online"></span>
<span class="text-[var(--color-text-secondary)] text-xl">/<span x-text="stats.federated.total"></span></span>
</div>
</div>
</div>
<div class="bg-blue-900/20 rounded-xl p-4 mb-6 border border-blue-700/30">
<p class="text-gray-300 text-sm leading-relaxed">
<i class="fas fa-lightbulb text-blue-400 mr-2"></i>
<div class="bg-[var(--color-primary-light)] rounded-xl p-4 mb-6 border border-[var(--color-primary-border)]/30">
<p class="text-[var(--color-text-secondary)] text-sm leading-relaxed">
<i class="fas fa-lightbulb text-[var(--color-primary)] mr-2"></i>
Start LocalAI in federated mode to share your instance, or launch a federated server to distribute requests intelligently across multiple nodes in your network.
</p>
</div>
@@ -327,36 +329,36 @@
<!-- Federation Nodes Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<template x-if="federationNodes.length === 0">
<div class="col-span-full flex flex-col items-center justify-center py-12 text-center bg-gray-800/50 border border-gray-700/50 rounded-xl">
<i class="fas fa-server text-gray-500 text-4xl mb-4"></i>
<p class="text-gray-400 text-lg font-medium">No nodes available</p>
<p class="text-gray-500 text-sm mt-2">Start some workers to see them here</p>
<div class="col-span-full flex flex-col items-center justify-center py-12 text-center bg-[var(--color-bg-primary)]/50 border border-[var(--color-border-subtle)]/50 rounded-xl">
<i class="fas fa-server text-[var(--color-text-muted)] text-4xl mb-4"></i>
<p class="text-[var(--color-text-secondary)] text-lg font-medium">No nodes available</p>
<p class="text-[var(--color-text-muted)] text-sm mt-2">Start some workers to see them here</p>
</div>
</template>
<template x-for="node in federationNodes" :key="node.id">
<div :class="node.isOnline ? 'border-green-400/50' : 'border-red-400/50'"
class="bg-[#101827] border rounded-lg p-5 transition-colors">
<div :class="node.isOnline ? 'border-[var(--color-success)]/50' : 'border-[var(--color-error)]/50'"
class="bg-[var(--color-bg-primary)] border rounded-lg p-5 transition-colors">
<!-- Header with node icon and status -->
<div class="flex items-center justify-between mb-4">
<!-- Node info -->
<div class="flex items-center">
<div class="w-10 h-10 bg-blue-500/20 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-server text-blue-400 text-lg"></i>
<div class="w-10 h-10 bg-[var(--color-primary-light)] rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-server text-[var(--color-primary)] text-lg"></i>
</div>
<div>
<h4 class="text-white font-semibold text-sm">Node</h4>
<p class="text-gray-400 text-xs font-mono break-all" x-text="node.id"></p>
<h4 class="text-[var(--color-text-primary)] font-semibold text-sm">Node</h4>
<p class="text-[var(--color-text-secondary)] text-xs font-mono break-all" x-text="node.id"></p>
</div>
</div>
<!-- Status badge -->
<div class="flex items-center bg-[#101827] rounded-lg px-3 py-1.5 border border-[#1E293B]">
<i :class="node.isOnline ? 'text-green-400' : 'text-red-400'" class="fas fa-circle mr-2 text-xs"></i>
<span :class="node.isOnline ? 'text-green-400' : 'text-red-400'" class="text-xs font-medium" x-text="node.isOnline ? 'Online' : 'Offline'"></span>
<div class="flex items-center bg-[var(--color-bg-primary)] rounded-lg px-3 py-1.5 border border-[var(--color-border-subtle)]">
<i :class="node.isOnline ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'" class="fas fa-circle mr-2 text-xs"></i>
<span :class="node.isOnline ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'" class="text-xs font-medium" x-text="node.isOnline ? 'Online' : 'Offline'"></span>
</div>
</div>
<!-- Footer with timestamp -->
<div class="text-xs text-gray-500 pt-3 border-t border-gray-700/30 flex items-center">
<div class="text-xs text-[var(--color-text-muted)] pt-3 border-t border-[var(--color-border-subtle)]/30 flex items-center">
<i class="fas fa-clock mr-2"></i>
<span x-text="'Updated: ' + new Date().toLocaleTimeString()"></span>
</div>
@@ -366,19 +368,19 @@
</div>
<div class="p-6">
<h3 class="text-2xl font-bold text-white mb-6">
<i class="fa-solid fa-book text-blue-400 mr-2"></i> Start a federated instance
<h3 class="text-2xl font-bold text-[var(--color-text-primary)] mb-6">
<i class="fa-solid fa-book text-[var(--color-primary)] mr-2"></i> Start a federated instance
</h3>
<!-- Tabs navigation -->
<ul class="mb-5 flex list-none flex-row flex-wrap ps-0 border border-gray-700/50 rounded-lg overflow-hidden" role="tablist" data-twe-nav-ref>
<ul class="mb-5 flex list-none flex-row flex-wrap ps-0 border border-[var(--color-border-subtle)] rounded-lg overflow-hidden" role="tablist" data-twe-nav-ref>
<li role="presentation" class="flex-auto text-center">
<a href="#tabs-federated-cli" class="tablink block border-0 bg-gray-800 px-7 py-4 text-sm font-medium uppercase leading-tight text-white hover:bg-gray-700 focus:bg-gray-700 data-[twe-nav-active]:border-blue-500 data-[twe-nav-active]:text-blue-400 data-[twe-nav-active]:bg-gray-700 active transition-all duration-200" data-twe-toggle="pill" data-twe-target="#tabs-federated-cli" data-twe-nav-active role="tab" aria-controls="tabs-federated-cli" aria-selected="true">
<a href="#tabs-federated-cli" class="tablink block border-0 bg-[var(--color-bg-primary)] px-7 py-4 text-sm font-medium uppercase leading-tight text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] focus:bg-[var(--color-bg-secondary)] data-[twe-nav-active]:border-[var(--color-primary)] data-[twe-nav-active]:text-[var(--color-primary)] data-[twe-nav-active]:bg-[var(--color-bg-secondary)] active transition-all duration-200" data-twe-toggle="pill" data-twe-target="#tabs-federated-cli" data-twe-nav-active role="tab" aria-controls="tabs-federated-cli" aria-selected="true">
<i class="fa-solid fa-terminal mr-2"></i> CLI
</a>
</li>
<li role="presentation" class="flex-auto text-center">
<a href="#tabs-federated-docker" class="tablink block border-0 bg-gray-800 px-7 py-4 text-sm font-medium uppercase leading-tight text-white hover:bg-gray-700 focus:bg-gray-700 data-[twe-nav-active]:border-blue-500 data-[twe-nav-active]:text-blue-400 data-[twe-nav-active]:bg-gray-700 transition-all duration-200" data-twe-toggle="pill" data-twe-target="#tabs-federated-docker" role="tab" aria-controls="tabs-federated-docker" aria-selected="false">
<a href="#tabs-federated-docker" class="tablink block border-0 bg-[var(--color-bg-primary)] px-7 py-4 text-sm font-medium uppercase leading-tight text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] focus:bg-[var(--color-bg-secondary)] data-[twe-nav-active]:border-[var(--color-primary)] data-[twe-nav-active]:text-[var(--color-primary)] data-[twe-nav-active]:bg-[var(--color-bg-secondary)] transition-all duration-200" data-twe-toggle="pill" data-twe-target="#tabs-federated-docker" role="tab" aria-controls="tabs-federated-docker" aria-selected="false">
<i class="fa-solid fa-box-open mr-2"></i> Container images
</a>
</li>
@@ -387,64 +389,64 @@
<!-- Tabs content -->
<div class="mb-6">
<div class="tabcontent hidden opacity-100 transition-opacity duration-150 ease-linear data-[twe-tab-active]:block p-4" id="tabs-federated-cli" role="tabpanel" aria-labelledby="tabs-federated-cli" data-twe-tab-active>
<div class="bg-gray-900/50 rounded-xl border border-gray-700/50 p-6">
<div class="bg-[var(--color-bg-primary)]/50 rounded-xl border border-[var(--color-border-subtle)]/50 p-6">
<div class="flex items-center justify-between mb-4">
<h4 class="text-lg font-bold text-white">
<h4 class="text-lg font-bold text-[var(--color-text-primary)]">
Start a new instance to share:
</h4>
<button onclick="copyClipboard('export TOKEN=\'{{.P2PToken}}\'\nlocal-ai run --federated --p2p')" class="bg-gray-700 hover:bg-gray-600 text-gray-300 p-2 rounded-lg transition-colors duration-200">
<button onclick="copyClipboard('export TOKEN=\'{{.P2PToken}}\'\nlocal-ai run --federated --p2p')" class="bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] p-2 rounded-lg transition-colors duration-200 border border-[var(--color-border-subtle)]">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<code class="block bg-gray-800 text-yellow-300 p-4 rounded-lg break-words mb-4 border border-gray-700/50">
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words mb-4 border border-[var(--color-border-subtle)]/50">
# Start a new instance to share with --federated and a TOKEN<br>
export TOKEN="<span class="token">{{.P2PToken}}</span>"<br>
local-ai run --federated --p2p</code>
<p class="text-gray-400 text-sm mt-2">Note: If you don't have a token do not specify it and use the generated one that you can find in this page.</p>
<p class="text-[var(--color-text-secondary)] text-sm mt-2">Note: If you don't have a token do not specify it and use the generated one that you can find in this page.</p>
<div class="flex items-center justify-between mb-4 mt-8">
<h4 class="text-lg font-bold text-white">
<h4 class="text-lg font-bold text-[var(--color-text-primary)]">
Start a new federated load balancer:
</h4>
<button onclick="copyClipboard('export TOKEN=\'{{.P2PToken}}\'\nlocal-ai federated')" class="bg-gray-700 hover:bg-gray-600 text-gray-300 p-2 rounded-lg transition-colors duration-200">
<button onclick="copyClipboard('export TOKEN=\'{{.P2PToken}}\'\nlocal-ai federated')" class="bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] p-2 rounded-lg transition-colors duration-200 border border-[var(--color-border-subtle)]">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<code class="block bg-gray-800 text-yellow-300 p-4 rounded-lg break-words mb-4 border border-gray-700/50">
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words mb-4 border border-[var(--color-border-subtle)]/50">
export TOKEN="<span class="token">{{.P2PToken}}</span>"<br>
local-ai federated</code>
<p class="text-gray-400 text-sm mt-2">Note: Token is needed when starting the federated server.</p>
<p class="text-[var(--color-text-secondary)] text-sm mt-2">Note: Token is needed when starting the federated server.</p>
<p class="text-gray-300 mt-4">For all the options available, please refer to the <a href="https://localai.io/features/distribute/#starting-workers" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">documentation</a>.</p>
<p class="text-[var(--color-text-secondary)] mt-4">For all the options available, please refer to the <a href="https://localai.io/features/distribute/#starting-workers" target="_blank" class="text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 transition-colors">documentation</a>.</p>
</div>
</div>
<div class="tabcontent hidden opacity-0 transition-opacity duration-150 ease-linear data-[twe-tab-active]:block p-4" id="tabs-federated-docker" role="tabpanel" aria-labelledby="tabs-federated-docker">
<div class="bg-gray-900/50 rounded-xl border border-gray-700/50 p-6">
<div class="bg-[var(--color-bg-primary)]/50 rounded-xl border border-[var(--color-border-subtle)]/50 p-6">
<div class="flex items-center justify-between mb-4">
<h4 class="text-lg font-bold text-white">
<h4 class="text-lg font-bold text-[var(--color-text-primary)]">
Start a new federated instance:
</h4>
<button onclick="copyClipboard('docker run -ti --net host -e TOKEN=\'{{.P2PToken}}\' --name local-ai -p 8080:8080 localai/localai:latest-cpu run --federated --p2p')" class="bg-gray-700 hover:bg-gray-600 text-gray-300 p-2 rounded-lg transition-colors duration-200">
<button onclick="copyClipboard('docker run -ti --net host -e TOKEN=\'{{.P2PToken}}\' --name local-ai -p 8080:8080 localai/localai:latest-cpu run --federated --p2p')" class="bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] p-2 rounded-lg transition-colors duration-200 border border-[var(--color-border-subtle)]">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<code class="block bg-gray-800 text-yellow-300 p-4 rounded-lg break-words mb-4 border border-gray-700/50">
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words mb-4 border border-[var(--color-border-subtle)]/50">
docker run -ti --net host -e TOKEN="<span class="token">{{.P2PToken}}</span>" --name local-ai -p 8080:8080 localai/localai:latest-cpu run --federated --p2p</code>
<div class="flex items-center justify-between mb-4 mt-8">
<h4 class="text-lg font-bold text-white">
<h4 class="text-lg font-bold text-[var(--color-text-primary)]">
Start a new federated server with Docker (port to 9090):
</h4>
<button onclick="copyClipboard('docker run -ti --net host -e TOKEN=\'{{.P2PToken}}\' --name local-ai -p 9090:8080 localai/localai:latest-cpu federated')" class="bg-gray-700 hover:bg-gray-600 text-gray-300 p-2 rounded-lg transition-colors duration-200">
<button onclick="copyClipboard('docker run -ti --net host -e TOKEN=\'{{.P2PToken}}\' --name local-ai -p 9090:8080 localai/localai:latest-cpu federated')" class="bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] p-2 rounded-lg transition-colors duration-200 border border-[var(--color-border-subtle)]">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<code class="block bg-gray-800 text-yellow-300 p-4 rounded-lg break-words mb-4 border border-gray-700/50">
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words mb-4 border border-[var(--color-border-subtle)]/50">
docker run -ti --net host -e TOKEN="<span class="token">{{.P2PToken}}</span>" --name local-ai -p 9090:8080 localai/localai:latest-cpu federated</code>
<p class="text-gray-300 mt-4">For all the options available and see what image to use, please refer to the <a href="https://localai.io/basics/container/" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">Container images documentation</a> and <a href="https://localai.io/advanced/#cli-parameters" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">CLI parameters documentation</a>.</p>
<p class="text-[var(--color-text-secondary)] mt-4">For all the options available and see what image to use, please refer to the <a href="https://localai.io/basics/container/" target="_blank" class="text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 transition-colors">Container images documentation</a> and <a href="https://localai.io/advanced/#cli-parameters" target="_blank" class="text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 transition-colors">CLI parameters documentation</a>.</p>
</div>
</div>
</div>
@@ -452,30 +454,30 @@ docker run -ti --net host -e TOKEN="<span class="token">{{.P2PToken}}</span>" --
</div>
<!-- Workers Box -->
<div class="bg-[#1E293B] border border-[#8B5CF6]/20 rounded-xl mb-10">
<div class="p-8 border-b border-gray-700/50">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 rounded-xl mb-10">
<div class="p-8 border-b border-[var(--color-border-subtle)]">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center">
<div class="w-14 h-14 bg-purple-500/20 rounded-2xl flex items-center justify-center mr-4">
<i class="text-purple-400 fa-solid fa-puzzle-piece text-2xl fa-spin-pulse"></i>
<div class="w-14 h-14 bg-[var(--color-accent-light)] rounded-2xl flex items-center justify-center mr-4">
<i class="text-[var(--color-accent)] fa-solid fa-puzzle-piece text-2xl fa-spin-pulse"></i>
</div>
<div>
<h2 class="text-2xl font-bold text-white">Worker Network</h2>
<p class="text-purple-300 text-sm">Distributed model computation (llama.cpp)</p>
<h2 class="text-2xl font-bold text-[var(--color-text-primary)]">Worker Network</h2>
<p class="text-[var(--color-accent)] text-sm">Distributed model computation (llama.cpp)</p>
</div>
</div>
<div class="text-right">
<div class="text-sm text-gray-400 mb-1">Active Workers</div>
<div class="text-sm text-[var(--color-text-secondary)] mb-1">Active Workers</div>
<div class="text-3xl font-bold">
<span :class="stats.workers.online > 0 ? 'text-purple-400' : 'text-red-400'" x-text="stats.workers.online"></span>
<span class="text-gray-400 text-xl">/<span x-text="stats.workers.total"></span></span>
<span :class="stats.workers.online > 0 ? 'text-[var(--color-accent)]' : 'text-[var(--color-error)]'" x-text="stats.workers.online"></span>
<span class="text-[var(--color-text-secondary)] text-xl">/<span x-text="stats.workers.total"></span></span>
</div>
</div>
</div>
<div class="bg-purple-900/20 rounded-xl p-4 mb-6 border border-purple-700/30">
<p class="text-gray-300 text-sm leading-relaxed">
<i class="fas fa-lightbulb text-purple-400 mr-2"></i>
<div class="bg-[var(--color-accent-light)] rounded-xl p-4 mb-6 border border-[var(--color-accent)]/30">
<p class="text-[var(--color-text-secondary)] text-sm leading-relaxed">
<i class="fas fa-lightbulb text-[var(--color-accent)] mr-2"></i>
Deploy llama.cpp workers to split model weights across multiple devices. This enables processing larger models by distributing computational load and memory requirements.
</p>
</div>
@@ -483,36 +485,36 @@ docker run -ti --net host -e TOKEN="<span class="token">{{.P2PToken}}</span>" --
<!-- Workers Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<template x-if="workerNodes.length === 0">
<div class="col-span-full flex flex-col items-center justify-center py-12 text-center bg-gray-800/50 border border-gray-700/50 rounded-xl">
<i class="fas fa-server text-gray-500 text-4xl mb-4"></i>
<p class="text-gray-400 text-lg font-medium">No workers available</p>
<p class="text-gray-500 text-sm mt-2">Start some workers to see them here</p>
<div class="col-span-full flex flex-col items-center justify-center py-12 text-center bg-[var(--color-bg-primary)]/50 border border-[var(--color-border-subtle)]/50 rounded-xl">
<i class="fas fa-server text-[var(--color-text-muted)] text-4xl mb-4"></i>
<p class="text-[var(--color-text-secondary)] text-lg font-medium">No workers available</p>
<p class="text-[var(--color-text-muted)] text-sm mt-2">Start some workers to see them here</p>
</div>
</template>
<template x-for="node in workerNodes" :key="node.id">
<div :class="node.isOnline ? 'border-green-400/50' : 'border-red-400/50'"
class="bg-[#101827] border rounded-lg p-5 transition-colors">
<div :class="node.isOnline ? 'border-[var(--color-success)]/50' : 'border-[var(--color-error)]/50'"
class="bg-[var(--color-bg-primary)] border rounded-lg p-5 transition-colors">
<!-- Header with node icon and status -->
<div class="flex items-center justify-between mb-4">
<!-- Node info -->
<div class="flex items-center">
<div class="w-10 h-10 bg-purple-500/20 rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-server text-purple-400 text-lg"></i>
<div class="w-10 h-10 bg-[var(--color-accent-light)] rounded-lg flex items-center justify-center mr-3">
<i class="fas fa-server text-[var(--color-accent)] text-lg"></i>
</div>
<div>
<h4 class="text-white font-semibold text-sm">Worker</h4>
<p class="text-gray-400 text-xs font-mono break-all" x-text="node.id"></p>
<h4 class="text-[var(--color-text-primary)] font-semibold text-sm">Worker</h4>
<p class="text-[var(--color-text-secondary)] text-xs font-mono break-all" x-text="node.id"></p>
</div>
</div>
<!-- Status badge -->
<div class="flex items-center bg-[#101827] rounded-lg px-3 py-1.5 border border-[#1E293B]">
<i :class="node.isOnline ? 'text-green-400' : 'text-red-400'" class="fas fa-circle mr-2 text-xs"></i>
<span :class="node.isOnline ? 'text-green-400' : 'text-red-400'" class="text-xs font-medium" x-text="node.isOnline ? 'Online' : 'Offline'"></span>
<div class="flex items-center bg-[var(--color-bg-primary)] rounded-lg px-3 py-1.5 border border-[var(--color-border-subtle)]">
<i :class="node.isOnline ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'" class="fas fa-circle mr-2 text-xs"></i>
<span :class="node.isOnline ? 'text-[var(--color-success)]' : 'text-[var(--color-error)]'" class="text-xs font-medium" x-text="node.isOnline ? 'Online' : 'Offline'"></span>
</div>
</div>
<!-- Footer with timestamp -->
<div class="text-xs text-gray-500 pt-3 border-t border-gray-700/30 flex items-center">
<div class="text-xs text-[var(--color-text-muted)] pt-3 border-t border-[var(--color-border-subtle)]/30 flex items-center">
<i class="fas fa-clock mr-2"></i>
<span x-text="'Updated: ' + new Date().toLocaleTimeString()"></span>
</div>
@@ -522,19 +524,19 @@ docker run -ti --net host -e TOKEN="<span class="token">{{.P2PToken}}</span>" --
</div>
<div class="p-8">
<h3 class="text-2xl font-bold text-white mb-6">
<i class="fa-solid fa-book text-purple-400 mr-2"></i> Start a new llama.cpp worker
<h3 class="text-2xl font-bold text-[var(--color-text-primary)] mb-6">
<i class="fa-solid fa-book text-[var(--color-accent)] mr-2"></i> Start a new llama.cpp worker
</h3>
<!-- Tabs navigation -->
<ul class="mb-5 flex list-none flex-row flex-wrap ps-0 border border-gray-700/50 rounded-lg overflow-hidden" role="tablist" data-twe-nav-ref>
<ul class="mb-5 flex list-none flex-row flex-wrap ps-0 border border-[var(--color-border-subtle)] rounded-lg overflow-hidden" role="tablist" data-twe-nav-ref>
<li role="presentation" class="flex-auto text-center">
<a href="#tabs-cli" class="tablink block border-0 bg-gray-800 px-7 py-4 text-sm font-medium uppercase leading-tight text-white hover:bg-gray-700 focus:bg-gray-700 data-[twe-nav-active]:border-purple-500 data-[twe-nav-active]:text-purple-400 data-[twe-nav-active]:bg-gray-700 active transition-all duration-200" data-twe-toggle="pill" data-twe-target="#tabs-cli" data-twe-nav-active role="tab" aria-controls="tabs-cli" aria-selected="true">
<a href="#tabs-cli" class="tablink block border-0 bg-[var(--color-bg-primary)] px-7 py-4 text-sm font-medium uppercase leading-tight text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] focus:bg-[var(--color-bg-secondary)] data-[twe-nav-active]:border-[var(--color-accent)] data-[twe-nav-active]:text-[var(--color-accent)] data-[twe-nav-active]:bg-[var(--color-bg-secondary)] active transition-all duration-200" data-twe-toggle="pill" data-twe-target="#tabs-cli" data-twe-nav-active role="tab" aria-controls="tabs-cli" aria-selected="true">
<i class="fa-solid fa-terminal mr-2"></i> CLI
</a>
</li>
<li role="presentation" class="flex-auto text-center">
<a href="#tabs-docker" class="tablink block border-0 bg-gray-800 px-7 py-4 text-sm font-medium uppercase leading-tight text-white hover:bg-gray-700 focus:bg-gray-700 data-[twe-nav-active]:border-purple-500 data-[twe-nav-active]:text-purple-400 data-[twe-nav-active]:bg-gray-700 transition-all duration-200" data-twe-toggle="pill" data-twe-target="#tabs-docker" role="tab" aria-controls="tabs-docker" aria-selected="false">
<a href="#tabs-docker" class="tablink block border-0 bg-[var(--color-bg-primary)] px-7 py-4 text-sm font-medium uppercase leading-tight text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] focus:bg-[var(--color-bg-secondary)] data-[twe-nav-active]:border-[var(--color-accent)] data-[twe-nav-active]:text-[var(--color-accent)] data-[twe-nav-active]:bg-[var(--color-bg-secondary)] transition-all duration-200" data-twe-toggle="pill" data-twe-target="#tabs-docker" role="tab" aria-controls="tabs-docker" aria-selected="false">
<i class="fa-solid fa-box-open mr-2"></i> Container images
</a>
</li>
@@ -543,36 +545,36 @@ docker run -ti --net host -e TOKEN="<span class="token">{{.P2PToken}}</span>" --
<!-- Tabs content -->
<div class="mb-6">
<div class="tabcontent hidden opacity-100 transition-opacity duration-150 ease-linear data-[twe-tab-active]:block p-4" id="tabs-cli" role="tabpanel" aria-labelledby="tabs-cli" data-twe-tab-active>
<div class="bg-gray-900/50 rounded-xl border border-gray-700/50 p-6">
<div class="bg-[var(--color-bg-primary)]/50 rounded-xl border border-[var(--color-border-subtle)]/50 p-6">
<div class="flex items-center justify-between mb-4">
<h4 class="text-lg font-bold text-white">
<h4 class="text-lg font-bold text-[var(--color-text-primary)]">
Start a new worker:
</h4>
<button onclick="copyClipboard('export TOKEN=\'{{.P2PToken}}\'\nlocal-ai worker p2p-llama-cpp-rpc')" class="bg-gray-700 hover:bg-gray-600 text-gray-300 p-2 rounded-lg transition-colors duration-200">
<button onclick="copyClipboard('export TOKEN=\'{{.P2PToken}}\'\nlocal-ai worker p2p-llama-cpp-rpc')" class="bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] p-2 rounded-lg transition-colors duration-200 border border-[var(--color-border-subtle)]">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<code class="block bg-gray-800 text-yellow-300 p-4 rounded-lg break-words mb-4 border border-gray-700/50">
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words mb-4 border border-[var(--color-border-subtle)]/50">
export TOKEN="<span class="token">{{.P2PToken}}</span>"<br>
local-ai worker p2p-llama-cpp-rpc</code>
<p class="text-gray-300 mt-4">For all the options available, please refer to the <a href="https://localai.io/features/distribute/#starting-workers" target="_blank" class="text-purple-400 hover:text-purple-300 transition-colors">documentation</a>.</p>
<p class="text-[var(--color-text-secondary)] mt-4">For all the options available, please refer to the <a href="https://localai.io/features/distribute/#starting-workers" target="_blank" class="text-[var(--color-accent)] hover:text-[var(--color-accent)]/80 transition-colors">documentation</a>.</p>
</div>
</div>
<div class="tabcontent hidden opacity-0 transition-opacity duration-150 ease-linear data-[twe-tab-active]:block p-4" id="tabs-docker" role="tabpanel" aria-labelledby="tabs-docker">
<div class="bg-gray-900/50 rounded-xl border border-gray-700/50 p-6">
<div class="bg-[var(--color-bg-primary)]/50 rounded-xl border border-[var(--color-border-subtle)]/50 p-6">
<div class="flex items-center justify-between mb-4">
<h4 class="text-lg font-bold text-white">
<h4 class="text-lg font-bold text-[var(--color-text-primary)]">
Start a new worker with Docker:
</h4>
<button onclick="copyClipboard('docker run -ti --net host -e TOKEN=\'{{.P2PToken}}\' --name local-ai -p 8080:8080 localai/localai:latest-cpu worker p2p-llama-cpp-rpc')" class="bg-gray-700 hover:bg-gray-600 text-gray-300 p-2 rounded-lg transition-colors duration-200">
<button onclick="copyClipboard('docker run -ti --net host -e TOKEN=\'{{.P2PToken}}\' --name local-ai -p 8080:8080 localai/localai:latest-cpu worker p2p-llama-cpp-rpc')" class="bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] p-2 rounded-lg transition-colors duration-200 border border-[var(--color-border-subtle)]">
<i class="fa-solid fa-copy"></i>
</button>
</div>
<code class="block bg-gray-800 text-yellow-300 p-4 rounded-lg break-words mb-4 border border-gray-700/50">
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words mb-4 border border-[var(--color-border-subtle)]/50">
docker run -ti --net host -e TOKEN="<span class="token">{{.P2PToken}}</span>" --name local-ai -p 8080:8080 localai/localai:latest-cpu worker p2p-llama-cpp-rpc</code>
<p class="text-gray-300 mt-4">For all the options available and see what image to use, please refer to the <a href="https://localai.io/basics/container/" target="_blank" class="text-purple-400 hover:text-purple-300 transition-colors">Container images documentation</a> and <a href="https://localai.io/advanced/#cli-parameters" target="_blank" class="text-purple-400 hover:text-purple-300 transition-colors">CLI parameters documentation</a>.</p>
<p class="text-[var(--color-text-secondary)] mt-4">For all the options available and see what image to use, please refer to the <a href="https://localai.io/basics/container/" target="_blank" class="text-[var(--color-accent)] hover:text-[var(--color-accent)]/80 transition-colors">Container images documentation</a> and <a href="https://localai.io/advanced/#cli-parameters" target="_blank" class="text-[var(--color-accent)] hover:text-[var(--color-accent)]/80 transition-colors">CLI parameters documentation</a>.</p>
</div>
</div>
</div>
@@ -583,7 +585,6 @@ docker run -ti --net host -e TOKEN="<span class="token">{{.P2PToken}}</span>" --
{{ end }}
</div>
{{template "views/partials/footer" .}}
</div>
{{ if ne .P2PToken "" }}
<script src="static/p2panimation.js"></script>
@@ -708,5 +709,10 @@ function p2pNetwork() {
</script>
{{ end }}
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>

View File

@@ -1,43 +1,37 @@
<footer class="bg-[#101827] border-t border-[#1E293B] py-8 mt-auto">
<div class="container mx-auto px-4">
<div class="flex flex-col items-center justify-center space-y-4">
<!-- Logo & Version -->
<div class="flex items-center space-x-2">
{{ if .Version }}
<span class="text-sm md:text-base font-medium text-[#94A3B8]">LocalAI Version <span class="text-[#38BDF8] font-semibold">{{.Version}}</span></span>
{{ end }}
</div>
<!-- Links -->
<div class="flex flex-wrap justify-center gap-x-6 gap-y-3">
<a href="https://github.com/mudler/LocalAI"
class="group flex items-center text-[#94A3B8] hover:text-[#38BDF8] transition duration-300 ease-in-out"
target="_blank">
<i class="fab fa-github mr-2 text-lg group-hover:scale-110 transition-transform"></i>
<span>GitHub</span>
<i class="fas fa-external-link-alt text-xs ml-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></i>
</a>
<a href="https://localai.io"
class="group flex items-center text-[#94A3B8] hover:text-[#38BDF8] transition duration-300 ease-in-out"
target="_blank">
<i class="fas fa-book mr-2 text-lg group-hover:scale-110 transition-transform"></i>
<span>Documentation</span>
<i class="fas fa-external-link-alt text-xs ml-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></i>
</a>
<a href="https://mudler.pm"
class="group flex items-center text-[#94A3B8] hover:text-[#38BDF8] transition duration-300 ease-in-out"
target="_blank">
<i class="fas fa-user mr-2 text-lg group-hover:scale-110 transition-transform"></i>
<span>Author</span>
<i class="fas fa-external-link-alt text-xs ml-1.5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></i>
</a>
</div>
<!-- Copyright Notice -->
<div class="mt-4 text-sm text-[#94A3B8]">
<span>© 2023-2025 <a href="https://mudler.pm" class="text-[#38BDF8] hover:text-[#8B5CF6] transition duration-300" target="_blank">Ettore Di Giacinto</a></span>
</div>
<footer class="bg-[var(--color-bg-secondary)] border-t border-[var(--color-border-subtle)] py-6 mt-auto">
<div class="container mx-auto px-6">
<div class="flex flex-col items-center justify-center space-y-3">
{{ if .Version }}
<span class="text-xs text-[var(--color-text-secondary)]">
LocalAI <span class="text-[var(--color-primary)] font-medium">{{.Version}}</span>
</span>
{{ end }}
<div class="flex flex-wrap justify-center gap-x-5 gap-y-2">
<a href="https://github.com/mudler/LocalAI"
class="group flex items-center text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors"
target="_blank">
<i class="fab fa-github mr-1.5 text-sm"></i>
<span>GitHub</span>
</a>
<a href="https://localai.io"
class="group flex items-center text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors"
target="_blank">
<i class="fas fa-book mr-1.5 text-sm"></i>
<span>Documentation</span>
</a>
<a href="https://mudler.pm"
class="group flex items-center text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors"
target="_blank">
<i class="fas fa-user mr-1.5 text-sm"></i>
<span>Author</span>
</a>
</div>
<div class="text-xs text-[var(--color-text-muted)]">
<span>© 2023-2025 <a href="https://mudler.pm" class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors" target="_blank">Ettore Di Giacinto</a></span>
</div>
</div>
<script src="static/assets/tw-elements.js"></script>
</footer>
</div>
<script src="static/assets/tw-elements.js"></script>
</footer>

View File

@@ -37,6 +37,58 @@
preflight: false,
},
};
// Theme Management
(function() {
const THEME_KEY = 'localai-theme';
const DARK = 'dark';
const LIGHT = 'light';
function getStoredTheme() {
return localStorage.getItem(THEME_KEY);
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? DARK : LIGHT;
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
// Also set class for Tailwind compatibility
if (theme === DARK) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || DARK;
const newTheme = current === DARK ? LIGHT : DARK;
localStorage.setItem(THEME_KEY, newTheme);
applyTheme(newTheme);
return newTheme;
}
// Initialize theme immediately to prevent flash
const stored = getStoredTheme();
const initialTheme = stored || getSystemTheme();
applyTheme(initialTheme);
// Expose toggle function globally
window.toggleTheme = toggleTheme;
window.getCurrentTheme = function() {
return document.documentElement.getAttribute('data-theme') || DARK;
};
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (!getStoredTheme()) {
applyTheme(e.matches ? DARK : LIGHT);
}
});
})();
function copyClipboard(token) {
// Try modern Clipboard API first (requires secure context)
if (navigator.clipboard && window.isSecureContext) {

View File

@@ -6,23 +6,23 @@
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="sticky top-0 left-0 right-0 z-40 bg-[#1E293B]/95 backdrop-blur-sm border-b border-[#38BDF8]/50">
class="sticky top-0 left-0 right-0 z-40 bg-[var(--color-bg-secondary)]/95 backdrop-blur-sm border-b border-[var(--color-primary)]/50">
<div class="container mx-auto px-4 py-3">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-2">
<div class="relative">
<i class="fas fa-spinner fa-spin text-[#38BDF8] text-lg"></i>
<i class="fas fa-spinner fa-spin text-[var(--color-primary)] text-lg"></i>
</div>
<h3 class="text-[#E5E7EB] font-semibold text-sm">
<h3 class="text-[var(--color-text-primary)] font-semibold text-sm">
Operations in Progress
<span class="ml-2 bg-[#38BDF8]/20 px-2 py-1 rounded-full text-xs border border-[#38BDF8]/30" x-text="operations.length"></span>
<span class="ml-2 bg-[var(--color-primary-light)] px-2 py-1 rounded-full text-xs border border-[var(--color-primary-border)]" x-text="operations.length"></span>
</h3>
</div>
</div>
<button @click="collapsed = !collapsed"
class="text-[#94A3B8] hover:text-[#E5E7EB] transition-colors">
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
<i class="fas" :class="collapsed ? 'fa-chevron-down' : 'fa-chevron-up'"></i>
</button>
</div>
@@ -37,28 +37,28 @@
x-transition:leave-end="opacity-0 max-h-0"
class="space-y-2 overflow-y-auto max-h-96">
<template x-for="operation in operations" :key="operation.id">
<div class="bg-[#101827]/80 rounded-lg p-3 border border-[#1E293B] hover:border-[#38BDF8]/50 transition-colors">
<div class="bg-[var(--color-bg-primary)]/80 rounded-lg p-3 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/50 transition-colors">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center space-x-3 flex-1 min-w-0">
<!-- Icon based on type -->
<div class="flex-shrink-0">
<i class="text-lg"
:class="{
'fas fa-cube text-[#38BDF8]': !operation.isBackend && !operation.isDeletion,
'fas fa-cubes text-[#8B5CF6]': operation.isBackend && !operation.isDeletion,
'fas fa-trash text-red-400': operation.isDeletion
'fas fa-cube text-[var(--color-primary)]': !operation.isBackend && !operation.isDeletion,
'fas fa-cubes text-[var(--color-accent)]': operation.isBackend && !operation.isDeletion,
'fas fa-trash text-[var(--color-error)]': operation.isDeletion
}"></i>
</div>
<!-- Operation details -->
<div class="flex-1 min-w-0">
<div class="flex items-center space-x-2">
<span class="text-[#E5E7EB] font-medium text-sm truncate" x-text="operation.name"></span>
<span class="text-[var(--color-text-primary)] font-medium text-sm truncate" x-text="operation.name"></span>
<span class="flex-shrink-0 text-xs px-2 py-0.5 rounded border"
:class="{
'bg-[#38BDF8]/10 text-[#38BDF8]': !operation.isDeletion && !operation.isBackend,
'bg-[#8B5CF6]/10 text-[#8B5CF6]': !operation.isDeletion && operation.isBackend,
'bg-red-500/10 text-red-300': operation.isDeletion
'bg-[var(--color-primary-light)] text-[var(--color-primary)]': !operation.isDeletion && !operation.isBackend,
'bg-[var(--color-accent-light)] text-[var(--color-accent)]': !operation.isDeletion && operation.isBackend,
'bg-[var(--color-error-light)] text-[var(--color-error)]': operation.isDeletion
}"
x-text="operation.isBackend ? 'Backend' : 'Model'"></span>
</div>
@@ -66,35 +66,35 @@
<!-- Status message -->
<div class="flex items-center space-x-2 mt-1">
<template x-if="operation.isQueued">
<span class="text-xs text-[#38BDF8] flex items-center">
<span class="text-xs text-[var(--color-primary)] flex items-center">
<i class="fas fa-clock mr-1"></i>
Queued
</span>
</template>
<template x-if="operation.isCancelled">
<span class="text-xs text-red-400 flex items-center">
<span class="text-xs text-[var(--color-error)] flex items-center">
<i class="fas fa-ban mr-1"></i>
Cancelling...
</span>
</template>
<template x-if="!operation.isQueued && !operation.isCancelled && operation.message">
<span class="text-xs text-[#94A3B8] truncate" x-text="operation.message"></span>
<span class="text-xs text-[var(--color-text-secondary)] truncate" x-text="operation.message"></span>
</template>
</div>
</div>
<!-- Progress percentage and cancel button -->
<div class="flex-shrink-0 text-right flex items-center space-x-2">
<span class="text-[#E5E7EB] font-bold text-lg" x-text="operation.progress + '%'"></span>
<span class="text-[var(--color-text-primary)] font-bold text-lg" x-text="operation.progress + '%'"></span>
<template x-if="operation.cancellable && !operation.isCancelled">
<button @click="cancelOperation(operation.jobID, operation.id)"
class="text-red-400 hover:text-red-300 transition-colors p-1 rounded hover:bg-red-500/20"
class="text-[var(--color-error)] hover:text-[var(--color-error)] transition-colors p-1 rounded hover:bg-[var(--color-error-light)]"
title="Cancel operation">
<i class="fas fa-times"></i>
</button>
</template>
<template x-if="operation.isCancelled">
<span class="text-red-400 text-xs flex items-center">
<span class="text-[var(--color-error)] text-xs flex items-center">
<i class="fas fa-ban mr-1"></i>
Cancelled
</span>
@@ -104,11 +104,11 @@
</div>
<!-- Progress bar -->
<div class="w-full bg-[#101827] rounded-full h-2 overflow-hidden border border-[#1E293B]">
<div class="w-full bg-[var(--color-bg-primary)] rounded-full h-2 overflow-hidden border border-[var(--color-border-subtle)]">
<div class="h-full rounded-full transition-all duration-300"
:class="{
'bg-[#38BDF8]': !operation.isDeletion && !operation.isCancelled,
'bg-red-500': operation.isDeletion || operation.isCancelled
'bg-[var(--color-primary)]': !operation.isDeletion && !operation.isCancelled,
'bg-[var(--color-error)]': operation.isDeletion || operation.isCancelled
}"
:style="'width: ' + operation.progress + '%'">
</div>

View File

@@ -1,175 +1,200 @@
<nav class="bg-[var(--color-bg-primary)] shadow-2xl border-b border-[var(--color-bg-secondary)]">
<div class="container mx-auto px-4 py-2">
<div class="flex items-center justify-between">
<div class="flex items-center">
<!-- Logo Image -->
<a href="./" class="flex items-center group">
<img src="static/logo_horizontal.png"
alt="LocalAI Logo"
class="h-10 mr-3 brightness-110 transition-all duration-300 group-hover:brightness-125 group-hover:drop-shadow-[0_0_8px_var(--color-primary-border)]">
</a>
</div>
<!-- Menu button for small screens -->
<div class="lg:hidden">
<button id="menu-toggle" class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] focus:outline-none p-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)]">
<i class="fas fa-bars fa-lg"></i>
</button>
</div>
<!-- Navigation links -->
<div class="hidden lg:flex lg:items-center lg:justify-end lg:space-x-1" x-data="{ manageOpen: false }">
<a href="./" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fas fa-home text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Home
</a>
<a href="chat/" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fa-solid fa-comments text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Chat
</a>
<a href="image/" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fas fa-image text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Images
</a>
<a href="video/" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fas fa-video text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Video
</a>
<a href="tts/" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fa-solid fa-music text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>TTS
</a>
<a href="sound/" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fas fa-volume-high text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Sound
</a>
<a href="talk/" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fa-solid fa-phone text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Talk
</a>
<a href="agent-jobs" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fas fa-tasks text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Agent Jobs
</a>
<a href="traces/" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fas fa-chart-line text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Traces
</a>
<a href="swagger/index.html" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fas fa-code text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>API
</a>
<!-- System Dropdown -->
<div class="relative" @click.away="manageOpen = false">
<button @click="manageOpen = !manageOpen"
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-2 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-secondary)] flex items-center group text-sm">
<i class="fas fa-cog text-[var(--color-primary)] mr-1.5 text-sm group-hover:scale-110 transition-transform"></i>Settings
<i class="fas fa-chevron-down ml-1 text-xs transition-transform" :class="manageOpen ? 'rotate-180' : ''"></i>
</button>
<div x-show="manageOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute top-full right-0 mt-1 w-48 bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded-lg shadow-lg z-50 py-1">
<a href="browse/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-primary)] px-3 py-2 text-sm transition-colors flex items-center">
<i class="fas fa-brain text-[var(--color-primary)] mr-2 text-xs"></i>Models
</a>
<a href="browse/backends" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-primary)] px-3 py-2 text-sm transition-colors flex items-center">
<i class="fas fa-server text-[var(--color-primary)] mr-2 text-xs"></i>Backends
</a>
<a href="p2p/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-primary)] px-3 py-2 text-sm transition-colors flex items-center">
<i class="fa-solid fa-circle-nodes text-[var(--color-primary)] mr-2 text-xs"></i>Swarm
</a>
<a href="/manage" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-primary)] px-3 py-2 text-sm transition-colors flex items-center">
<i class="fas fa-cog text-[var(--color-primary)] mr-2 text-xs"></i>System
</a>
</div>
</div>
</div>
</div>
<!-- Collapsible menu for small screens -->
<div class="hidden lg:hidden" id="mobile-menu" x-data="{ manageOpen: false }">
<div class="pt-3 pb-2 space-y-1 border-t border-[var(--color-bg-secondary)] mt-2">
<a href="./" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-home text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Home
</a>
<a href="chat/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fa-solid fa-comments text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Chat
</a>
<a href="image/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-image text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Images
</a>
<a href="video/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-video text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Video
</a>
<a href="tts/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fa-solid fa-music text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>TTS
</a>
<a href="sound/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-volume-high text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Sound
</a>
<a href="talk/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fa-solid fa-phone text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Talk
</a>
<a href="agent-jobs" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-tasks text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Agent Jobs
</a>
<a href="traces/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-chart-line text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Traces
</a>
<a href="swagger/index.html" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-code text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>API
</a>
<!-- System with submenu -->
<div>
<button @click="manageOpen = !manageOpen"
class="w-full text-left text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center justify-between text-sm">
<div class="flex items-center">
<i class="fas fa-cog text-[var(--color-primary)] mr-3 w-5 text-center text-sm"></i>Settings
</div>
<i class="fas fa-chevron-down text-xs transition-transform" :class="manageOpen ? 'rotate-180' : ''"></i>
</button>
<div x-show="manageOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 max-h-0"
x-transition:enter-end="opacity-100 max-h-96"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 max-h-96"
x-transition:leave-end="opacity-0 max-h-0"
class="overflow-hidden">
<a href="browse/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] pl-8 pr-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-brain text-[var(--color-primary)] mr-3 w-5 text-center text-xs"></i>Models
</a>
<a href="browse/backends" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] pl-8 pr-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-server text-[var(--color-primary)] mr-3 w-5 text-center text-xs"></i>Backends
</a>
<a href="p2p/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] pl-8 pr-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fa-solid fa-circle-nodes text-[var(--color-primary)] mr-3 w-5 text-center text-xs"></i>Swarm
</a>
<a href="/manage" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] pl-8 pr-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center text-sm">
<i class="fas fa-cog text-[var(--color-primary)] mr-3 w-5 text-center text-xs"></i>System
</a>
</div>
</div>
</div>
</div>
<!-- Mobile Menu Button -->
<button id="mobile-menu-btn" class="mobile-menu-btn" aria-label="Open menu">
<i class="fas fa-bars"></i>
</button>
<!-- Mobile Overlay -->
<div id="sidebar-overlay" class="sidebar-overlay"></div>
<!-- Sidebar Navigation -->
<aside id="sidebar" class="sidebar">
<!-- Logo Header -->
<div class="sidebar-header">
<a href="./" class="block">
<img src="static/logo_horizontal.png" alt="LocalAI" class="w-full h-auto px-2">
</a>
<button id="sidebar-close-btn" class="sidebar-close-btn" aria-label="Close menu">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Navigation Content -->
<nav class="sidebar-content">
<!-- Main Section -->
<div class="sidebar-section">
<a href="./" class="nav-item" :class="{ 'active': window.location.pathname === '/' || window.location.pathname.endsWith('/index.html') }">
<i class="fas fa-home nav-icon"></i>
<span class="nav-label">Home</span>
</a>
<a href="browse/" class="nav-item">
<i class="fas fa-download nav-icon"></i>
<span class="nav-label">Install Models</span>
</a>
<a href="chat/" class="nav-item">
<i class="fa-solid fa-comments nav-icon"></i>
<span class="nav-label">Chat</span>
</a>
<a href="image/" class="nav-item">
<i class="fas fa-image nav-icon"></i>
<span class="nav-label">Images</span>
</a>
<a href="video/" class="nav-item">
<i class="fas fa-video nav-icon"></i>
<span class="nav-label">Video</span>
</a>
<a href="tts/" class="nav-item">
<i class="fa-solid fa-music nav-icon"></i>
<span class="nav-label">TTS</span>
</a>
<a href="sound/" class="nav-item">
<i class="fas fa-volume-high nav-icon"></i>
<span class="nav-label">Sound</span>
</a>
<a href="talk/" class="nav-item">
<i class="fa-solid fa-phone nav-icon"></i>
<span class="nav-label">Talk</span>
</a>
</div>
</nav>
<!-- Tools Section -->
<div class="sidebar-section">
<div class="sidebar-section-title">Tools</div>
<a href="agent-jobs" class="nav-item">
<i class="fas fa-tasks nav-icon"></i>
<span class="nav-label">Agent Jobs</span>
</a>
<a href="traces/" class="nav-item">
<i class="fas fa-chart-line nav-icon"></i>
<span class="nav-label">Traces</span>
</a>
</div>
<!-- System Section -->
<div class="sidebar-section">
<div class="sidebar-section-title">System</div>
<a href="swagger/index.html" class="nav-item">
<i class="fas fa-code nav-icon"></i>
<span class="nav-label">API</span>
</a>
<a href="browse/backends" class="nav-item">
<i class="fas fa-server nav-icon"></i>
<span class="nav-label">Backends</span>
</a>
<a href="p2p/" class="nav-item">
<i class="fa-solid fa-circle-nodes nav-icon"></i>
<span class="nav-label">Swarm</span>
</a>
<a href="/manage" class="nav-item">
<i class="fas fa-cog nav-icon"></i>
<span class="nav-label">Settings</span>
</a>
</div>
</nav>
<!-- Theme Toggle Footer -->
<div class="sidebar-footer">
<div class="theme-toggle">
<span class="theme-toggle-label">
<i class="fas fa-sun" id="theme-icon-light"></i>
<i class="fas fa-moon" id="theme-icon-dark" style="display: none;"></i>
<span id="theme-label">Dark</span>
</span>
<div
class="toggle-switch"
id="theme-toggle-switch"
onclick="toggleTheme && toggleTheme(); updateThemeUI();"
role="switch"
aria-label="Toggle theme"
tabindex="0"
onkeydown="if(event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleTheme && toggleTheme(); updateThemeUI(); }"
></div>
</div>
</div>
</aside>
<script>
// JavaScript to toggle the mobile menu with animation
document.getElementById('menu-toggle').addEventListener('click', function () {
var mobileMenu = document.getElementById('mobile-menu');
if (mobileMenu.classList.contains('hidden')) {
mobileMenu.classList.remove('hidden');
// Use setTimeout to create a mild animation effect
setTimeout(function() {
mobileMenu.classList.add('opacity-100');
mobileMenu.classList.remove('opacity-0');
}, 10);
} else {
mobileMenu.classList.add('opacity-0');
mobileMenu.classList.remove('opacity-100');
// Wait for transition to finish before hiding
setTimeout(function() {
mobileMenu.classList.add('hidden');
}, 300);
// Mobile menu functionality
(function() {
const menuBtn = document.getElementById('mobile-menu-btn');
const closeBtn = document.getElementById('sidebar-close-btn');
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebar-overlay');
function openSidebar() {
sidebar.classList.add('open');
overlay.classList.add('open');
if (menuBtn) menuBtn.style.opacity = '0';
if (menuBtn) menuBtn.style.pointerEvents = 'none';
document.body.style.overflow = 'hidden';
}
function closeSidebar() {
sidebar.classList.remove('open');
overlay.classList.remove('open');
if (menuBtn) menuBtn.style.opacity = '1';
if (menuBtn) menuBtn.style.pointerEvents = '';
document.body.style.overflow = '';
}
if (menuBtn) {
menuBtn.addEventListener('click', openSidebar);
}
if (closeBtn) {
closeBtn.addEventListener('click', closeSidebar);
}
if (overlay) {
overlay.addEventListener('click', closeSidebar);
}
// Close sidebar on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && sidebar.classList.contains('open')) {
closeSidebar();
}
});
// Close sidebar when clicking a nav link on mobile
const navLinks = sidebar.querySelectorAll('.nav-item');
navLinks.forEach(function(link) {
if (link.tagName === 'A') {
link.addEventListener('click', function() {
if (window.innerWidth < 1024) {
closeSidebar();
}
});
</script>
});
}
});
})();
// Theme UI update function
function updateThemeUI() {
const theme = (window.getCurrentTheme && window.getCurrentTheme()) || 'dark';
const toggle = document.getElementById('theme-toggle-switch');
const label = document.getElementById('theme-label');
const iconLight = document.getElementById('theme-icon-light');
const iconDark = document.getElementById('theme-icon-dark');
if (toggle) {
if (theme === 'light') {
toggle.classList.add('active');
if (label) label.textContent = 'Light';
if (iconLight) iconLight.style.display = 'none';
if (iconDark) iconDark.style.display = 'inline';
} else {
toggle.classList.remove('active');
if (label) label.textContent = 'Dark';
if (iconLight) iconLight.style.display = 'inline';
if (iconDark) iconDark.style.display = 'none';
}
}
}
// Initialize theme UI on load
document.addEventListener('DOMContentLoaded', updateThemeUI);
// Also run immediately in case DOM is already ready
if (document.readyState !== 'loading') {
updateThemeUI();
}
</script>

View File

@@ -1,4 +1,4 @@
<nav class="bg-gradient-to-r from-gray-900 to-gray-950 shadow-lg border-b border-gray-800/50">
<nav class="bg-[var(--color-bg-secondary)] shadow-lg border-b border-[var(--color-border-subtle)]">
<div class="container mx-auto px-4 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center">
@@ -6,44 +6,44 @@
<a href="./" class="flex items-center group">
<img src="static/logo_horizontal.png"
alt="LocalAI Logo"
class="h-10 mr-3 rounded-lg border border-blue-600/30 shadow-md transition-all duration-300 group-hover:shadow-blue-500/20 group-hover:border-blue-500/50">
class="h-10 mr-3 rounded-lg border border-[var(--color-primary-border)] shadow-md transition-all duration-300 group-hover:shadow-[var(--color-primary)]/20 group-hover:border-[var(--color-primary)]/50">
</a>
</div>
<!-- Menu button for small screens -->
<div class="lg:hidden">
<button id="menu-toggle" class="text-gray-300 hover:text-blue-400 focus:outline-none p-2 rounded-lg transition duration-300 ease-in-out hover:bg-gray-800/70">
<button id="menu-toggle" class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] focus:outline-none p-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-primary)]">
<i class="fas fa-bars fa-lg"></i>
</button>
</div>
<!-- Navigation links -->
<div class="hidden lg:flex lg:items-center lg:justify-end lg:space-x-1">
<a href="./" class="text-gray-300 hover:text-white px-3 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-blue-900/30 flex items-center">
<i class="fas fa-home text-blue-400 mr-2"></i>Home
<a href="./" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-primary)] flex items-center">
<i class="fas fa-home text-[var(--color-primary)] mr-2"></i>Home
</a>
<a href="https://localai.io" target="_blank" class="text-gray-300 hover:text-white px-3 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-blue-900/30 flex items-center group">
<i class="fas fa-book-reader text-blue-400 mr-2"></i>Documentation
<a href="https://localai.io" target="_blank" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-primary)] flex items-center group">
<i class="fas fa-book-reader text-[var(--color-primary)] mr-2"></i>Documentation
<i class="fas fa-external-link-alt text-xs ml-1 opacity-70 group-hover:opacity-100 transition-opacity"></i>
</a>
<a href="https://models.localai.io/" class="text-gray-300 hover:text-white px-3 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-blue-900/30 flex items-center">
<i class="fas fa-brain text-blue-400 mr-2"></i>Models
<a href="https://models.localai.io/" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out hover:bg-[var(--color-bg-primary)] flex items-center">
<i class="fas fa-brain text-[var(--color-primary)] mr-2"></i>Models
</a>
</div>
</div>
<!-- Collapsible menu for small screens -->
<div class="hidden lg:hidden" id="mobile-menu">
<div class="pt-3 pb-2 space-y-1 border-t border-gray-800/50 mt-2">
<a href="./" class="block text-gray-300 hover:text-white hover:bg-blue-900/30 px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center">
<i class="fas fa-home text-blue-400 mr-3 w-5 text-center"></i>Home
<div class="pt-3 pb-2 space-y-1 border-t border-[var(--color-border-subtle)] mt-2">
<a href="./" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-primary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center">
<i class="fas fa-home text-[var(--color-primary)] mr-3 w-5 text-center"></i>Home
</a>
<a href="https://localai.io" target="_blank" class="block text-gray-300 hover:text-white hover:bg-blue-900/30 px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center">
<i class="fas fa-book-reader text-blue-400 mr-3 w-5 text-center"></i>Documentation
<a href="https://localai.io" target="_blank" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-primary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center">
<i class="fas fa-book-reader text-[var(--color-primary)] mr-3 w-5 text-center"></i>Documentation
<i class="fas fa-external-link-alt text-xs ml-1 opacity-70"></i>
</a>
<a href="https://models.localai.io/" class="block text-gray-300 hover:text-white hover:bg-blue-900/30 px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center">
<i class="fas fa-brain text-blue-400 mr-3 w-5 text-center"></i>Models
<a href="https://models.localai.io/" class="block text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-primary)] px-3 py-2 rounded-lg transition duration-300 ease-in-out flex items-center">
<i class="fas fa-brain text-[var(--color-primary)] mr-3 w-5 text-center"></i>Models
</a>
</div>
</div>
@@ -71,4 +71,4 @@
}
});
</script>
</script>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="settingsDashboard()">
{{template "views/partials/navbar" .}}
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="settingsDashboard()">
<!-- Notifications -->
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
@@ -71,7 +73,7 @@
<input type="checkbox" x-model="settings.watchdog_enabled"
@change="updateWatchdogEnabled()"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
@@ -85,7 +87,7 @@
<input type="checkbox" x-model="settings.watchdog_idle_enabled"
:disabled="!settings.watchdog_enabled"
class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
@@ -110,7 +112,7 @@
<input type="checkbox" x-model="settings.watchdog_busy_enabled"
:disabled="!settings.watchdog_enabled"
class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
@@ -146,7 +148,7 @@
<input type="checkbox" x-model="settings.force_eviction_when_busy"
:disabled="!settings.watchdog_enabled"
class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
@@ -226,7 +228,7 @@
<input type="checkbox" x-model="settings.memory_reclaimer_enabled"
:disabled="!settings.watchdog_enabled"
class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
@@ -279,7 +281,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.parallel_backend_requests"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
</label>
</div>
</div>
@@ -325,7 +327,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.f16"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
</label>
</div>
@@ -338,7 +340,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.debug"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
</label>
</div>
@@ -351,7 +353,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.enable_tracing"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
</label>
</div>
@@ -390,7 +392,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.cors"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-warning-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-warning)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-warning-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-warning)]"></div>
</label>
</div>
@@ -412,7 +414,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.csrf"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-warning-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-warning)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-warning-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-warning)]"></div>
</label>
</div>
</div>
@@ -456,7 +458,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.federated"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
</label>
</div>
</div>
@@ -551,7 +553,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.autoload_galleries"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
</label>
</div>
@@ -564,7 +566,7 @@
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.autoload_backend_galleries"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
</label>
</div>
@@ -616,7 +618,6 @@
</form>
</div>
{{template "views/partials/footer" .}}
</div>
<script>
@@ -922,6 +923,10 @@ function resourceStatus() {
}
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>

View File

@@ -4,9 +4,11 @@
<script defer src="static/sound.js"></script>
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen">
{{template "views/partials/navbar" .}}
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow">
<!-- Hero Section -->
<div class="hero-section">
@@ -169,6 +171,8 @@
</div>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
<script>

View File

@@ -2,10 +2,12 @@
<html lang="en">
{{template "views/partials/head" .}}
<script defer src="static/talk.js"></script>
<body class="bg-[#101827] text-[#E5E7EB]" x-data="{ key: $store.chat.key }">
<div class="flex flex-col min-h-screen">
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]" x-data="{ key: $store.chat.key }">
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow">
<!-- Hero Section -->
@@ -24,25 +26,25 @@
<!-- Talk Interface Body -->
<div class="p-6">
<!-- Recording Status -->
<div id="recording" class="bg-red-500/10 border border-red-500/30 rounded-lg p-4 mb-4 flex items-center space-x-3" style="display: none;">
<i class="fa-solid fa-microphone text-2xl text-red-400"></i>
<span class="text-red-300 font-medium">Recording... press "Stop recording" to stop</span>
<div id="recording" class="bg-[var(--color-error-light)] border border-[var(--color-error)]/30 rounded-lg p-4 mb-4 flex items-center space-x-3" style="display: none;">
<i class="fa-solid fa-microphone text-2xl text-[var(--color-error)]"></i>
<span class="text-[var(--color-error)] font-medium">Recording... press "Stop recording" to stop</span>
</div>
<!-- Loader -->
<div id="loader" class="my-4 flex justify-center" style="display: none;">
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-[#38BDF8]"></div>
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-[var(--color-primary)]"></div>
</div>
<!-- Status Text -->
<div id="statustext" class="my-4 p-3 bg-[#101827]/50 border border-[#1E293B] rounded-lg text-[#E5E7EB]" style="min-height: 3rem;">Press the record button to start recording.</div>
<div id="statustext" class="my-4 p-3 bg-[var(--color-bg-primary)]/50 border border-[var(--color-border-subtle)] rounded-lg text-[var(--color-text-primary)]" style="min-height: 3rem;">Press the record button to start recording.</div>
<!-- Note -->
<div class="bg-[#38BDF8]/10 border border-[#38BDF8]/20 rounded-lg p-4 mb-6">
<div class="bg-[var(--color-primary-light)] border border-[var(--color-primary)]/20 rounded-lg p-4 mb-6">
<div class="flex items-start">
<i class="fas fa-info-circle text-[#38BDF8] mt-1 mr-3 flex-shrink-0"></i>
<p class="text-[#94A3B8]">
<strong class="text-[#38BDF8]">Note:</strong> You need an LLM, an audio-transcription (whisper), and a TTS model installed for this to work. Select the appropriate models below and click 'Talk' to start recording. The recording will continue until you click 'Stop recording'. Make sure your microphone is set up and enabled.
<i class="fas fa-info-circle text-[var(--color-primary)] mt-1 mr-3 flex-shrink-0"></i>
<p class="text-[var(--color-text-secondary)]">
<strong class="text-[var(--color-primary)]">Note:</strong> You need an LLM, an audio-transcription (whisper), and a TTS model installed for this to work. Select the appropriate models below and click 'Talk' to start recording. The recording will continue until you click 'Stop recording'. Make sure your microphone is set up and enabled.
</p>
</div>
</div>
@@ -51,42 +53,42 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<!-- LLM Model -->
<div class="space-y-2">
<label for="modelSelect" class="flex items-center text-[#94A3B8] font-medium">
<i class="fas fa-brain text-[#38BDF8] mr-2"></i>LLM Model
<label for="modelSelect" class="flex items-center text-[var(--color-text-secondary)] font-medium">
<i class="fas fa-brain text-[var(--color-primary)] mr-2"></i>LLM Model
</label>
<select id="modelSelect"
class="w-full bg-[#101827] text-[#E5E7EB] border border-[#1E293B] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50 rounded-lg shadow-sm p-2.5 appearance-none">
<option value="" disabled class="text-[#94A3B8]">Select a model</option>
class="w-full bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] border border-[var(--color-border-subtle)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50 rounded-lg shadow-sm p-2.5 appearance-none">
<option value="" disabled class="text-[var(--color-text-secondary)]">Select a model</option>
{{ range .ModelsConfig }}
<option value="{{.}}" class="bg-[#101827] text-[#E5E7EB]">{{.}}</option>
<option value="{{.}}" class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{.}}</option>
{{ end }}
</select>
</div>
<!-- Whisper Model -->
<div class="space-y-2">
<label for="whisperModelSelect" class="flex items-center text-[#94A3B8] font-medium">
<i class="fas fa-ear-listen text-[#8B5CF6] mr-2"></i>Whisper Model
<label for="whisperModelSelect" class="flex items-center text-[var(--color-text-secondary)] font-medium">
<i class="fas fa-ear-listen text-[var(--color-accent)] mr-2"></i>Whisper Model
</label>
<select id="whisperModelSelect"
class="w-full bg-[#101827] text-[#E5E7EB] border border-[#1E293B] focus:border-[#8B5CF6] focus:ring-2 focus:ring-[#8B5CF6]/50 rounded-lg shadow-sm p-2.5 appearance-none">
<option value="" disabled class="text-[#94A3B8]">Select a model</option>
class="w-full bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] border border-[var(--color-border-subtle)] focus:border-[var(--color-accent)] focus:ring-2 focus:ring-[var(--color-accent)]/50 rounded-lg shadow-sm p-2.5 appearance-none">
<option value="" disabled class="text-[var(--color-text-secondary)]">Select a model</option>
{{ range .ModelsConfig }}
<option value="{{.}}" class="bg-[#101827] text-[#E5E7EB]">{{.}}</option>
<option value="{{.}}" class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{.}}</option>
{{ end }}
</select>
</div>
<!-- TTS Model -->
<div class="space-y-2">
<label for="ttsModelSelect" class="flex items-center text-[#94A3B8] font-medium">
<i class="fas fa-volume-high text-green-400 mr-2"></i>TTS Model
<label for="ttsModelSelect" class="flex items-center text-[var(--color-text-secondary)] font-medium">
<i class="fas fa-volume-high text-[var(--color-success)] mr-2"></i>TTS Model
</label>
<select id="ttsModelSelect"
class="w-full bg-[#101827] text-[#E5E7EB] border border-[#1E293B] focus:border-green-500 focus:ring-2 focus:ring-green-500/50 rounded-lg shadow-sm p-2.5 appearance-none">
<option value="" disabled class="text-[#94A3B8]">Select a model</option>
class="w-full bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] border border-[var(--color-border-subtle)] focus:border-[var(--color-success)] focus:ring-2 focus:ring-[var(--color-success)]/50 rounded-lg shadow-sm p-2.5 appearance-none">
<option value="" disabled class="text-[var(--color-text-secondary)]">Select a model</option>
{{ range .ModelsConfig }}
<option value="{{.}}" class="bg-[#101827] text-[#E5E7EB]">{{.}}</option>
<option value="{{.}}" class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{.}}</option>
{{ end }}
</select>
</div>
@@ -95,15 +97,15 @@
<!-- Buttons -->
<div class="flex items-center justify-between mt-8">
<button id="recordButton"
class="inline-flex items-center bg-red-500 hover:bg-red-600 text-white font-semibold py-2 px-6 rounded-lg transition-colors">
class="inline-flex items-center bg-[var(--color-error)] hover:bg-[var(--color-error)]/90 text-white font-semibold py-2 px-6 rounded-lg transition-colors">
<i class="fas fa-microphone mr-2"></i>
<span>Talk</span>
</button>
<a id="resetButton"
class="flex items-center text-[#38BDF8] hover:text-[#8B5CF6] transition-colors"
class="flex items-center text-[var(--color-primary)] hover:text-[var(--color-accent)] transition-colors"
href="#">
<i class="fas fa-rotate-right mr-2"></i>
<i class="fas fa-rotate-left mr-2"></i>
<span>Reset conversation</span>
</a>
</div>
@@ -116,6 +118,8 @@
</div>
{{template "views/partials/footer" .}}
</div>
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -3,9 +3,11 @@
{{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="flex flex-col min-h-screen" x-data="tracesApp()" x-init="init()">
{{template "views/partials/navbar" .}}
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="tracesApp()" x-init="init()">
<!-- Notifications -->
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
@@ -73,7 +75,7 @@
<input type="checkbox" x-model="settings.enable_tracing"
@change="updateTracingEnabled()"
class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label>
</div>
@@ -155,7 +157,6 @@
</div>
{{template "views/partials/footer" .}}
</div>
@@ -330,5 +331,10 @@ function tracesApp() {
}
</script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>

View File

@@ -3,10 +3,12 @@
{{template "views/partials/head" .}}
<script defer src="static/tts.js"></script>
<body class="bg-[#101827] text-[#E5E7EB]">
<div class="flex flex-col min-h-screen">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner">
<div class="container mx-auto px-4 py-8 flex-grow">
<!-- Hero Section -->
<div class="hero-section">
@@ -22,12 +24,12 @@
<div class="max-w-3xl mx-auto">
<div class="card overflow-hidden">
<!-- Header with Model Selection -->
<div class="border-b border-[#1E293B] p-5">
<div class="border-b border-[var(--color-border-subtle)] p-5">
<div class="flex flex-col sm:flex-row items-center justify-between gap-4">
<!-- Model Selection -->
<div class="flex items-center" x-data="{ link : '{{ if .Model }}tts/{{.Model}}{{ end }}' }">
<label for="model-select" class="mr-3 text-[#94A3B8] font-medium">
<i class="fas fa-microphone-lines text-[#8B5CF6] mr-2"></i>Model:
<label for="model-select" class="mr-3 text-[var(--color-text-secondary)] font-medium">
<i class="fas fa-microphone-lines text-[var(--color-accent)] mr-2"></i>Model:
</label>
<select
id="model-select"
@@ -35,18 +37,18 @@
@change="window.location = link"
class="input p-2.5"
>
<option value="" disabled class="text-[#94A3B8]">Select a model</option>
<option value="" disabled class="text-[var(--color-text-secondary)]">Select a model</option>
{{ $model:=.Model}}
{{ range .ModelsConfig }}
{{ $cfg := . }}
{{ range .KnownUsecaseStrings }}
{{ if eq . "FLAG_TTS" }}
<option value="tts/{{$cfg.Name}}" {{ if eq $cfg.Name $model }} selected {{end}} class="bg-[#101827] text-[#E5E7EB]">{{$cfg.Name}}</option>
<option value="tts/{{$cfg.Name}}" {{ if eq $cfg.Name $model }} selected {{end}} class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{$cfg.Name}}</option>
{{ end }}
{{ end }}
{{ end }}
{{ range .ModelsWithoutConfig }}
<option value="tts/{{.}}" {{ if eq . $model }} selected {{ end }} class="bg-[#101827] text-[#E5E7EB]">{{.}}</option>
<option value="tts/{{.}}" {{ if eq . $model }} selected {{ end }} class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">{{.}}</option>
{{end}}
</select>
</div>
@@ -55,10 +57,10 @@
<!-- Input Area -->
<div class="p-6">
<div class="bg-[#8B5CF6]/10 border border-[#8B5CF6]/20 rounded-lg p-4 mb-6">
<div class="bg-[var(--color-accent-light)] border border-[var(--color-accent)]/20 rounded-lg p-4 mb-6">
<div class="flex items-start">
<i class="fas fa-info-circle text-[#8B5CF6] mt-1 mr-3 flex-shrink-0"></i>
<p class="text-[#94A3B8]">
<i class="fas fa-info-circle text-[var(--color-accent)] mt-1 mr-3 flex-shrink-0"></i>
<p class="text-[var(--color-text-secondary)]">
Enter your text below and submit to generate speech with the selected TTS model.
The generated audio will appear below the input field.
</p>
@@ -77,7 +79,7 @@
class="input w-full p-4 pl-4 pr-12"
required
/>
<button type="submit" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-[#8B5CF6] hover:text-[#38BDF8] transition icon-hover">
<button type="submit" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-[var(--color-accent)] hover:text-[var(--color-primary)] transition icon-hover">
<i class="fas fa-paper-plane"></i>
</button>
</div>
@@ -85,11 +87,11 @@
<!-- Loading indicator -->
<div class="flex justify-center my-6">
<div id="loader" class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-[#8B5CF6]" style="display: none;"></div>
<div id="loader" class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-[var(--color-accent)]" style="display: none;"></div>
</div>
<!-- Results Area -->
<div class="bg-[#101827]/50 border border-[#1E293B] rounded-lg p-4 min-h-[100px] flex items-center justify-center">
<div class="bg-[var(--color-bg-primary)]/50 border border-[var(--color-border-subtle)] rounded-lg p-4 min-h-[100px] flex items-center justify-center">
<div id="result" class="w-full"></div>
</div>
</div>
@@ -98,6 +100,8 @@
</div>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body>
</html>
</html>

View File

@@ -3,10 +3,12 @@
{{template "views/partials/head" .}}
<script defer src="static/video.js"></script>
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen">
<div class="flex flex-col flex-1 overflow-hidden">
{{template "views/partials/navbar" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
<div class="app-layout">
{{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">
<!-- 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">
@@ -274,6 +276,8 @@
</div>
</div>
</div>
</main>
</div>
<script>

View File

@@ -27,8 +27,8 @@ import (
"github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/xsync"
"github.com/mudler/cogito"
"github.com/robfig/cron/v3"
"github.com/mudler/xlog"
"github.com/robfig/cron/v3"
)
// AgentJobService manages agent tasks and job execution
@@ -676,6 +676,7 @@ func (s *AgentJobService) executeJobInternal(job schema.Job, task schema.Task, c
job.Status = schema.JobStatusRunning
job.StartedAt = &now
s.jobs.Set(job.ID, job)
xlog.Info("Job started", "job_id", job.ID, "task_id", job.TaskID)
// Load model config
modelConfig, err := s.configLoader.LoadModelConfigFileByNameDefaultOptions(task.Model, s.appConfig)
@@ -893,17 +894,6 @@ func (s *AgentJobService) executeJobInternal(job schema.Job, task schema.Task, c
return fmt.Errorf("failed to execute tools: %w", err)
}
// Get final response
f, err = defaultLLM.Ask(ctx, f)
if err != nil {
job.Status = schema.JobStatusFailed
job.Error = fmt.Sprintf("failed to get response: %v", err)
completedAt := time.Now()
job.CompletedAt = &completedAt
s.jobs.Set(job.ID, job)
return fmt.Errorf("failed to get response: %w", err)
}
// Extract traces from fragment.Status after execution
// This provides complete information about tool calls and results
// We use Status data to supplement/replace callback data for completeness
@@ -980,6 +970,7 @@ func (s *AgentJobService) executeJobInternal(job schema.Job, task schema.Task, c
job.Result = f.LastMessage().Content
job.CompletedAt = &completedAt
s.jobs.Set(job.ID, job)
xlog.Info("Job completed", "job_id", job.ID, "status", job.Status)
// Save to file (async)
go func() {

View File

@@ -12,7 +12,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
var _ = Describe("InstallExternalBackend", func() {

View File

@@ -13,7 +13,7 @@ import (
"github.com/mudler/LocalAI/pkg/system"
"github.com/mudler/LocalAI/pkg/utils"
"github.com/mudler/xlog"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
const (

View File

@@ -122,3 +122,4 @@ LocalAI supports various types of backends:
- **Diffusion Backends**: For image generation
- **TTS Backends**: For text-to-speech conversion
- **Whisper Backends**: For speech-to-text conversion
- **Sound Generation Backends**: For music and audio generation (e.g., ACE-Step)

View File

@@ -78,12 +78,16 @@ mcp:
}
agent:
max_attempts: 3 # Maximum number of tool execution attempts
max_iterations: 3 # Maximum number of reasoning iterations
enable_reasoning: true # Enable tool reasoning capabilities
enable_planning: false # Enable auto-planning capabilities
enable_mcp_prompts: false # Enable MCP prompts
max_attempts: 3 # Maximum number of tool execution attempts
max_iterations: 3 # Maximum number of reasoning iterations
enable_reasoning: true # Enable tool reasoning capabilities
enable_planning: false # Enable auto-planning capabilities
enable_mcp_prompts: false # Enable MCP prompts
enable_plan_re_evaluator: false # Enable plan re-evaluation
disable_sink_state: false # Disable sink state behavior
loop_detection: 3 # Loop detection sensitivity level
max_adjustment_attempts: 5 # Maximum adjustment attempts for tool calls
force_reasoning_tool: false # Force reasoning tool usage
```
### Configuration Options
@@ -104,12 +108,21 @@ Configure local command-based MCP servers:
#### Agent Configuration (`agent`)
Configure agent behavior and tool execution:
- **`max_attempts`**: Maximum number of tool execution attempts (default: 3)
- **`max_iterations`**: Maximum number of reasoning iterations (default: 3)
- **`enable_reasoning`**: Enable tool reasoning capabilities (default: false)
- **`enable_planning`**: Enable auto-planning capabilities (default: false)
- **`enable_mcp_prompts`**: Enable MCP prompts (default: false)
- **`enable_plan_re_evaluator`**: Enable plan re-evaluation (default: false)
**Execution Control**
- **`max_attempts`**: Maximum number of tool execution attempts (default: 3). Higher values provide more resilience but may increase response time.
- **`max_iterations`**: Maximum number of reasoning iterations (default: 3). More iterations allow for complex multi-step problem solving.
- **`loop_detection`**: Loop detection sensitivity level (default: 0, disabled). Set to a positive integer (e.g., 3) to enable loop detection and prevent infinite execution cycles.
- **`max_adjustment_attempts`**: Maximum adjustment attempts for tool calls (default: 5). Prevents infinite loops when adjusting tool call parameters.
**Reasoning and Planning**
- **`enable_reasoning`**: Enable tool reasoning capabilities (default: false). When enabled, the agent uses advanced reasoning to better understand tool results.
- **`enable_planning`**: Enable auto-planning capabilities (default: false). When enabled, breaks down complex tasks into manageable steps.
- **`disable_sink_state`**: Disable sink state behavior (default: false). When enabled, prevents the agent from entering a sink state.
- **`force_reasoning_tool`**: Force reasoning tool usage (default: false). When enabled, always use the reasoning tool in the agent's reasoning process.
**MCP Integration**
- **`enable_mcp_prompts`**: Enable MCP prompts (default: false). When enabled, uses specialized prompts exposed by MCP servers.
- **`enable_plan_re_evaluator`**: Enable plan re-evaluation (default: false). When enabled, dynamically adjusts execution plans based on results.
## Usage
@@ -186,9 +199,13 @@ The `agent` section controls how the AI model interacts with MCP tools:
### Execution Control
- **`max_attempts`**: Limits how many times a tool can be retried if it fails. Higher values provide more resilience but may increase response time.
- **`max_iterations`**: Controls the maximum number of reasoning cycles the agent can perform. More iterations allow for complex multi-step problem solving.
- **`loop_detection`**: Set to a positive integer (e.g., 3) to enable loop detection and prevent infinite execution cycles. Default is 0 (disabled).
- **`max_adjustment_attempts`**: Limits the number of times the agent can adjust tool call parameters. Prevents infinite loops during tool execution (default: 5).
### Reasoning Capabilities
- **`enable_reasoning`**: When enabled, the agent uses advanced reasoning to better understand tool results and plan next steps.
- **`force_reasoning_tool`**: When enabled, forces the agent to always use the reasoning tool in its reasoning process, ensuring explicit reasoning steps.
- **`disable_sink_state`**: When enabled, prevents the agent from entering a sink state where it stops making progress.
### Planning Capabilities
- **`enable_planning`**: When enabled, the agent uses auto-planning to break down complex tasks into manageable steps and execute them systematically. The agent will automatically detect when planning is needed.
@@ -198,8 +215,9 @@ The `agent` section controls how the AI model interacts with MCP tools:
### Recommended Settings
- **Simple tasks**: `max_attempts: 2`, `max_iterations: 2`, `enable_reasoning: false`, `enable_planning: false`
- **Complex tasks**: `max_attempts: 5`, `max_iterations: 5`, `enable_reasoning: true`, `enable_planning: true`, `enable_mcp_prompts: true`
- **Advanced planning**: `max_attempts: 5`, `max_iterations: 5`, `enable_reasoning: true`, `enable_planning: true`, `enable_mcp_prompts: true`, `enable_plan_re_evaluator: true`
- **Advanced planning**: `max_attempts: 5`, `max_iterations: 5`, `enable_reasoning: true`, `enable_planning: true`, `enable_mcp_prompts: true`, `enable_plan_re_evaluator: true`, `loop_detection: 3`
- **Development/Debugging**: `max_attempts: 1`, `max_iterations: 1`, `enable_reasoning: true`, `enable_planning: true`
- **Aggressive loop prevention**: `max_attempts: 5`, `max_iterations: 5`, `loop_detection: 2`, `max_adjustment_attempts: 3`, `force_reasoning_tool: true`
## How It Works

View File

@@ -14,7 +14,7 @@ LocalAI to ease out installations of models provide a way to preload models on s
{{% notice note %}}
The models in this gallery are not directly maintained by LocalAI. If you find a model that is not working, please open an issue on the model gallery repository.
The models in this gallery are not directly maintained by LocalAI. If you find a model that is not working, please open an issue on the [main LocalAI repository](https://github.com/mudler/LocalAI/issues).
{{% /notice %}}
{{% notice note %}}

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