mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-24 08:38:02 -04:00
Compare commits
105 Commits
fix/step-f
...
fix/ci-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aaddbb3b8 | ||
|
|
0063e5d68f | ||
|
|
c7c4a20a9e | ||
|
|
94539f3992 | ||
|
|
525278658d | ||
|
|
919f801e25 | ||
|
|
362eb261c5 | ||
|
|
d407f4ead5 | ||
|
|
1fc8ad854f | ||
|
|
f49a8edd87 | ||
|
|
510b830d2b | ||
|
|
ddb36468ed | ||
|
|
983db7bedc | ||
|
|
b260378694 | ||
|
|
b10443ab5a | ||
|
|
b647b6caf1 | ||
|
|
c187b160e7 | ||
|
|
42e580bed0 | ||
|
|
5e13193d84 | ||
|
|
1c5dc83232 | ||
|
|
73b997686a | ||
|
|
00abf1be1f | ||
|
|
959458f0db | ||
|
|
dfc6efb88d | ||
|
|
65082b3a6f | ||
|
|
0483d47674 | ||
|
|
8ad40091a6 | ||
|
|
8bfe458fbc | ||
|
|
657ba8cdad | ||
|
|
fb86f6461d | ||
|
|
1027c487a6 | ||
|
|
bb226d1eaa | ||
|
|
b032cf489b | ||
|
|
3ac7301f31 | ||
|
|
c4783a0a05 | ||
|
|
c44f03b882 | ||
|
|
eeec92af78 | ||
|
|
842033b8b5 | ||
|
|
a2941228a7 | ||
|
|
791e6b84ee | ||
|
|
d845c39963 | ||
|
|
1331e23b67 | ||
|
|
36ff2a0138 | ||
|
|
db6ba4ef07 | ||
|
|
d19dcac863 | ||
|
|
fd42675bec | ||
|
|
3391538806 | ||
|
|
c4f879c4ea | ||
|
|
b7e0de54fe | ||
|
|
f0868acdf3 | ||
|
|
9a5b5ee8a9 | ||
|
|
ed0bfb8732 | ||
|
|
be84b1d258 | ||
|
|
cbedcc9091 | ||
|
|
e45d63c86e | ||
|
|
f40c8dd0ce | ||
|
|
559ab99890 | ||
|
|
91f2dd5820 | ||
|
|
8250815763 | ||
|
|
b1b67b973e | ||
|
|
fcecc12e57 | ||
|
|
51902df7ba | ||
|
|
05f3ae31de | ||
|
|
bb0924dff1 | ||
|
|
51eec4e6b8 | ||
|
|
462c82fad2 | ||
|
|
352b8aaa1b | ||
|
|
df792d6243 | ||
|
|
b1c434f0fc | ||
|
|
bb42b342de | ||
|
|
e555057f8b | ||
|
|
76fba02e56 | ||
|
|
dadc7158fb | ||
|
|
68c7077491 | ||
|
|
b471619ad9 | ||
|
|
a0476d5567 | ||
|
|
a2228f1418 | ||
|
|
7dd9a155a3 | ||
|
|
4fe830ff58 | ||
|
|
86b3bc9313 | ||
|
|
2fabdc08e6 | ||
|
|
ed832cf0e0 | ||
|
|
95db1da309 | ||
|
|
9e692967c3 | ||
|
|
ecba23d44e | ||
|
|
067a255435 | ||
|
|
637ecba382 | ||
|
|
46c64e59f5 | ||
|
|
f806838c37 | ||
|
|
074a982853 | ||
|
|
109f29cc24 | ||
|
|
587e4a21b3 | ||
|
|
3f1f58b2ab | ||
|
|
01eb70caff | ||
|
|
d784851337 | ||
|
|
1c4e5aa5c0 | ||
|
|
94df096fb9 | ||
|
|
820bd7dd01 | ||
|
|
42cb7bda19 | ||
|
|
2fb9940b8a | ||
|
|
2ff0ad4190 | ||
|
|
bd12103ed4 | ||
|
|
2e17edd72a | ||
|
|
24aab68b3f | ||
|
|
5bdbb10593 |
@@ -10,7 +10,8 @@ services:
|
|||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- localai_workspace:/workspace
|
- localai_workspace:/workspace
|
||||||
- ../models:/host-models
|
- models:/host-models
|
||||||
|
- backends:/host-backends
|
||||||
- ./customization:/devcontainer-customization
|
- ./customization:/devcontainer-customization
|
||||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||||
cap_add:
|
cap_add:
|
||||||
@@ -39,6 +40,9 @@ services:
|
|||||||
- GF_SECURITY_ADMIN_PASSWORD=grafana
|
- GF_SECURITY_ADMIN_PASSWORD=grafana
|
||||||
volumes:
|
volumes:
|
||||||
- ./grafana:/etc/grafana/provisioning/datasources
|
- ./grafana:/etc/grafana/provisioning/datasources
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
prom_data:
|
prom_data:
|
||||||
localai_workspace:
|
localai_workspace:
|
||||||
|
models:
|
||||||
|
backends:
|
||||||
|
|||||||
4
.github/gallery-agent/agent.go
vendored
4
.github/gallery-agent/agent.go
vendored
@@ -141,12 +141,12 @@ func getRealReadme(ctx context.Context, repository string) (string, error) {
|
|||||||
result = result.AddMessage("user", "Describe the model in a clear and concise way that can be shared in a model gallery.")
|
result = result.AddMessage("user", "Describe the model in a clear and concise way that can be shared in a model gallery.")
|
||||||
|
|
||||||
// Get a response
|
// Get a response
|
||||||
newFragment, err := llm.Ask(ctx, result)
|
_, err = llm.Ask(ctx, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
content := newFragment.LastMessage().Content
|
content := result.LastMessage().Content
|
||||||
return cleanTextContent(content), nil
|
return cleanTextContent(content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
.github/gallery-agent/tools.go
vendored
8
.github/gallery-agent/tools.go
vendored
@@ -13,16 +13,16 @@ type HFReadmeTool struct {
|
|||||||
client *hfapi.Client
|
client *hfapi.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HFReadmeTool) Execute(args map[string]any) (string, error) {
|
func (s *HFReadmeTool) Execute(args map[string]any) (string, any, error) {
|
||||||
q, ok := args["repository"].(string)
|
q, ok := args["repository"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("no query")
|
return "", nil, fmt.Errorf("no query")
|
||||||
}
|
}
|
||||||
readme, err := s.client.GetReadmeContent(q, "README.md")
|
readme, err := s.client.GetReadmeContent(q, "README.md")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
return readme, nil
|
return readme, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HFReadmeTool) Tool() openai.Tool {
|
func (s *HFReadmeTool) Tool() openai.Tool {
|
||||||
|
|||||||
67
.github/workflows/backend.yml
vendored
67
.github/workflows/backend.yml
vendored
@@ -210,6 +210,19 @@ jobs:
|
|||||||
dockerfile: "./backend/Dockerfile.python"
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
context: "./"
|
context: "./"
|
||||||
ubuntu-version: '2404'
|
ubuntu-version: '2404'
|
||||||
|
- build-type: 'cublas'
|
||||||
|
cuda-major-version: "12"
|
||||||
|
cuda-minor-version: "8"
|
||||||
|
platforms: 'linux/amd64'
|
||||||
|
tag-latest: 'auto'
|
||||||
|
tag-suffix: '-gpu-nvidia-cuda-12-faster-qwen3-tts'
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
base-image: "ubuntu:24.04"
|
||||||
|
skip-drivers: 'false'
|
||||||
|
backend: "faster-qwen3-tts"
|
||||||
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
|
context: "./"
|
||||||
|
ubuntu-version: '2404'
|
||||||
- build-type: 'cublas'
|
- build-type: 'cublas'
|
||||||
cuda-major-version: "12"
|
cuda-major-version: "12"
|
||||||
cuda-minor-version: "8"
|
cuda-minor-version: "8"
|
||||||
@@ -575,6 +588,19 @@ jobs:
|
|||||||
dockerfile: "./backend/Dockerfile.python"
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
context: "./"
|
context: "./"
|
||||||
ubuntu-version: '2404'
|
ubuntu-version: '2404'
|
||||||
|
- build-type: 'cublas'
|
||||||
|
cuda-major-version: "13"
|
||||||
|
cuda-minor-version: "0"
|
||||||
|
platforms: 'linux/amd64'
|
||||||
|
tag-latest: 'auto'
|
||||||
|
tag-suffix: '-gpu-nvidia-cuda-13-faster-qwen3-tts'
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
base-image: "ubuntu:24.04"
|
||||||
|
skip-drivers: 'false'
|
||||||
|
backend: "faster-qwen3-tts"
|
||||||
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
|
context: "./"
|
||||||
|
ubuntu-version: '2404'
|
||||||
- build-type: 'cublas'
|
- build-type: 'cublas'
|
||||||
cuda-major-version: "13"
|
cuda-major-version: "13"
|
||||||
cuda-minor-version: "0"
|
cuda-minor-version: "0"
|
||||||
@@ -705,6 +731,19 @@ jobs:
|
|||||||
backend: "qwen-tts"
|
backend: "qwen-tts"
|
||||||
dockerfile: "./backend/Dockerfile.python"
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
context: "./"
|
context: "./"
|
||||||
|
- build-type: 'l4t'
|
||||||
|
cuda-major-version: "13"
|
||||||
|
cuda-minor-version: "0"
|
||||||
|
platforms: 'linux/arm64'
|
||||||
|
tag-latest: 'auto'
|
||||||
|
tag-suffix: '-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts'
|
||||||
|
runs-on: 'ubuntu-24.04-arm'
|
||||||
|
base-image: "ubuntu:24.04"
|
||||||
|
skip-drivers: 'false'
|
||||||
|
ubuntu-version: '2404'
|
||||||
|
backend: "faster-qwen3-tts"
|
||||||
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
|
context: "./"
|
||||||
- build-type: 'l4t'
|
- build-type: 'l4t'
|
||||||
cuda-major-version: "13"
|
cuda-major-version: "13"
|
||||||
cuda-minor-version: "0"
|
cuda-minor-version: "0"
|
||||||
@@ -718,6 +757,19 @@ jobs:
|
|||||||
backend: "pocket-tts"
|
backend: "pocket-tts"
|
||||||
dockerfile: "./backend/Dockerfile.python"
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
context: "./"
|
context: "./"
|
||||||
|
- build-type: 'l4t'
|
||||||
|
cuda-major-version: "13"
|
||||||
|
cuda-minor-version: "0"
|
||||||
|
platforms: 'linux/arm64'
|
||||||
|
tag-latest: 'auto'
|
||||||
|
tag-suffix: '-nvidia-l4t-cuda-13-arm64-chatterbox'
|
||||||
|
runs-on: 'ubuntu-24.04-arm'
|
||||||
|
base-image: "ubuntu:24.04"
|
||||||
|
skip-drivers: 'false'
|
||||||
|
ubuntu-version: '2404'
|
||||||
|
backend: "chatterbox"
|
||||||
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
|
context: "./"
|
||||||
- build-type: 'l4t'
|
- build-type: 'l4t'
|
||||||
cuda-major-version: "13"
|
cuda-major-version: "13"
|
||||||
cuda-minor-version: "0"
|
cuda-minor-version: "0"
|
||||||
@@ -1293,6 +1345,19 @@ jobs:
|
|||||||
dockerfile: "./backend/Dockerfile.python"
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
context: "./"
|
context: "./"
|
||||||
ubuntu-version: '2204'
|
ubuntu-version: '2204'
|
||||||
|
- build-type: 'l4t'
|
||||||
|
cuda-major-version: "12"
|
||||||
|
cuda-minor-version: "0"
|
||||||
|
platforms: 'linux/arm64'
|
||||||
|
tag-latest: 'auto'
|
||||||
|
tag-suffix: '-nvidia-l4t-faster-qwen3-tts'
|
||||||
|
runs-on: 'ubuntu-24.04-arm'
|
||||||
|
base-image: "nvcr.io/nvidia/l4t-jetpack:r36.4.0"
|
||||||
|
skip-drivers: 'true'
|
||||||
|
backend: "faster-qwen3-tts"
|
||||||
|
dockerfile: "./backend/Dockerfile.python"
|
||||||
|
context: "./"
|
||||||
|
ubuntu-version: '2204'
|
||||||
- build-type: 'l4t'
|
- build-type: 'l4t'
|
||||||
cuda-major-version: "12"
|
cuda-major-version: "12"
|
||||||
cuda-minor-version: "0"
|
cuda-minor-version: "0"
|
||||||
@@ -1892,7 +1957,7 @@ jobs:
|
|||||||
- build-type: ''
|
- build-type: ''
|
||||||
cuda-major-version: ""
|
cuda-major-version: ""
|
||||||
cuda-minor-version: ""
|
cuda-minor-version: ""
|
||||||
platforms: 'linux/amd64'
|
platforms: 'linux/amd64,linux/arm64'
|
||||||
tag-latest: 'auto'
|
tag-latest: 'auto'
|
||||||
tag-suffix: '-cpu-voxcpm'
|
tag-suffix: '-cpu-voxcpm'
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
|
|||||||
4
.github/workflows/bump_deps.yaml
vendored
4
.github/workflows/bump_deps.yaml
vendored
@@ -18,10 +18,6 @@ jobs:
|
|||||||
variable: "WHISPER_CPP_VERSION"
|
variable: "WHISPER_CPP_VERSION"
|
||||||
branch: "master"
|
branch: "master"
|
||||||
file: "backend/go/whisper/Makefile"
|
file: "backend/go/whisper/Makefile"
|
||||||
- repository: "PABannier/bark.cpp"
|
|
||||||
variable: "BARKCPP_VERSION"
|
|
||||||
branch: "main"
|
|
||||||
file: "Makefile"
|
|
||||||
- repository: "leejet/stable-diffusion.cpp"
|
- repository: "leejet/stable-diffusion.cpp"
|
||||||
variable: "STABLEDIFFUSION_GGML_VERSION"
|
variable: "STABLEDIFFUSION_GGML_VERSION"
|
||||||
branch: "master"
|
branch: "master"
|
||||||
|
|||||||
2
.github/workflows/localaibot_automerge.yml
vendored
2
.github/workflows/localaibot_automerge.yml
vendored
@@ -10,7 +10,7 @@ permissions:
|
|||||||
actions: write # to dispatch publish workflow
|
actions: write # to dispatch publish workflow
|
||||||
jobs:
|
jobs:
|
||||||
dependabot:
|
dependabot:
|
||||||
if: github.repository == 'mudler/LocalAI' && github.actor == 'localai-bot' && !contains(github.event.pull_request.title, 'chore(model gallery):')
|
if: github.repository == 'mudler/LocalAI' && github.actor == 'localai-bot' && contains(github.event.pull_request.title, 'chore:')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: 1.23
|
go-version: 1.23
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
version: v2.11.0
|
version: v2.11.0
|
||||||
args: release --clean
|
args: release --clean
|
||||||
|
|||||||
2
.github/workflows/stalebot.yml
vendored
2
.github/workflows/stalebot.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
if: github.repository == 'mudler/LocalAI'
|
if: github.repository == 'mudler/LocalAI'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v9
|
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v9
|
||||||
with:
|
with:
|
||||||
stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||||
stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
|
stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 10 days.'
|
||||||
|
|||||||
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
@@ -93,30 +93,15 @@ jobs:
|
|||||||
- name: Dependencies
|
- name: Dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install build-essential ccache upx-ucl curl ffmpeg
|
sudo apt-get install curl ffmpeg
|
||||||
sudo apt-get install -y libgmock-dev clang
|
- name: Build backends
|
||||||
# Install UV
|
run: |
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
make backends/transformers
|
||||||
sudo apt-get install -y ca-certificates cmake patch python3-pip unzip
|
mv backends/transformer external/transformers
|
||||||
sudo apt-get install -y libopencv-dev
|
|
||||||
|
|
||||||
curl -L -s https://github.com/protocolbuffers/protobuf/releases/download/v26.1/protoc-26.1-linux-x86_64.zip -o protoc.zip && \
|
|
||||||
unzip -j -d /usr/local/bin protoc.zip bin/protoc && \
|
|
||||||
rm protoc.zip
|
|
||||||
|
|
||||||
curl -O https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
|
|
||||||
sudo dpkg -i cuda-keyring_1.1-1_all.deb
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y cuda-nvcc-${CUDA_VERSION} libcublas-dev-${CUDA_VERSION}
|
|
||||||
export CUDACXX=/usr/local/cuda/bin/nvcc
|
|
||||||
make -C backend/python/transformers
|
|
||||||
|
|
||||||
make backends/huggingface backends/llama-cpp backends/local-store backends/silero-vad backends/piper backends/whisper backends/stablediffusion-ggml
|
make backends/huggingface backends/llama-cpp backends/local-store backends/silero-vad backends/piper backends/whisper backends/stablediffusion-ggml
|
||||||
env:
|
|
||||||
CUDA_VERSION: 12-4
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
PATH="$PATH:/root/go/bin" GO_TAGS="tts" make --jobs 5 --output-sync=target test
|
TRANSFORMER_BACKEND=$(abspath ./)/external/transformers/run.sh PATH="$PATH:/root/go/bin" GO_TAGS="tts" make --jobs 5 --output-sync=target test
|
||||||
- name: Setup tmate session if tests fail
|
- name: Setup tmate session if tests fail
|
||||||
if: ${{ failure() }}
|
if: ${{ failure() }}
|
||||||
uses: mxschmitt/action-tmate@v3.23
|
uses: mxschmitt/action-tmate@v3.23
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -1,5 +1,5 @@
|
|||||||
# Disable parallel execution for backend builds
|
# Disable parallel execution for backend builds
|
||||||
.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/outetts backends/piper backends/stablediffusion-ggml backends/whisper backends/faster-whisper backends/silero-vad backends/local-store backends/huggingface backends/rfdetr backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/moonshine backends/pocket-tts backends/qwen-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/voxtral
|
.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/outetts backends/piper backends/stablediffusion-ggml backends/whisper backends/faster-whisper backends/silero-vad backends/local-store backends/huggingface backends/rfdetr backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/moonshine backends/pocket-tts backends/qwen-tts backends/faster-qwen3-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/voxtral
|
||||||
|
|
||||||
GOCMD=go
|
GOCMD=go
|
||||||
GOTEST=$(GOCMD) test
|
GOTEST=$(GOCMD) test
|
||||||
@@ -149,7 +149,7 @@ test: test-models/testmodel.ggml protogen-go
|
|||||||
@echo 'Running tests'
|
@echo 'Running tests'
|
||||||
export GO_TAGS="debug"
|
export GO_TAGS="debug"
|
||||||
$(MAKE) prepare-test
|
$(MAKE) prepare-test
|
||||||
HUGGINGFACE_GRPC=$(abspath ./)/backend/python/transformers/run.sh TEST_DIR=$(abspath ./)/test-dir/ FIXTURES=$(abspath ./)/tests/fixtures CONFIG_FILE=$(abspath ./)/test-models/config.yaml MODELS_PATH=$(abspath ./)/test-models BACKENDS_PATH=$(abspath ./)/backends \
|
TEST_DIR=$(abspath ./)/test-dir/ FIXTURES=$(abspath ./)/tests/fixtures CONFIG_FILE=$(abspath ./)/test-models/config.yaml MODELS_PATH=$(abspath ./)/test-models BACKENDS_PATH=$(abspath ./)/backends \
|
||||||
$(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!llama-gguf" --flake-attempts $(TEST_FLAKES) --fail-fast -v -r $(TEST_PATHS)
|
$(GOCMD) run github.com/onsi/ginkgo/v2/ginkgo --label-filter="!llama-gguf" --flake-attempts $(TEST_FLAKES) --fail-fast -v -r $(TEST_PATHS)
|
||||||
$(MAKE) test-llama-gguf
|
$(MAKE) test-llama-gguf
|
||||||
$(MAKE) test-tts
|
$(MAKE) test-tts
|
||||||
@@ -317,6 +317,7 @@ prepare-test-extra: protogen-python
|
|||||||
$(MAKE) -C backend/python/moonshine
|
$(MAKE) -C backend/python/moonshine
|
||||||
$(MAKE) -C backend/python/pocket-tts
|
$(MAKE) -C backend/python/pocket-tts
|
||||||
$(MAKE) -C backend/python/qwen-tts
|
$(MAKE) -C backend/python/qwen-tts
|
||||||
|
$(MAKE) -C backend/python/faster-qwen3-tts
|
||||||
$(MAKE) -C backend/python/qwen-asr
|
$(MAKE) -C backend/python/qwen-asr
|
||||||
$(MAKE) -C backend/python/nemo
|
$(MAKE) -C backend/python/nemo
|
||||||
$(MAKE) -C backend/python/voxcpm
|
$(MAKE) -C backend/python/voxcpm
|
||||||
@@ -334,6 +335,7 @@ test-extra: prepare-test-extra
|
|||||||
$(MAKE) -C backend/python/moonshine test
|
$(MAKE) -C backend/python/moonshine test
|
||||||
$(MAKE) -C backend/python/pocket-tts test
|
$(MAKE) -C backend/python/pocket-tts test
|
||||||
$(MAKE) -C backend/python/qwen-tts test
|
$(MAKE) -C backend/python/qwen-tts test
|
||||||
|
$(MAKE) -C backend/python/faster-qwen3-tts test
|
||||||
$(MAKE) -C backend/python/qwen-asr test
|
$(MAKE) -C backend/python/qwen-asr test
|
||||||
$(MAKE) -C backend/python/nemo test
|
$(MAKE) -C backend/python/nemo test
|
||||||
$(MAKE) -C backend/python/voxcpm test
|
$(MAKE) -C backend/python/voxcpm test
|
||||||
@@ -473,6 +475,7 @@ BACKEND_VIBEVOICE = vibevoice|python|.|--progress=plain|true
|
|||||||
BACKEND_MOONSHINE = moonshine|python|.|false|true
|
BACKEND_MOONSHINE = moonshine|python|.|false|true
|
||||||
BACKEND_POCKET_TTS = pocket-tts|python|.|false|true
|
BACKEND_POCKET_TTS = pocket-tts|python|.|false|true
|
||||||
BACKEND_QWEN_TTS = qwen-tts|python|.|false|true
|
BACKEND_QWEN_TTS = qwen-tts|python|.|false|true
|
||||||
|
BACKEND_FASTER_QWEN3_TTS = faster-qwen3-tts|python|.|false|true
|
||||||
BACKEND_QWEN_ASR = qwen-asr|python|.|false|true
|
BACKEND_QWEN_ASR = qwen-asr|python|.|false|true
|
||||||
BACKEND_NEMO = nemo|python|.|false|true
|
BACKEND_NEMO = nemo|python|.|false|true
|
||||||
BACKEND_VOXCPM = voxcpm|python|.|false|true
|
BACKEND_VOXCPM = voxcpm|python|.|false|true
|
||||||
@@ -525,6 +528,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_VIBEVOICE)))
|
|||||||
$(eval $(call generate-docker-build-target,$(BACKEND_MOONSHINE)))
|
$(eval $(call generate-docker-build-target,$(BACKEND_MOONSHINE)))
|
||||||
$(eval $(call generate-docker-build-target,$(BACKEND_POCKET_TTS)))
|
$(eval $(call generate-docker-build-target,$(BACKEND_POCKET_TTS)))
|
||||||
$(eval $(call generate-docker-build-target,$(BACKEND_QWEN_TTS)))
|
$(eval $(call generate-docker-build-target,$(BACKEND_QWEN_TTS)))
|
||||||
|
$(eval $(call generate-docker-build-target,$(BACKEND_FASTER_QWEN3_TTS)))
|
||||||
$(eval $(call generate-docker-build-target,$(BACKEND_QWEN_ASR)))
|
$(eval $(call generate-docker-build-target,$(BACKEND_QWEN_ASR)))
|
||||||
$(eval $(call generate-docker-build-target,$(BACKEND_NEMO)))
|
$(eval $(call generate-docker-build-target,$(BACKEND_NEMO)))
|
||||||
$(eval $(call generate-docker-build-target,$(BACKEND_VOXCPM)))
|
$(eval $(call generate-docker-build-target,$(BACKEND_VOXCPM)))
|
||||||
@@ -535,7 +539,7 @@ $(eval $(call generate-docker-build-target,$(BACKEND_ACE_STEP)))
|
|||||||
docker-save-%: backend-images
|
docker-save-%: backend-images
|
||||||
docker save local-ai-backend:$* -o backend-images/$*.tar
|
docker save local-ai-backend:$* -o backend-images/$*.tar
|
||||||
|
|
||||||
docker-build-backends: docker-build-llama-cpp docker-build-rerankers docker-build-vllm docker-build-vllm-omni docker-build-transformers docker-build-outetts docker-build-diffusers docker-build-kokoro docker-build-faster-whisper docker-build-coqui docker-build-chatterbox docker-build-vibevoice docker-build-moonshine docker-build-pocket-tts docker-build-qwen-tts docker-build-qwen-asr docker-build-nemo docker-build-voxcpm docker-build-whisperx docker-build-ace-step docker-build-voxtral
|
docker-build-backends: docker-build-llama-cpp docker-build-rerankers docker-build-vllm docker-build-vllm-omni docker-build-transformers docker-build-outetts docker-build-diffusers docker-build-kokoro docker-build-faster-whisper docker-build-coqui docker-build-chatterbox docker-build-vibevoice docker-build-moonshine docker-build-pocket-tts docker-build-qwen-tts docker-build-faster-qwen3-tts docker-build-qwen-asr docker-build-nemo docker-build-voxcpm docker-build-whisperx docker-build-ace-step docker-build-voxtral
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
### Mock Backend for E2E Tests
|
### Mock Backend for E2E Tests
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -93,16 +93,7 @@ Liking LocalAI? LocalAI is part of an integrated suite of AI infrastructure tool
|
|||||||
|
|
||||||
## 💻 Quickstart
|
## 💻 Quickstart
|
||||||
|
|
||||||
> ⚠️ **Note:** The `install.sh` script is currently experiencing issues due to the heavy changes currently undergoing in LocalAI and may produce broken or misconfigured installations. Please use Docker installation (see below) or manual binary installation until [issue #8032](https://github.com/mudler/LocalAI/issues/8032) is resolved.
|
|
||||||
|
|
||||||
Run the installer script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic installation
|
|
||||||
curl https://localai.io/install.sh | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
For more installation options, see [Installer Options](https://localai.io/installation/).
|
|
||||||
|
|
||||||
### macOS Download:
|
### macOS Download:
|
||||||
|
|
||||||
@@ -237,7 +228,7 @@ Roadmap items: [List of issues](https://github.com/mudler/LocalAI/issues?q=is%3A
|
|||||||
- 🧩 [Backend Gallery](https://localai.io/backends/): Install/remove backends on the fly, powered by OCI images — fully customizable and API-driven.
|
- 🧩 [Backend Gallery](https://localai.io/backends/): Install/remove backends on the fly, powered by OCI images — fully customizable and API-driven.
|
||||||
- 📖 [Text generation with GPTs](https://localai.io/features/text-generation/) (`llama.cpp`, `transformers`, `vllm` ... [:book: and more](https://localai.io/model-compatibility/index.html#model-compatibility-table))
|
- 📖 [Text generation with GPTs](https://localai.io/features/text-generation/) (`llama.cpp`, `transformers`, `vllm` ... [:book: and more](https://localai.io/model-compatibility/index.html#model-compatibility-table))
|
||||||
- 🗣 [Text to Audio](https://localai.io/features/text-to-audio/)
|
- 🗣 [Text to Audio](https://localai.io/features/text-to-audio/)
|
||||||
- 🔈 [Audio to Text](https://localai.io/features/audio-to-text/) (Audio transcription with `whisper.cpp`)
|
- 🔈 [Audio to Text](https://localai.io/features/audio-to-text/)
|
||||||
- 🎨 [Image generation](https://localai.io/features/image-generation)
|
- 🎨 [Image generation](https://localai.io/features/image-generation)
|
||||||
- 🔥 [OpenAI-alike tools API](https://localai.io/features/openai-functions/)
|
- 🔥 [OpenAI-alike tools API](https://localai.io/features/openai-functions/)
|
||||||
- ⚡ [Realtime API](https://localai.io/features/openai-realtime/) (Speech-to-speech)
|
- ⚡ [Realtime API](https://localai.io/features/openai-realtime/) (Speech-to-speech)
|
||||||
@@ -343,7 +334,7 @@ Other:
|
|||||||
- Langchain: https://python.langchain.com/docs/integrations/providers/localai/
|
- Langchain: https://python.langchain.com/docs/integrations/providers/localai/
|
||||||
- Terminal utility https://github.com/djcopley/ShellOracle
|
- Terminal utility https://github.com/djcopley/ShellOracle
|
||||||
- Local Smart assistant https://github.com/mudler/LocalAGI
|
- Local Smart assistant https://github.com/mudler/LocalAGI
|
||||||
- Home Assistant https://github.com/sammcj/homeassistant-localai / https://github.com/drndos/hass-openai-custom-conversation / https://github.com/valentinfrlch/ha-gpt4vision
|
- Home Assistant https://github.com/drndos/hass-openai-custom-conversation / https://github.com/valentinfrlch/ha-llmvision / https://github.com/loryanstrant/HA-LocalAI-Monitor
|
||||||
- Discord bot https://github.com/mudler/LocalAGI/tree/main/examples/discord
|
- Discord bot https://github.com/mudler/LocalAGI/tree/main/examples/discord
|
||||||
- Slack bot https://github.com/mudler/LocalAGI/tree/main/examples/slack
|
- Slack bot https://github.com/mudler/LocalAGI/tree/main/examples/slack
|
||||||
- Shell-Pilot(Interact with LLM using LocalAI models via pure shell scripts on your Linux or MacOS system) https://github.com/reid41/shell-pilot
|
- Shell-Pilot(Interact with LLM using LocalAI models via pure shell scripts on your Linux or MacOS system) https://github.com/reid41/shell-pilot
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
LLAMA_VERSION?=338085c69e486b7155e5b03d7b5087e02c0e2528
|
LLAMA_VERSION?=05728db18eea59de81ee3a7699739daaf015206b
|
||||||
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
|
LLAMA_REPO?=https://github.com/ggerganov/llama.cpp
|
||||||
|
|
||||||
CMAKE_ARGS?=
|
CMAKE_ARGS?=
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
|
|||||||
params.mmproj.path = request->mmproj();
|
params.mmproj.path = request->mmproj();
|
||||||
}
|
}
|
||||||
// params.model_alias ??
|
// params.model_alias ??
|
||||||
params.model_alias = request->modelfile();
|
params.model_alias.insert(request->modelfile());
|
||||||
if (!request->cachetypekey().empty()) {
|
if (!request->cachetypekey().empty()) {
|
||||||
params.cache_type_k = kv_cache_type_from_str(request->cachetypekey());
|
params.cache_type_k = kv_cache_type_from_str(request->cachetypekey());
|
||||||
}
|
}
|
||||||
@@ -417,6 +417,12 @@ static void params_parse(server_context& /*ctx_server*/, const backend::ModelOpt
|
|||||||
// n_ctx_checkpoints: max context checkpoints per slot (default: 8)
|
// n_ctx_checkpoints: max context checkpoints per slot (default: 8)
|
||||||
params.n_ctx_checkpoints = 8;
|
params.n_ctx_checkpoints = 8;
|
||||||
|
|
||||||
|
// llama memory fit fails if we don't provide a buffer for tensor overrides
|
||||||
|
const size_t ntbo = llama_max_tensor_buft_overrides();
|
||||||
|
while (params.tensor_buft_overrides.size() < ntbo) {
|
||||||
|
params.tensor_buft_overrides.push_back({nullptr, nullptr});
|
||||||
|
}
|
||||||
|
|
||||||
// decode options. Options are in form optname:optvale, or if booleans only optname.
|
// decode options. Options are in form optname:optvale, or if booleans only optname.
|
||||||
for (int i = 0; i < request->options_size(); i++) {
|
for (int i = 0; i < request->options_size(); i++) {
|
||||||
std::string opt = request->options(i);
|
std::string opt = request->options(i);
|
||||||
@@ -1255,6 +1261,42 @@ public:
|
|||||||
body_json["add_generation_prompt"] = data["add_generation_prompt"];
|
body_json["add_generation_prompt"] = data["add_generation_prompt"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass sampling parameters to body_json so oaicompat_chat_params_parse respects them
|
||||||
|
// and doesn't overwrite them with defaults in the returned parsed_data
|
||||||
|
if (data.contains("n_predict")) {
|
||||||
|
body_json["max_tokens"] = data["n_predict"];
|
||||||
|
}
|
||||||
|
if (data.contains("ignore_eos")) {
|
||||||
|
body_json["ignore_eos"] = data["ignore_eos"];
|
||||||
|
}
|
||||||
|
if (data.contains("stop")) {
|
||||||
|
body_json["stop"] = data["stop"];
|
||||||
|
}
|
||||||
|
if (data.contains("temperature")) {
|
||||||
|
body_json["temperature"] = data["temperature"];
|
||||||
|
}
|
||||||
|
if (data.contains("top_p")) {
|
||||||
|
body_json["top_p"] = data["top_p"];
|
||||||
|
}
|
||||||
|
if (data.contains("frequency_penalty")) {
|
||||||
|
body_json["frequency_penalty"] = data["frequency_penalty"];
|
||||||
|
}
|
||||||
|
if (data.contains("presence_penalty")) {
|
||||||
|
body_json["presence_penalty"] = data["presence_penalty"];
|
||||||
|
}
|
||||||
|
if (data.contains("seed")) {
|
||||||
|
body_json["seed"] = data["seed"];
|
||||||
|
}
|
||||||
|
if (data.contains("logit_bias")) {
|
||||||
|
body_json["logit_bias"] = data["logit_bias"];
|
||||||
|
}
|
||||||
|
if (data.contains("top_k")) {
|
||||||
|
body_json["top_k"] = data["top_k"];
|
||||||
|
}
|
||||||
|
if (data.contains("min_p")) {
|
||||||
|
body_json["min_p"] = data["min_p"];
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: Print full body_json before template processing (includes messages, tools, tool_choice, etc.)
|
// Debug: Print full body_json before template processing (includes messages, tools, tool_choice, etc.)
|
||||||
SRV_DBG("[CONVERSATION DEBUG] PredictStream: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str());
|
SRV_DBG("[CONVERSATION DEBUG] PredictStream: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str());
|
||||||
|
|
||||||
@@ -1986,6 +2028,42 @@ public:
|
|||||||
body_json["add_generation_prompt"] = data["add_generation_prompt"];
|
body_json["add_generation_prompt"] = data["add_generation_prompt"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass sampling parameters to body_json so oaicompat_chat_params_parse respects them
|
||||||
|
// and doesn't overwrite them with defaults in the returned parsed_data
|
||||||
|
if (data.contains("n_predict")) {
|
||||||
|
body_json["max_tokens"] = data["n_predict"];
|
||||||
|
}
|
||||||
|
if (data.contains("ignore_eos")) {
|
||||||
|
body_json["ignore_eos"] = data["ignore_eos"];
|
||||||
|
}
|
||||||
|
if (data.contains("stop")) {
|
||||||
|
body_json["stop"] = data["stop"];
|
||||||
|
}
|
||||||
|
if (data.contains("temperature")) {
|
||||||
|
body_json["temperature"] = data["temperature"];
|
||||||
|
}
|
||||||
|
if (data.contains("top_p")) {
|
||||||
|
body_json["top_p"] = data["top_p"];
|
||||||
|
}
|
||||||
|
if (data.contains("frequency_penalty")) {
|
||||||
|
body_json["frequency_penalty"] = data["frequency_penalty"];
|
||||||
|
}
|
||||||
|
if (data.contains("presence_penalty")) {
|
||||||
|
body_json["presence_penalty"] = data["presence_penalty"];
|
||||||
|
}
|
||||||
|
if (data.contains("seed")) {
|
||||||
|
body_json["seed"] = data["seed"];
|
||||||
|
}
|
||||||
|
if (data.contains("logit_bias")) {
|
||||||
|
body_json["logit_bias"] = data["logit_bias"];
|
||||||
|
}
|
||||||
|
if (data.contains("top_k")) {
|
||||||
|
body_json["top_k"] = data["top_k"];
|
||||||
|
}
|
||||||
|
if (data.contains("min_p")) {
|
||||||
|
body_json["min_p"] = data["min_p"];
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: Print full body_json before template processing (includes messages, tools, tool_choice, etc.)
|
// Debug: Print full body_json before template processing (includes messages, tools, tool_choice, etc.)
|
||||||
SRV_DBG("[CONVERSATION DEBUG] Predict: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str());
|
SRV_DBG("[CONVERSATION DEBUG] Predict: Full body_json before oaicompat_chat_params_parse:\n%s\n", body_json.dump(2).c_str());
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ JOBS?=$(shell nproc --ignore=1 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || e
|
|||||||
|
|
||||||
# voxtral.c version
|
# voxtral.c version
|
||||||
VOXTRAL_REPO?=https://github.com/antirez/voxtral.c
|
VOXTRAL_REPO?=https://github.com/antirez/voxtral.c
|
||||||
VOXTRAL_VERSION?=c9e8773a2042d67c637fc492c8a655c485354080
|
VOXTRAL_VERSION?=134d366c24d20c64b614a3dcc8bda2a6922d077d
|
||||||
|
|
||||||
# Detect OS
|
# Detect OS
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ JOBS?=$(shell nproc --ignore=1)
|
|||||||
|
|
||||||
# whisper.cpp version
|
# whisper.cpp version
|
||||||
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
|
WHISPER_REPO?=https://github.com/ggml-org/whisper.cpp
|
||||||
WHISPER_CPP_VERSION?=764482c3175d9c3bc6089c1ec84df7d1b9537d83
|
WHISPER_CPP_VERSION?=9453b4b9be9b73adfc35051083f37cefa039acee
|
||||||
SO_TARGET?=libgowhisper.so
|
SO_TARGET?=libgowhisper.so
|
||||||
|
|
||||||
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
CMAKE_ARGS+=-DBUILD_SHARED_LIBS=OFF
|
||||||
|
|||||||
@@ -528,6 +528,28 @@
|
|||||||
nvidia-l4t-cuda-12: "nvidia-l4t-qwen-tts"
|
nvidia-l4t-cuda-12: "nvidia-l4t-qwen-tts"
|
||||||
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-qwen-tts"
|
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-qwen-tts"
|
||||||
icon: https://cdn-avatars.huggingface.co/v1/production/uploads/620760a26e3b7210c2ff1943/-s1gyJfvbE1RgO5iBeNOi.png
|
icon: https://cdn-avatars.huggingface.co/v1/production/uploads/620760a26e3b7210c2ff1943/-s1gyJfvbE1RgO5iBeNOi.png
|
||||||
|
- &faster-qwen3-tts
|
||||||
|
urls:
|
||||||
|
- https://github.com/andimarafioti/faster-qwen3-tts
|
||||||
|
- https://pypi.org/project/faster-qwen3-tts/
|
||||||
|
description: |
|
||||||
|
Real-time Qwen3-TTS inference using CUDA graph capture. Voice clone only; requires NVIDIA GPU with CUDA.
|
||||||
|
tags:
|
||||||
|
- text-to-speech
|
||||||
|
- TTS
|
||||||
|
- voice-clone
|
||||||
|
license: apache-2.0
|
||||||
|
name: "faster-qwen3-tts"
|
||||||
|
alias: "faster-qwen3-tts"
|
||||||
|
capabilities:
|
||||||
|
nvidia: "cuda12-faster-qwen3-tts"
|
||||||
|
default: "cuda12-faster-qwen3-tts"
|
||||||
|
nvidia-cuda-13: "cuda13-faster-qwen3-tts"
|
||||||
|
nvidia-cuda-12: "cuda12-faster-qwen3-tts"
|
||||||
|
nvidia-l4t: "nvidia-l4t-faster-qwen3-tts"
|
||||||
|
nvidia-l4t-cuda-12: "nvidia-l4t-faster-qwen3-tts"
|
||||||
|
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts"
|
||||||
|
icon: https://cdn-avatars.huggingface.co/v1/production/uploads/620760a26e3b7210c2ff1943/-s1gyJfvbE1RgO5iBeNOi.png
|
||||||
- &qwen-asr
|
- &qwen-asr
|
||||||
urls:
|
urls:
|
||||||
- https://github.com/QwenLM/Qwen3-ASR
|
- https://github.com/QwenLM/Qwen3-ASR
|
||||||
@@ -2030,7 +2052,7 @@
|
|||||||
nvidia-cuda-13: "cuda13-chatterbox-development"
|
nvidia-cuda-13: "cuda13-chatterbox-development"
|
||||||
nvidia-cuda-12: "cuda12-chatterbox-development"
|
nvidia-cuda-12: "cuda12-chatterbox-development"
|
||||||
nvidia-l4t-cuda-12: "nvidia-l4t-arm64-chatterbox"
|
nvidia-l4t-cuda-12: "nvidia-l4t-arm64-chatterbox"
|
||||||
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-chatterbox"
|
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-chatterbox-development"
|
||||||
- !!merge <<: *chatterbox
|
- !!merge <<: *chatterbox
|
||||||
name: "cpu-chatterbox"
|
name: "cpu-chatterbox"
|
||||||
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-chatterbox"
|
uri: "quay.io/go-skynet/local-ai-backends:latest-cpu-chatterbox"
|
||||||
@@ -2279,6 +2301,57 @@
|
|||||||
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-qwen-tts"
|
uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-qwen-tts"
|
||||||
mirrors:
|
mirrors:
|
||||||
- localai/localai-backends:master-metal-darwin-arm64-qwen-tts
|
- localai/localai-backends:master-metal-darwin-arm64-qwen-tts
|
||||||
|
## faster-qwen3-tts
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "faster-qwen3-tts-development"
|
||||||
|
capabilities:
|
||||||
|
nvidia: "cuda12-faster-qwen3-tts-development"
|
||||||
|
default: "cuda12-faster-qwen3-tts-development"
|
||||||
|
nvidia-cuda-13: "cuda13-faster-qwen3-tts-development"
|
||||||
|
nvidia-cuda-12: "cuda12-faster-qwen3-tts-development"
|
||||||
|
nvidia-l4t: "nvidia-l4t-faster-qwen3-tts-development"
|
||||||
|
nvidia-l4t-cuda-12: "nvidia-l4t-faster-qwen3-tts-development"
|
||||||
|
nvidia-l4t-cuda-13: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts-development"
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "cuda12-faster-qwen3-tts"
|
||||||
|
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-12-faster-qwen3-tts"
|
||||||
|
mirrors:
|
||||||
|
- localai/localai-backends:latest-gpu-nvidia-cuda-12-faster-qwen3-tts
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "cuda12-faster-qwen3-tts-development"
|
||||||
|
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-12-faster-qwen3-tts"
|
||||||
|
mirrors:
|
||||||
|
- localai/localai-backends:master-gpu-nvidia-cuda-12-faster-qwen3-tts
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "cuda13-faster-qwen3-tts"
|
||||||
|
uri: "quay.io/go-skynet/local-ai-backends:latest-gpu-nvidia-cuda-13-faster-qwen3-tts"
|
||||||
|
mirrors:
|
||||||
|
- localai/localai-backends:latest-gpu-nvidia-cuda-13-faster-qwen3-tts
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "cuda13-faster-qwen3-tts-development"
|
||||||
|
uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-13-faster-qwen3-tts"
|
||||||
|
mirrors:
|
||||||
|
- localai/localai-backends:master-gpu-nvidia-cuda-13-faster-qwen3-tts
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "nvidia-l4t-faster-qwen3-tts"
|
||||||
|
uri: "quay.io/go-skynet/local-ai-backends:latest-nvidia-l4t-faster-qwen3-tts"
|
||||||
|
mirrors:
|
||||||
|
- localai/localai-backends:latest-nvidia-l4t-faster-qwen3-tts
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "nvidia-l4t-faster-qwen3-tts-development"
|
||||||
|
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-faster-qwen3-tts"
|
||||||
|
mirrors:
|
||||||
|
- localai/localai-backends:master-nvidia-l4t-faster-qwen3-tts
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts"
|
||||||
|
uri: "quay.io/go-skynet/local-ai-backends:latest-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts"
|
||||||
|
mirrors:
|
||||||
|
- localai/localai-backends:latest-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts
|
||||||
|
- !!merge <<: *faster-qwen3-tts
|
||||||
|
name: "cuda13-nvidia-l4t-arm64-faster-qwen3-tts-development"
|
||||||
|
uri: "quay.io/go-skynet/local-ai-backends:master-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts"
|
||||||
|
mirrors:
|
||||||
|
- localai/localai-backends:master-nvidia-l4t-cuda-13-arm64-faster-qwen3-tts
|
||||||
## qwen-asr
|
## qwen-asr
|
||||||
- !!merge <<: *qwen-asr
|
- !!merge <<: *qwen-asr
|
||||||
name: "qwen-asr-development"
|
name: "qwen-asr-development"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
grpcio==1.76.0
|
grpcio==1.78.1
|
||||||
protobuf
|
protobuf
|
||||||
grpcio-tools
|
grpcio-tools
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
grpcio==1.76.0
|
grpcio==1.78.1
|
||||||
protobuf
|
protobuf
|
||||||
certifi
|
certifi
|
||||||
packaging==24.1
|
packaging==24.1
|
||||||
@@ -40,7 +40,21 @@ from compel import Compel, ReturnedEmbeddingsType
|
|||||||
from optimum.quanto import freeze, qfloat8, quantize
|
from optimum.quanto import freeze, qfloat8, quantize
|
||||||
from transformers import T5EncoderModel
|
from transformers import T5EncoderModel
|
||||||
from safetensors.torch import load_file
|
from safetensors.torch import load_file
|
||||||
from sd_embed.embedding_funcs import get_weighted_text_embeddings_sd15, get_weighted_text_embeddings_sdxl, get_weighted_text_embeddings_sd3, get_weighted_text_embeddings_flux1
|
# Try to import sd_embed - it might not always be available
|
||||||
|
try:
|
||||||
|
from sd_embed.embedding_funcs import (
|
||||||
|
get_weighted_text_embeddings_sd15,
|
||||||
|
get_weighted_text_embeddings_sdxl,
|
||||||
|
get_weighted_text_embeddings_sd3,
|
||||||
|
get_weighted_text_embeddings_flux1,
|
||||||
|
)
|
||||||
|
SD_EMBED_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
get_weighted_text_embeddings_sd15 = None
|
||||||
|
get_weighted_text_embeddings_sdxl = None
|
||||||
|
get_weighted_text_embeddings_sd3 = None
|
||||||
|
get_weighted_text_embeddings_flux1 = None
|
||||||
|
SD_EMBED_AVAILABLE = False
|
||||||
|
|
||||||
# Import LTX-2 specific utilities
|
# Import LTX-2 specific utilities
|
||||||
from diffusers.pipelines.ltx2.export_utils import encode_video as ltx2_encode_video
|
from diffusers.pipelines.ltx2.export_utils import encode_video as ltx2_encode_video
|
||||||
@@ -49,6 +63,9 @@ from diffusers import LTX2VideoTransformer3DModel, GGUFQuantizationConfig
|
|||||||
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
|
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
|
||||||
COMPEL = os.environ.get("COMPEL", "0") == "1"
|
COMPEL = os.environ.get("COMPEL", "0") == "1"
|
||||||
SD_EMBED = os.environ.get("SD_EMBED", "0") == "1"
|
SD_EMBED = os.environ.get("SD_EMBED", "0") == "1"
|
||||||
|
# Warn if SD_EMBED is enabled but the module is not available
|
||||||
|
if SD_EMBED and not SD_EMBED_AVAILABLE:
|
||||||
|
print("WARNING: SD_EMBED is enabled but sd_embed module is not available. Falling back to standard prompt processing.", file=sys.stderr)
|
||||||
XPU = os.environ.get("XPU", "0") == "1"
|
XPU = os.environ.get("XPU", "0") == "1"
|
||||||
CLIPSKIP = os.environ.get("CLIPSKIP", "1") == "1"
|
CLIPSKIP = os.environ.get("CLIPSKIP", "1") == "1"
|
||||||
SAFETENSORS = os.environ.get("SAFETENSORS", "1") == "1"
|
SAFETENSORS = os.environ.get("SAFETENSORS", "1") == "1"
|
||||||
@@ -179,7 +196,7 @@ def get_scheduler(name: str, config: dict = {}):
|
|||||||
# Implement the BackendServicer class with the service methods
|
# Implement the BackendServicer class with the service methods
|
||||||
class BackendServicer(backend_pb2_grpc.BackendServicer):
|
class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||||
|
|
||||||
def _load_pipeline(self, request, modelFile, fromSingleFile, torchType, variant):
|
def _load_pipeline(self, request, modelFile, fromSingleFile, torchType, variant, device_map=None):
|
||||||
"""
|
"""
|
||||||
Load a diffusers pipeline dynamically using the dynamic loader.
|
Load a diffusers pipeline dynamically using the dynamic loader.
|
||||||
|
|
||||||
@@ -193,6 +210,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
fromSingleFile: Whether to use from_single_file() vs from_pretrained()
|
fromSingleFile: Whether to use from_single_file() vs from_pretrained()
|
||||||
torchType: The torch dtype to use
|
torchType: The torch dtype to use
|
||||||
variant: Model variant (e.g., "fp16")
|
variant: Model variant (e.g., "fp16")
|
||||||
|
device_map: Device mapping strategy (e.g., "auto" for multi-GPU)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The loaded pipeline instance
|
The loaded pipeline instance
|
||||||
@@ -214,14 +232,14 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
dtype = torch.bfloat16
|
dtype = torch.bfloat16
|
||||||
bfl_repo = os.environ.get("BFL_REPO", "ChuckMcSneed/FLUX.1-dev")
|
bfl_repo = os.environ.get("BFL_REPO", "ChuckMcSneed/FLUX.1-dev")
|
||||||
|
|
||||||
transformer = FluxTransformer2DModel.from_single_file(modelFile, torch_dtype=dtype)
|
transformer = FluxTransformer2DModel.from_single_file(modelFile, torch_dtype=dtype, device_map=device_map)
|
||||||
quantize(transformer, weights=qfloat8)
|
quantize(transformer, weights=qfloat8)
|
||||||
freeze(transformer)
|
freeze(transformer)
|
||||||
text_encoder_2 = T5EncoderModel.from_pretrained(bfl_repo, subfolder="text_encoder_2", torch_dtype=dtype)
|
text_encoder_2 = T5EncoderModel.from_pretrained(bfl_repo, subfolder="text_encoder_2", torch_dtype=dtype, device_map=device_map)
|
||||||
quantize(text_encoder_2, weights=qfloat8)
|
quantize(text_encoder_2, weights=qfloat8)
|
||||||
freeze(text_encoder_2)
|
freeze(text_encoder_2)
|
||||||
|
|
||||||
pipe = FluxPipeline.from_pretrained(bfl_repo, transformer=None, text_encoder_2=None, torch_dtype=dtype)
|
pipe = FluxPipeline.from_pretrained(bfl_repo, transformer=None, text_encoder_2=None, torch_dtype=dtype, device_map=device_map)
|
||||||
pipe.transformer = transformer
|
pipe.transformer = transformer
|
||||||
pipe.text_encoder_2 = text_encoder_2
|
pipe.text_encoder_2 = text_encoder_2
|
||||||
|
|
||||||
@@ -234,13 +252,15 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
vae = AutoencoderKLWan.from_pretrained(
|
vae = AutoencoderKLWan.from_pretrained(
|
||||||
request.Model,
|
request.Model,
|
||||||
subfolder="vae",
|
subfolder="vae",
|
||||||
torch_dtype=torch.float32
|
torch_dtype=torch.float32,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
pipe = load_diffusers_pipeline(
|
pipe = load_diffusers_pipeline(
|
||||||
class_name="WanPipeline",
|
class_name="WanPipeline",
|
||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
vae=vae,
|
vae=vae,
|
||||||
torch_dtype=torchType
|
torch_dtype=torchType,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
self.txt2vid = True
|
self.txt2vid = True
|
||||||
return pipe
|
return pipe
|
||||||
@@ -250,13 +270,15 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
vae = AutoencoderKLWan.from_pretrained(
|
vae = AutoencoderKLWan.from_pretrained(
|
||||||
request.Model,
|
request.Model,
|
||||||
subfolder="vae",
|
subfolder="vae",
|
||||||
torch_dtype=torch.float32
|
torch_dtype=torch.float32,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
pipe = load_diffusers_pipeline(
|
pipe = load_diffusers_pipeline(
|
||||||
class_name="WanImageToVideoPipeline",
|
class_name="WanImageToVideoPipeline",
|
||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
vae=vae,
|
vae=vae,
|
||||||
torch_dtype=torchType
|
torch_dtype=torchType,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
self.img2vid = True
|
self.img2vid = True
|
||||||
return pipe
|
return pipe
|
||||||
@@ -267,7 +289,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
class_name="SanaPipeline",
|
class_name="SanaPipeline",
|
||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
variant="bf16",
|
variant="bf16",
|
||||||
torch_dtype=torch.bfloat16
|
torch_dtype=torch.bfloat16,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
pipe.vae.to(torch.bfloat16)
|
pipe.vae.to(torch.bfloat16)
|
||||||
pipe.text_encoder.to(torch.bfloat16)
|
pipe.text_encoder.to(torch.bfloat16)
|
||||||
@@ -279,7 +302,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
pipe = load_diffusers_pipeline(
|
pipe = load_diffusers_pipeline(
|
||||||
class_name="DiffusionPipeline",
|
class_name="DiffusionPipeline",
|
||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
torch_dtype=torchType
|
torch_dtype=torchType,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
return pipe
|
return pipe
|
||||||
|
|
||||||
@@ -290,7 +314,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
class_name="StableVideoDiffusionPipeline",
|
class_name="StableVideoDiffusionPipeline",
|
||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
torch_dtype=torchType,
|
torch_dtype=torchType,
|
||||||
variant=variant
|
variant=variant,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
if not DISABLE_CPU_OFFLOAD:
|
if not DISABLE_CPU_OFFLOAD:
|
||||||
pipe.enable_model_cpu_offload()
|
pipe.enable_model_cpu_offload()
|
||||||
@@ -314,6 +339,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
modelFile,
|
modelFile,
|
||||||
config=request.Model, # Use request.Model as the config/model_id
|
config=request.Model, # Use request.Model as the config/model_id
|
||||||
subfolder="transformer",
|
subfolder="transformer",
|
||||||
|
device_map=device_map,
|
||||||
**transformer_kwargs,
|
**transformer_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -323,6 +349,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
transformer=transformer,
|
transformer=transformer,
|
||||||
torch_dtype=torchType,
|
torch_dtype=torchType,
|
||||||
|
device_map=device_map,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Single file but not GGUF - use standard single file loading
|
# Single file but not GGUF - use standard single file loading
|
||||||
@@ -331,6 +358,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
model_id=modelFile,
|
model_id=modelFile,
|
||||||
from_single_file=True,
|
from_single_file=True,
|
||||||
torch_dtype=torchType,
|
torch_dtype=torchType,
|
||||||
|
device_map=device_map,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Standard loading from pretrained
|
# Standard loading from pretrained
|
||||||
@@ -338,7 +366,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
class_name="LTX2ImageToVideoPipeline",
|
class_name="LTX2ImageToVideoPipeline",
|
||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
torch_dtype=torchType,
|
torch_dtype=torchType,
|
||||||
variant=variant
|
variant=variant,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
|
|
||||||
if not DISABLE_CPU_OFFLOAD:
|
if not DISABLE_CPU_OFFLOAD:
|
||||||
@@ -363,6 +392,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
modelFile,
|
modelFile,
|
||||||
config=request.Model, # Use request.Model as the config/model_id
|
config=request.Model, # Use request.Model as the config/model_id
|
||||||
subfolder="transformer",
|
subfolder="transformer",
|
||||||
|
device_map=device_map,
|
||||||
**transformer_kwargs,
|
**transformer_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -372,6 +402,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
transformer=transformer,
|
transformer=transformer,
|
||||||
torch_dtype=torchType,
|
torch_dtype=torchType,
|
||||||
|
device_map=device_map,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Single file but not GGUF - use standard single file loading
|
# Single file but not GGUF - use standard single file loading
|
||||||
@@ -380,6 +411,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
model_id=modelFile,
|
model_id=modelFile,
|
||||||
from_single_file=True,
|
from_single_file=True,
|
||||||
torch_dtype=torchType,
|
torch_dtype=torchType,
|
||||||
|
device_map=device_map,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Standard loading from pretrained
|
# Standard loading from pretrained
|
||||||
@@ -387,7 +419,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
class_name="LTX2Pipeline",
|
class_name="LTX2Pipeline",
|
||||||
model_id=request.Model,
|
model_id=request.Model,
|
||||||
torch_dtype=torchType,
|
torch_dtype=torchType,
|
||||||
variant=variant
|
variant=variant,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
|
|
||||||
if not DISABLE_CPU_OFFLOAD:
|
if not DISABLE_CPU_OFFLOAD:
|
||||||
@@ -410,6 +443,10 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
if not fromSingleFile:
|
if not fromSingleFile:
|
||||||
load_kwargs["use_safetensors"] = SAFETENSORS
|
load_kwargs["use_safetensors"] = SAFETENSORS
|
||||||
|
|
||||||
|
# Add device_map for multi-GPU support (when TensorParallelSize > 1)
|
||||||
|
if device_map:
|
||||||
|
load_kwargs["device_map"] = device_map
|
||||||
|
|
||||||
# Determine pipeline class name - default to AutoPipelineForText2Image
|
# Determine pipeline class name - default to AutoPipelineForText2Image
|
||||||
effective_pipeline_type = pipeline_type if pipeline_type else "AutoPipelineForText2Image"
|
effective_pipeline_type = pipeline_type if pipeline_type else "AutoPipelineForText2Image"
|
||||||
|
|
||||||
@@ -512,6 +549,13 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
|
|
||||||
print(f"LoadModel: PipelineType from request: {request.PipelineType}", file=sys.stderr)
|
print(f"LoadModel: PipelineType from request: {request.PipelineType}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Determine device_map for multi-GPU support based on TensorParallelSize
|
||||||
|
# When TensorParallelSize > 1, use device_map='auto' to distribute model across GPUs
|
||||||
|
device_map = None
|
||||||
|
if hasattr(request, 'TensorParallelSize') and request.TensorParallelSize > 1:
|
||||||
|
device_map = "auto"
|
||||||
|
print(f"LoadModel: Multi-GPU mode enabled with TensorParallelSize={request.TensorParallelSize}, using device_map='auto'", file=sys.stderr)
|
||||||
|
|
||||||
# Load pipeline using dynamic loader
|
# Load pipeline using dynamic loader
|
||||||
# Special cases that require custom initialization are handled first
|
# Special cases that require custom initialization are handled first
|
||||||
self.pipe = self._load_pipeline(
|
self.pipe = self._load_pipeline(
|
||||||
@@ -519,7 +563,8 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
modelFile=modelFile,
|
modelFile=modelFile,
|
||||||
fromSingleFile=fromSingleFile,
|
fromSingleFile=fromSingleFile,
|
||||||
torchType=torchType,
|
torchType=torchType,
|
||||||
variant=variant
|
variant=variant,
|
||||||
|
device_map=device_map
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"LoadModel: After loading - ltx2_pipeline: {self.ltx2_pipeline}, img2vid: {self.img2vid}, txt2vid: {self.txt2vid}, PipelineType: {self.PipelineType}", file=sys.stderr)
|
print(f"LoadModel: After loading - ltx2_pipeline: {self.ltx2_pipeline}, img2vid: {self.img2vid}, txt2vid: {self.txt2vid}, PipelineType: {self.PipelineType}", file=sys.stderr)
|
||||||
@@ -544,7 +589,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
|
|
||||||
if request.ControlNet:
|
if request.ControlNet:
|
||||||
self.controlnet = ControlNetModel.from_pretrained(
|
self.controlnet = ControlNetModel.from_pretrained(
|
||||||
request.ControlNet, torch_dtype=torchType, variant=variant
|
request.ControlNet, torch_dtype=torchType, variant=variant, device_map=device_map
|
||||||
)
|
)
|
||||||
self.pipe.controlnet = self.controlnet
|
self.pipe.controlnet = self.controlnet
|
||||||
else:
|
else:
|
||||||
@@ -583,7 +628,9 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
|
|
||||||
self.pipe.set_adapters(adapters_name, adapter_weights=adapters_weights)
|
self.pipe.set_adapters(adapters_name, adapter_weights=adapters_weights)
|
||||||
|
|
||||||
if device != "cpu":
|
# Only move pipeline to device if NOT using device_map
|
||||||
|
# device_map handles device placement automatically
|
||||||
|
if device_map is None and device != "cpu":
|
||||||
self.pipe.to(device)
|
self.pipe.to(device)
|
||||||
if self.controlnet:
|
if self.controlnet:
|
||||||
self.controlnet.to(device)
|
self.controlnet.to(device)
|
||||||
@@ -743,7 +790,7 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
|
|||||||
guidance_scale=self.cfg_scale,
|
guidance_scale=self.cfg_scale,
|
||||||
**kwargs
|
**kwargs
|
||||||
).images[0]
|
).images[0]
|
||||||
elif SD_EMBED:
|
elif SD_EMBED and SD_EMBED_AVAILABLE:
|
||||||
if self.PipelineType == "StableDiffusionPipeline":
|
if self.PipelineType == "StableDiffusionPipeline":
|
||||||
(
|
(
|
||||||
kwargs["prompt_embeds"],
|
kwargs["prompt_embeds"],
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ git+https://github.com/huggingface/diffusers
|
|||||||
transformers
|
transformers
|
||||||
accelerate
|
accelerate
|
||||||
compel
|
compel
|
||||||
git+https://github.com/xhinker/sd_embed
|
|
||||||
peft
|
peft
|
||||||
optimum-quanto
|
optimum-quanto
|
||||||
numpy<2
|
numpy<2
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ git+https://github.com/huggingface/diffusers
|
|||||||
transformers
|
transformers
|
||||||
accelerate
|
accelerate
|
||||||
compel
|
compel
|
||||||
git+https://github.com/xhinker/sd_embed
|
|
||||||
peft
|
peft
|
||||||
optimum-quanto
|
optimum-quanto
|
||||||
numpy<2
|
numpy<2
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ opencv-python
|
|||||||
transformers
|
transformers
|
||||||
accelerate
|
accelerate
|
||||||
compel
|
compel
|
||||||
git+https://github.com/xhinker/sd_embed
|
|
||||||
peft
|
peft
|
||||||
sentencepiece
|
sentencepiece
|
||||||
optimum-quanto
|
optimum-quanto
|
||||||
|
|||||||
23
backend/python/faster-qwen3-tts/Makefile
Normal file
23
backend/python/faster-qwen3-tts/Makefile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.PHONY: faster-qwen3-tts
|
||||||
|
faster-qwen3-tts:
|
||||||
|
bash install.sh
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run: faster-qwen3-tts
|
||||||
|
@echo "Running faster-qwen3-tts..."
|
||||||
|
bash run.sh
|
||||||
|
@echo "faster-qwen3-tts run."
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: faster-qwen3-tts
|
||||||
|
@echo "Testing faster-qwen3-tts..."
|
||||||
|
bash test.sh
|
||||||
|
@echo "faster-qwen3-tts tested."
|
||||||
|
|
||||||
|
.PHONY: protogen-clean
|
||||||
|
protogen-clean:
|
||||||
|
$(RM) backend_pb2_grpc.py backend_pb2.py
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: protogen-clean
|
||||||
|
rm -rf venv __pycache__
|
||||||
193
backend/python/faster-qwen3-tts/backend.py
Normal file
193
backend/python/faster-qwen3-tts/backend.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
gRPC server of LocalAI for Faster Qwen3-TTS (CUDA graph capture, voice clone only).
|
||||||
|
"""
|
||||||
|
from concurrent import futures
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import backend_pb2
|
||||||
|
import backend_pb2_grpc
|
||||||
|
import torch
|
||||||
|
import soundfile as sf
|
||||||
|
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
|
||||||
|
def is_float(s):
|
||||||
|
try:
|
||||||
|
float(s)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_int(s):
|
||||||
|
try:
|
||||||
|
int(s)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
_ONE_DAY_IN_SECONDS = 60 * 60 * 24
|
||||||
|
MAX_WORKERS = int(os.environ.get('PYTHON_GRPC_MAX_WORKERS', '1'))
|
||||||
|
|
||||||
|
|
||||||
|
class BackendServicer(backend_pb2_grpc.BackendServicer):
|
||||||
|
def Health(self, request, context):
|
||||||
|
return backend_pb2.Reply(message=bytes("OK", 'utf-8'))
|
||||||
|
|
||||||
|
def LoadModel(self, request, context):
|
||||||
|
if not torch.cuda.is_available():
|
||||||
|
return backend_pb2.Result(
|
||||||
|
success=False,
|
||||||
|
message="faster-qwen3-tts requires NVIDIA GPU with CUDA"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.options = {}
|
||||||
|
for opt in request.Options:
|
||||||
|
if ":" not in opt:
|
||||||
|
continue
|
||||||
|
key, value = opt.split(":", 1)
|
||||||
|
if is_float(value):
|
||||||
|
value = float(value)
|
||||||
|
elif is_int(value):
|
||||||
|
value = int(value)
|
||||||
|
elif value.lower() in ["true", "false"]:
|
||||||
|
value = value.lower() == "true"
|
||||||
|
self.options[key] = value
|
||||||
|
|
||||||
|
model_path = request.Model or "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
|
||||||
|
self.audio_path = request.AudioPath if hasattr(request, 'AudioPath') and request.AudioPath else None
|
||||||
|
self.model_file = request.ModelFile if hasattr(request, 'ModelFile') and request.ModelFile else None
|
||||||
|
self.model_path = request.ModelPath if hasattr(request, 'ModelPath') and request.ModelPath else None
|
||||||
|
|
||||||
|
from faster_qwen3_tts import FasterQwen3TTS
|
||||||
|
print(f"Loading model from: {model_path}", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
self.model = FasterQwen3TTS.from_pretrained(model_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Loading model: {type(e).__name__}: {e}", file=sys.stderr)
|
||||||
|
print(traceback.format_exc(), file=sys.stderr)
|
||||||
|
return backend_pb2.Result(success=False, message=str(e))
|
||||||
|
|
||||||
|
print(f"Model loaded successfully: {model_path}", file=sys.stderr)
|
||||||
|
return backend_pb2.Result(message="Model loaded successfully", success=True)
|
||||||
|
|
||||||
|
def _get_ref_audio_path(self, request):
|
||||||
|
if not self.audio_path:
|
||||||
|
return None
|
||||||
|
if os.path.isabs(self.audio_path):
|
||||||
|
return self.audio_path
|
||||||
|
if self.model_file:
|
||||||
|
model_file_base = os.path.dirname(self.model_file)
|
||||||
|
ref_path = os.path.join(model_file_base, self.audio_path)
|
||||||
|
if os.path.exists(ref_path):
|
||||||
|
return ref_path
|
||||||
|
if self.model_path:
|
||||||
|
ref_path = os.path.join(self.model_path, self.audio_path)
|
||||||
|
if os.path.exists(ref_path):
|
||||||
|
return ref_path
|
||||||
|
return self.audio_path
|
||||||
|
|
||||||
|
def TTS(self, request, context):
|
||||||
|
try:
|
||||||
|
if not request.dst:
|
||||||
|
return backend_pb2.Result(
|
||||||
|
success=False,
|
||||||
|
message="dst (output path) is required"
|
||||||
|
)
|
||||||
|
text = request.text.strip()
|
||||||
|
if not text:
|
||||||
|
return backend_pb2.Result(
|
||||||
|
success=False,
|
||||||
|
message="Text is empty"
|
||||||
|
)
|
||||||
|
|
||||||
|
language = request.language if hasattr(request, 'language') and request.language else None
|
||||||
|
if not language or language == "":
|
||||||
|
language = "English"
|
||||||
|
|
||||||
|
ref_audio = self._get_ref_audio_path(request)
|
||||||
|
if not ref_audio:
|
||||||
|
return backend_pb2.Result(
|
||||||
|
success=False,
|
||||||
|
message="AudioPath is required for voice clone (set in LoadModel)"
|
||||||
|
)
|
||||||
|
ref_text = self.options.get("ref_text")
|
||||||
|
if not ref_text and hasattr(request, 'ref_text') and request.ref_text:
|
||||||
|
ref_text = request.ref_text
|
||||||
|
if not ref_text:
|
||||||
|
return backend_pb2.Result(
|
||||||
|
success=False,
|
||||||
|
message="ref_text is required for voice clone (set via LoadModel Options, e.g. ref_text:Your reference transcript)"
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk_size = self.options.get("chunk_size")
|
||||||
|
generation_kwargs = {}
|
||||||
|
if chunk_size is not None:
|
||||||
|
generation_kwargs["chunk_size"] = int(chunk_size)
|
||||||
|
|
||||||
|
audio_list, sr = self.model.generate_voice_clone(
|
||||||
|
text=text,
|
||||||
|
language=language,
|
||||||
|
ref_audio=ref_audio,
|
||||||
|
ref_text=ref_text,
|
||||||
|
**generation_kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
if audio_list is None or (isinstance(audio_list, list) and len(audio_list) == 0):
|
||||||
|
return backend_pb2.Result(
|
||||||
|
success=False,
|
||||||
|
message="No audio output generated"
|
||||||
|
)
|
||||||
|
audio_data = audio_list[0] if isinstance(audio_list, list) else audio_list
|
||||||
|
sf.write(request.dst, audio_data, sr)
|
||||||
|
print(f"Saved output to {request.dst}", file=sys.stderr)
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
print(f"Error in TTS: {err}", file=sys.stderr)
|
||||||
|
print(traceback.format_exc(), file=sys.stderr)
|
||||||
|
return backend_pb2.Result(success=False, message=f"Unexpected {err=}, {type(err)=}")
|
||||||
|
|
||||||
|
return backend_pb2.Result(success=True)
|
||||||
|
|
||||||
|
|
||||||
|
def serve(address):
|
||||||
|
server = grpc.server(
|
||||||
|
futures.ThreadPoolExecutor(max_workers=MAX_WORKERS),
|
||||||
|
options=[
|
||||||
|
('grpc.max_message_length', 50 * 1024 * 1024),
|
||||||
|
('grpc.max_send_message_length', 50 * 1024 * 1024),
|
||||||
|
('grpc.max_receive_message_length', 50 * 1024 * 1024),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
backend_pb2_grpc.add_BackendServicer_to_server(BackendServicer(), server)
|
||||||
|
server.add_insecure_port(address)
|
||||||
|
server.start()
|
||||||
|
print("Server started. Listening on: " + address, file=sys.stderr)
|
||||||
|
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
print("Received termination signal. Shutting down...")
|
||||||
|
server.stop(0)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(_ONE_DAY_IN_SECONDS)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
server.stop(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Run the gRPC server.")
|
||||||
|
parser.add_argument("--addr", default="localhost:50051", help="The address to bind the server to.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
serve(args.addr)
|
||||||
13
backend/python/faster-qwen3-tts/install.sh
Normal file
13
backend/python/faster-qwen3-tts/install.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
EXTRA_PIP_INSTALL_FLAGS="--no-build-isolation"
|
||||||
|
|
||||||
|
backend_dir=$(dirname $0)
|
||||||
|
if [ -d $backend_dir/common ]; then
|
||||||
|
source $backend_dir/common/libbackend.sh
|
||||||
|
else
|
||||||
|
source $backend_dir/../common/libbackend.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
installRequirements
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
--extra-index-url https://download.pytorch.org/whl/cu121
|
||||||
|
torch
|
||||||
|
torchaudio
|
||||||
|
faster-qwen3-tts
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
--extra-index-url https://download.pytorch.org/whl/cu130
|
||||||
|
torch
|
||||||
|
torchaudio
|
||||||
|
faster-qwen3-tts
|
||||||
1
backend/python/faster-qwen3-tts/requirements-install.txt
Normal file
1
backend/python/faster-qwen3-tts/requirements-install.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
setuptools
|
||||||
4
backend/python/faster-qwen3-tts/requirements-l4t12.txt
Normal file
4
backend/python/faster-qwen3-tts/requirements-l4t12.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
--extra-index-url https://pypi.jetson-ai-lab.io/jp6/cu129/
|
||||||
|
torch
|
||||||
|
torchaudio
|
||||||
|
faster-qwen3-tts
|
||||||
4
backend/python/faster-qwen3-tts/requirements-l4t13.txt
Normal file
4
backend/python/faster-qwen3-tts/requirements-l4t13.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
--extra-index-url https://download.pytorch.org/whl/cu130
|
||||||
|
torch
|
||||||
|
torchaudio
|
||||||
|
faster-qwen3-tts
|
||||||
8
backend/python/faster-qwen3-tts/requirements.txt
Normal file
8
backend/python/faster-qwen3-tts/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
grpcio==1.71.0
|
||||||
|
protobuf
|
||||||
|
certifi
|
||||||
|
packaging==24.1
|
||||||
|
soundfile
|
||||||
|
setuptools
|
||||||
|
six
|
||||||
|
sox
|
||||||
9
backend/python/faster-qwen3-tts/run.sh
Normal file
9
backend/python/faster-qwen3-tts/run.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
backend_dir=$(dirname $0)
|
||||||
|
if [ -d $backend_dir/common ]; then
|
||||||
|
source $backend_dir/common/libbackend.sh
|
||||||
|
else
|
||||||
|
source $backend_dir/../common/libbackend.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
startBackend $@
|
||||||
104
backend/python/faster-qwen3-tts/test.py
Normal file
104
backend/python/faster-qwen3-tts/test.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"""
|
||||||
|
Tests for the faster-qwen3-tts gRPC backend.
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import backend_pb2
|
||||||
|
import backend_pb2_grpc
|
||||||
|
import grpc
|
||||||
|
|
||||||
|
|
||||||
|
class TestBackendServicer(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.service = subprocess.Popen(
|
||||||
|
["python3", "backend.py", "--addr", "localhost:50052"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
cwd=os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
)
|
||||||
|
time.sleep(15)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.service.terminate()
|
||||||
|
try:
|
||||||
|
self.service.communicate(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.service.kill()
|
||||||
|
self.service.communicate()
|
||||||
|
|
||||||
|
def test_health(self):
|
||||||
|
with grpc.insecure_channel("localhost:50052") as channel:
|
||||||
|
stub = backend_pb2_grpc.BackendStub(channel)
|
||||||
|
reply = stub.Health(backend_pb2.HealthMessage(), timeout=5.0)
|
||||||
|
self.assertEqual(reply.message, b"OK")
|
||||||
|
|
||||||
|
def test_load_model_requires_cuda(self):
|
||||||
|
with grpc.insecure_channel("localhost:50052") as channel:
|
||||||
|
stub = backend_pb2_grpc.BackendStub(channel)
|
||||||
|
response = stub.LoadModel(
|
||||||
|
backend_pb2.ModelOptions(
|
||||||
|
Model="Qwen/Qwen3-TTS-12Hz-0.6B-Base",
|
||||||
|
CUDA=True,
|
||||||
|
),
|
||||||
|
timeout=10.0,
|
||||||
|
)
|
||||||
|
self.assertFalse(response.success)
|
||||||
|
|
||||||
|
@unittest.skipUnless(
|
||||||
|
__import__("torch").cuda.is_available(),
|
||||||
|
"faster-qwen3-tts TTS requires CUDA",
|
||||||
|
)
|
||||||
|
def test_tts(self):
|
||||||
|
import soundfile as sf
|
||||||
|
try:
|
||||||
|
with grpc.insecure_channel("localhost:50052") as channel:
|
||||||
|
stub = backend_pb2_grpc.BackendStub(channel)
|
||||||
|
ref_audio = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
|
||||||
|
ref_audio.close()
|
||||||
|
try:
|
||||||
|
sr = 22050
|
||||||
|
duration = 1.0
|
||||||
|
samples = int(sr * duration)
|
||||||
|
sf.write(ref_audio.name, [0.0] * samples, sr)
|
||||||
|
|
||||||
|
response = stub.LoadModel(
|
||||||
|
backend_pb2.ModelOptions(
|
||||||
|
Model="Qwen/Qwen3-TTS-12Hz-0.6B-Base",
|
||||||
|
AudioPath=ref_audio.name,
|
||||||
|
Options=["ref_text:Hello world"],
|
||||||
|
),
|
||||||
|
timeout=600.0,
|
||||||
|
)
|
||||||
|
self.assertTrue(response.success, response.message)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as out:
|
||||||
|
output_path = out.name
|
||||||
|
try:
|
||||||
|
tts_response = stub.TTS(
|
||||||
|
backend_pb2.TTSRequest(
|
||||||
|
text="Test output.",
|
||||||
|
dst=output_path,
|
||||||
|
language="English",
|
||||||
|
),
|
||||||
|
timeout=120.0,
|
||||||
|
)
|
||||||
|
self.assertTrue(tts_response.success, tts_response.message)
|
||||||
|
self.assertTrue(os.path.exists(output_path))
|
||||||
|
self.assertGreater(os.path.getsize(output_path), 0)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(output_path):
|
||||||
|
os.unlink(output_path)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(ref_audio.name):
|
||||||
|
os.unlink(ref_audio.name)
|
||||||
|
except Exception as err:
|
||||||
|
self.fail(f"TTS test failed: {err}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
11
backend/python/faster-qwen3-tts/test.sh
Normal file
11
backend/python/faster-qwen3-tts/test.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
backend_dir=$(dirname $0)
|
||||||
|
if [ -d $backend_dir/common ]; then
|
||||||
|
source $backend_dir/common/libbackend.sh
|
||||||
|
else
|
||||||
|
source $backend_dir/../common/libbackend.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
runUnittests
|
||||||
@@ -32,7 +32,14 @@ if [ "x${BUILD_PROFILE}" == "xl4t12" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
git clone https://github.com/neuphonic/neutts-air neutts-air
|
git clone --depth 100 https://github.com/neuphonic/neutts-air neutts-air
|
||||||
|
|
||||||
|
cd neutts-air
|
||||||
|
|
||||||
|
git checkout 1737487debe5b40a0bb97875edce8c66b391722b
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
|
||||||
cp -rfv neutts-air/neuttsair ./
|
cp -rfv neutts-air/neuttsair ./
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ protobuf
|
|||||||
certifi
|
certifi
|
||||||
packaging==24.1
|
packaging==24.1
|
||||||
setuptools
|
setuptools
|
||||||
|
h11
|
||||||
|
gradio
|
||||||
|
uvicorn
|
||||||
@@ -4,4 +4,6 @@ certifi
|
|||||||
packaging==24.1
|
packaging==24.1
|
||||||
soundfile
|
soundfile
|
||||||
setuptools
|
setuptools
|
||||||
six
|
six
|
||||||
|
scipy
|
||||||
|
librosa
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
grpcio==1.76.0
|
grpcio==1.78.1
|
||||||
protobuf
|
protobuf
|
||||||
certifi
|
certifi
|
||||||
@@ -4,5 +4,5 @@ numba==0.60.0
|
|||||||
accelerate
|
accelerate
|
||||||
transformers
|
transformers
|
||||||
bitsandbytes
|
bitsandbytes
|
||||||
sentence-transformers==5.2.2
|
sentence-transformers==5.2.3
|
||||||
protobuf==6.33.5
|
protobuf==6.33.5
|
||||||
@@ -4,5 +4,5 @@ llvmlite==0.43.0
|
|||||||
numba==0.60.0
|
numba==0.60.0
|
||||||
transformers
|
transformers
|
||||||
bitsandbytes
|
bitsandbytes
|
||||||
sentence-transformers==5.2.2
|
sentence-transformers==5.2.3
|
||||||
protobuf==6.33.5
|
protobuf==6.33.5
|
||||||
@@ -4,5 +4,5 @@ llvmlite==0.43.0
|
|||||||
numba==0.60.0
|
numba==0.60.0
|
||||||
transformers
|
transformers
|
||||||
bitsandbytes
|
bitsandbytes
|
||||||
sentence-transformers==5.2.2
|
sentence-transformers==5.2.3
|
||||||
protobuf==6.33.5
|
protobuf==6.33.5
|
||||||
@@ -5,5 +5,5 @@ transformers
|
|||||||
llvmlite==0.43.0
|
llvmlite==0.43.0
|
||||||
numba==0.60.0
|
numba==0.60.0
|
||||||
bitsandbytes
|
bitsandbytes
|
||||||
sentence-transformers==5.2.2
|
sentence-transformers==5.2.3
|
||||||
protobuf==6.33.5
|
protobuf==6.33.5
|
||||||
@@ -5,5 +5,5 @@ llvmlite==0.43.0
|
|||||||
numba==0.60.0
|
numba==0.60.0
|
||||||
transformers
|
transformers
|
||||||
bitsandbytes
|
bitsandbytes
|
||||||
sentence-transformers==5.2.2
|
sentence-transformers==5.2.3
|
||||||
protobuf==6.33.5
|
protobuf==6.33.5
|
||||||
@@ -4,5 +4,5 @@ numba==0.60.0
|
|||||||
accelerate
|
accelerate
|
||||||
transformers
|
transformers
|
||||||
bitsandbytes
|
bitsandbytes
|
||||||
sentence-transformers==5.2.2
|
sentence-transformers==5.2.3
|
||||||
protobuf==6.33.5
|
protobuf==6.33.5
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
grpcio==1.76.0
|
grpcio==1.78.1
|
||||||
protobuf==6.33.5
|
protobuf==6.33.5
|
||||||
certifi
|
certifi
|
||||||
setuptools
|
setuptools
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
grpcio==1.76.0
|
grpcio==1.78.1
|
||||||
protobuf
|
protobuf
|
||||||
certifi
|
certifi
|
||||||
setuptools
|
setuptools
|
||||||
@@ -9,7 +9,12 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
installRequirements
|
installRequirements
|
||||||
|
|
||||||
|
if [ "x${USE_PIP}" == "xtrue" ]; then
|
||||||
|
pip install "setuptools<70.0.0"
|
||||||
|
else
|
||||||
|
uv pip install "setuptools<70.0.0"
|
||||||
|
fi
|
||||||
# Apply patch to fix PyTorch compatibility issue in voxcpm
|
# Apply patch to fix PyTorch compatibility issue in voxcpm
|
||||||
# This fixes the "Dimension out of range" error in scaled_dot_product_attention
|
# This fixes the "Dimension out of range" error in scaled_dot_product_attention
|
||||||
# by changing .contiguous() to .unsqueeze(0) in the attention module
|
# by changing .contiguous() to .unsqueeze(0) in the attention module
|
||||||
|
|||||||
@@ -319,6 +319,29 @@ func loadRuntimeSettingsFromFile(options *config.ApplicationConfig) {
|
|||||||
options.MemoryReclaimerThreshold = *settings.MemoryReclaimerThreshold
|
options.MemoryReclaimerThreshold = *settings.MemoryReclaimerThreshold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if settings.ForceEvictionWhenBusy != nil {
|
||||||
|
// Only apply if current value is default (false), suggesting it wasn't set from env var
|
||||||
|
if !options.ForceEvictionWhenBusy {
|
||||||
|
options.ForceEvictionWhenBusy = *settings.ForceEvictionWhenBusy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if settings.LRUEvictionMaxRetries != nil {
|
||||||
|
// Only apply if current value is default (30), suggesting it wasn't set from env var
|
||||||
|
if options.LRUEvictionMaxRetries == 0 {
|
||||||
|
options.LRUEvictionMaxRetries = *settings.LRUEvictionMaxRetries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if settings.LRUEvictionRetryInterval != nil {
|
||||||
|
// Only apply if current value is default (1s), suggesting it wasn't set from env var
|
||||||
|
if options.LRUEvictionRetryInterval == 0 {
|
||||||
|
dur, err := time.ParseDuration(*settings.LRUEvictionRetryInterval)
|
||||||
|
if err == nil {
|
||||||
|
options.LRUEvictionRetryInterval = dur
|
||||||
|
} else {
|
||||||
|
xlog.Warn("invalid LRU eviction retry interval in runtime_settings.json", "error", err, "interval", *settings.LRUEvictionRetryInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if settings.AgentJobRetentionDays != nil {
|
if settings.AgentJobRetentionDays != nil {
|
||||||
// Only apply if current value is default (0), suggesting it wasn't set from env var
|
// Only apply if current value is default (0), suggesting it wasn't set from env var
|
||||||
if options.AgentJobRetentionDays == 0 {
|
if options.AgentJobRetentionDays == 0 {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package application
|
package application
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/pkg/model"
|
"github.com/mudler/LocalAI/pkg/model"
|
||||||
"github.com/mudler/xlog"
|
"github.com/mudler/xlog"
|
||||||
)
|
)
|
||||||
@@ -37,11 +35,15 @@ func (a *Application) startWatchdog() error {
|
|||||||
model.WithMemoryReclaimer(appConfig.MemoryReclaimerEnabled, appConfig.MemoryReclaimerThreshold),
|
model.WithMemoryReclaimer(appConfig.MemoryReclaimerEnabled, appConfig.MemoryReclaimerThreshold),
|
||||||
model.WithForceEvictionWhenBusy(appConfig.ForceEvictionWhenBusy),
|
model.WithForceEvictionWhenBusy(appConfig.ForceEvictionWhenBusy),
|
||||||
)
|
)
|
||||||
a.modelLoader.SetWatchDog(wd)
|
|
||||||
|
|
||||||
// Create new stop channel
|
// Create new stop channel BEFORE setting up any goroutines
|
||||||
|
// This prevents race conditions where the old shutdown handler might
|
||||||
|
// receive the closed channel and try to shut down the new watchdog
|
||||||
a.watchdogStop = make(chan bool, 1)
|
a.watchdogStop = make(chan bool, 1)
|
||||||
|
|
||||||
|
// Set the watchdog on the model loader
|
||||||
|
a.modelLoader.SetWatchDog(wd)
|
||||||
|
|
||||||
// Start watchdog goroutine if any periodic checks are enabled
|
// Start watchdog goroutine if any periodic checks are enabled
|
||||||
// LRU eviction doesn't need the Run() loop - it's triggered on model load
|
// LRU eviction doesn't need the Run() loop - it's triggered on model load
|
||||||
// But memory reclaimer needs the Run() loop for periodic checking
|
// But memory reclaimer needs the Run() loop for periodic checking
|
||||||
@@ -49,15 +51,19 @@ func (a *Application) startWatchdog() error {
|
|||||||
go wd.Run()
|
go wd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup shutdown handler
|
// Setup shutdown handler - this goroutine will wait on a.watchdogStop
|
||||||
|
// which is now a fresh channel, so it won't receive any stale signals
|
||||||
|
// Note: We capture wd in a local variable to ensure this handler operates
|
||||||
|
// on the correct watchdog instance (not a later one that gets assigned to wd)
|
||||||
|
wdForShutdown := wd
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
case <-a.watchdogStop:
|
case <-a.watchdogStop:
|
||||||
xlog.Debug("Watchdog stop signal received")
|
xlog.Debug("Watchdog stop signal received")
|
||||||
wd.Shutdown()
|
wdForShutdown.Shutdown()
|
||||||
case <-appConfig.Context.Done():
|
case <-appConfig.Context.Done():
|
||||||
xlog.Debug("Context canceled, shutting down watchdog")
|
xlog.Debug("Context canceled, shutting down watchdog")
|
||||||
wd.Shutdown()
|
wdForShutdown.Shutdown()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -82,20 +88,41 @@ func (a *Application) RestartWatchdog() error {
|
|||||||
a.watchdogMutex.Lock()
|
a.watchdogMutex.Lock()
|
||||||
defer a.watchdogMutex.Unlock()
|
defer a.watchdogMutex.Unlock()
|
||||||
|
|
||||||
// Shutdown existing watchdog if running
|
// Get the old watchdog before we shut it down
|
||||||
|
oldWD := a.modelLoader.GetWatchDog()
|
||||||
|
|
||||||
|
// Get the state from the old watchdog before shutting it down
|
||||||
|
// This preserves information about loaded models
|
||||||
|
var oldState model.WatchDogState
|
||||||
|
if oldWD != nil {
|
||||||
|
oldState = oldWD.GetState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal all handlers to stop by closing the stop channel
|
||||||
|
// This will cause any goroutine waiting on <-a.watchdogStop to unblock
|
||||||
if a.watchdogStop != nil {
|
if a.watchdogStop != nil {
|
||||||
close(a.watchdogStop)
|
close(a.watchdogStop)
|
||||||
a.watchdogStop = nil
|
a.watchdogStop = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown existing watchdog if running
|
// Shutdown existing watchdog - this triggers the stop signal
|
||||||
currentWD := a.modelLoader.GetWatchDog()
|
if oldWD != nil {
|
||||||
if currentWD != nil {
|
oldWD.Shutdown()
|
||||||
currentWD.Shutdown()
|
// Wait for the old watchdog's Run() goroutine to fully shut down
|
||||||
// Wait a bit for shutdown to complete
|
oldWD.WaitDone()
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start watchdog with new settings
|
// Start watchdog with new settings
|
||||||
return a.startWatchdog()
|
if err := a.startWatchdog(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the model state from the old watchdog to the new one
|
||||||
|
// This ensures the new watchdog knows about already-loaded models
|
||||||
|
newWD := a.modelLoader.GetWatchDog()
|
||||||
|
if newWD != nil && len(oldState.AddressModelMap) > 0 {
|
||||||
|
newWD.RestoreState(oldState)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package backend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/pkg/grpc"
|
"github.com/mudler/LocalAI/pkg/grpc"
|
||||||
model "github.com/mudler/LocalAI/pkg/model"
|
model "github.com/mudler/LocalAI/pkg/model"
|
||||||
@@ -53,7 +55,7 @@ func ModelEmbedding(s string, tokens []int, loader *model.ModelLoader, modelConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return func() ([]float32, error) {
|
wrappedFn := func() ([]float32, error) {
|
||||||
embeds, err := fn()
|
embeds, err := fn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return embeds, err
|
return embeds, err
|
||||||
@@ -67,5 +69,48 @@ func ModelEmbedding(s string, tokens []int, loader *model.ModelLoader, modelConf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return embeds, nil
|
return embeds, nil
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
|
||||||
|
traceData := map[string]any{
|
||||||
|
"input_text": trace.TruncateString(s, 1000),
|
||||||
|
"input_tokens_count": len(tokens),
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
originalFn := wrappedFn
|
||||||
|
wrappedFn = func() ([]float32, error) {
|
||||||
|
result, err := originalFn()
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
traceData["embedding_dimensions"] = len(result)
|
||||||
|
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := trace.TruncateString(s, 200)
|
||||||
|
if summary == "" {
|
||||||
|
summary = fmt.Sprintf("tokens[%d]", len(tokens))
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: duration,
|
||||||
|
Type: trace.BackendTraceEmbedding,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: summary,
|
||||||
|
Error: errStr,
|
||||||
|
Data: traceData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrappedFn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||||
model "github.com/mudler/LocalAI/pkg/model"
|
model "github.com/mudler/LocalAI/pkg/model"
|
||||||
@@ -36,6 +39,46 @@ func ImageGeneration(height, width, step, seed int, positive_prompt, negative_pr
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
|
||||||
|
traceData := map[string]any{
|
||||||
|
"positive_prompt": positive_prompt,
|
||||||
|
"negative_prompt": negative_prompt,
|
||||||
|
"height": height,
|
||||||
|
"width": width,
|
||||||
|
"step": step,
|
||||||
|
"seed": seed,
|
||||||
|
"source_image": src,
|
||||||
|
"destination": dst,
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
originalFn := fn
|
||||||
|
fn = func() error {
|
||||||
|
err := originalFn()
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: duration,
|
||||||
|
Type: trace.BackendTraceImageGeneration,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: trace.TruncateString(positive_prompt, 200),
|
||||||
|
Error: errStr,
|
||||||
|
Data: traceData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return fn, nil
|
return fn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/mudler/xlog"
|
"github.com/mudler/xlog"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
"github.com/mudler/LocalAI/core/schema"
|
"github.com/mudler/LocalAI/core/schema"
|
||||||
"github.com/mudler/LocalAI/core/services"
|
"github.com/mudler/LocalAI/core/services"
|
||||||
|
|
||||||
@@ -220,6 +222,84 @@ func ModelInference(ctx context.Context, s string, messages schema.Messages, ima
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(o.TracingMaxItems)
|
||||||
|
|
||||||
|
traceData := map[string]any{
|
||||||
|
"prompt": s,
|
||||||
|
"use_tokenizer_template": c.TemplateConfig.UseTokenizerTemplate,
|
||||||
|
"chat_template": c.TemplateConfig.Chat,
|
||||||
|
"function_template": c.TemplateConfig.Functions,
|
||||||
|
"grammar": c.Grammar,
|
||||||
|
"stop_words": c.StopWords,
|
||||||
|
"streaming": tokenCallback != nil,
|
||||||
|
"images_count": len(images),
|
||||||
|
"videos_count": len(videos),
|
||||||
|
"audios_count": len(audios),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(messages) > 0 {
|
||||||
|
if msgJSON, err := json.Marshal(messages); err == nil {
|
||||||
|
traceData["messages"] = string(msgJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tools != "" {
|
||||||
|
traceData["tools"] = tools
|
||||||
|
}
|
||||||
|
if toolChoice != "" {
|
||||||
|
traceData["tool_choice"] = toolChoice
|
||||||
|
}
|
||||||
|
if reasoningJSON, err := json.Marshal(c.ReasoningConfig); err == nil {
|
||||||
|
traceData["reasoning_config"] = string(reasoningJSON)
|
||||||
|
}
|
||||||
|
traceData["functions_config"] = map[string]any{
|
||||||
|
"grammar_disabled": c.FunctionsConfig.GrammarConfig.NoGrammar,
|
||||||
|
"parallel_calls": c.FunctionsConfig.GrammarConfig.ParallelCalls,
|
||||||
|
"mixed_mode": c.FunctionsConfig.GrammarConfig.MixedMode,
|
||||||
|
"xml_format_preset": c.FunctionsConfig.XMLFormatPreset,
|
||||||
|
}
|
||||||
|
if c.Temperature != nil {
|
||||||
|
traceData["temperature"] = *c.Temperature
|
||||||
|
}
|
||||||
|
if c.TopP != nil {
|
||||||
|
traceData["top_p"] = *c.TopP
|
||||||
|
}
|
||||||
|
if c.Maxtokens != nil {
|
||||||
|
traceData["max_tokens"] = *c.Maxtokens
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
originalFn := fn
|
||||||
|
fn = func() (LLMResponse, error) {
|
||||||
|
resp, err := originalFn()
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
traceData["response"] = resp.Response
|
||||||
|
traceData["token_usage"] = map[string]any{
|
||||||
|
"prompt": resp.Usage.Prompt,
|
||||||
|
"completion": resp.Usage.Completion,
|
||||||
|
}
|
||||||
|
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: duration,
|
||||||
|
Type: trace.BackendTraceLLM,
|
||||||
|
ModelName: c.Name,
|
||||||
|
Backend: c.Backend,
|
||||||
|
Summary: trace.GenerateLLMSummary(messages, s),
|
||||||
|
Error: errStr,
|
||||||
|
Data: traceData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return fn, nil
|
return fn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package backend
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||||
model "github.com/mudler/LocalAI/pkg/model"
|
model "github.com/mudler/LocalAI/pkg/model"
|
||||||
)
|
)
|
||||||
@@ -20,7 +22,35 @@ func Rerank(request *proto.RerankRequest, loader *model.ModelLoader, appConfig *
|
|||||||
return nil, fmt.Errorf("could not load rerank model")
|
return nil, fmt.Errorf("could not load rerank model")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var startTime time.Time
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
startTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
res, err := rerankModel.Rerank(context.Background(), request)
|
res, err := rerankModel.Rerank(context.Background(), request)
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
Type: trace.BackendTraceRerank,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: trace.TruncateString(request.Query, 200),
|
||||||
|
Error: errStr,
|
||||||
|
Data: map[string]any{
|
||||||
|
"query": request.Query,
|
||||||
|
"documents_count": len(request.Documents),
|
||||||
|
"top_n": request.TopN,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||||
"github.com/mudler/LocalAI/pkg/model"
|
"github.com/mudler/LocalAI/pkg/model"
|
||||||
"github.com/mudler/LocalAI/pkg/utils"
|
"github.com/mudler/LocalAI/pkg/utils"
|
||||||
@@ -92,7 +94,51 @@ func SoundGeneration(
|
|||||||
req.Instrumental = instrumental
|
req.Instrumental = instrumental
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var startTime time.Time
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
startTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
res, err := soundGenModel.SoundGeneration(context.Background(), req)
|
res, err := soundGenModel.SoundGeneration(context.Background(), req)
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
} else if res != nil && !res.Success {
|
||||||
|
errStr = fmt.Sprintf("sound generation error: %s", res.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := trace.TruncateString(text, 200)
|
||||||
|
if summary == "" && caption != "" {
|
||||||
|
summary = trace.TruncateString(caption, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
traceData := map[string]any{
|
||||||
|
"text": text,
|
||||||
|
"caption": caption,
|
||||||
|
"lyrics": lyrics,
|
||||||
|
}
|
||||||
|
if duration != nil {
|
||||||
|
traceData["duration"] = *duration
|
||||||
|
}
|
||||||
|
if temperature != nil {
|
||||||
|
traceData["temperature"] = *temperature
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
Type: trace.BackendTraceSoundGeneration,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: summary,
|
||||||
|
Error: errStr,
|
||||||
|
Data: traceData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
"github.com/mudler/LocalAI/core/schema"
|
"github.com/mudler/LocalAI/core/schema"
|
||||||
"github.com/mudler/LocalAI/pkg/grpc"
|
"github.com/mudler/LocalAI/pkg/grpc"
|
||||||
"github.com/mudler/LocalAI/pkg/model"
|
"github.com/mudler/LocalAI/pkg/model"
|
||||||
@@ -21,8 +24,41 @@ func ModelTokenize(s string, loader *model.ModelLoader, modelConfig config.Model
|
|||||||
predictOptions := gRPCPredictOpts(modelConfig, loader.ModelPath)
|
predictOptions := gRPCPredictOpts(modelConfig, loader.ModelPath)
|
||||||
predictOptions.Prompt = s
|
predictOptions.Prompt = s
|
||||||
|
|
||||||
|
var startTime time.Time
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
startTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
// tokenize the string
|
// tokenize the string
|
||||||
resp, err := inferenceModel.TokenizeString(appConfig.Context, predictOptions)
|
resp, err := inferenceModel.TokenizeString(appConfig.Context, predictOptions)
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenCount := 0
|
||||||
|
if resp.Tokens != nil {
|
||||||
|
tokenCount = len(resp.Tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
Type: trace.BackendTraceTokenize,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: trace.TruncateString(s, 200),
|
||||||
|
Error: errStr,
|
||||||
|
Data: map[string]any{
|
||||||
|
"input_text": trace.TruncateString(s, 1000),
|
||||||
|
"token_count": tokenCount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return schema.TokenizeResponse{}, err
|
return schema.TokenizeResponse{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
"github.com/mudler/LocalAI/core/schema"
|
"github.com/mudler/LocalAI/core/schema"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||||
@@ -28,6 +29,12 @@ func ModelTranscription(audio, language string, translate, diarize bool, prompt
|
|||||||
return nil, fmt.Errorf("could not load transcription model")
|
return nil, fmt.Errorf("could not load transcription model")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var startTime time.Time
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
startTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
r, err := transcriptionModel.AudioTranscription(context.Background(), &proto.TranscriptRequest{
|
r, err := transcriptionModel.AudioTranscription(context.Background(), &proto.TranscriptRequest{
|
||||||
Dst: audio,
|
Dst: audio,
|
||||||
Language: language,
|
Language: language,
|
||||||
@@ -37,6 +44,24 @@ func ModelTranscription(audio, language string, translate, diarize bool, prompt
|
|||||||
Prompt: prompt,
|
Prompt: prompt,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
Type: trace.BackendTraceTranscription,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: trace.TruncateString(audio, 200),
|
||||||
|
Error: err.Error(),
|
||||||
|
Data: map[string]any{
|
||||||
|
"audio_file": audio,
|
||||||
|
"language": language,
|
||||||
|
"translate": translate,
|
||||||
|
"diarize": diarize,
|
||||||
|
"prompt": prompt,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tr := &schema.TranscriptionResult{
|
tr := &schema.TranscriptionResult{
|
||||||
@@ -57,5 +82,26 @@ func ModelTranscription(audio, language string, translate, diarize bool, prompt
|
|||||||
Speaker: s.Speaker,
|
Speaker: s.Speaker,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
Type: trace.BackendTraceTranscription,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: trace.TruncateString(audio+" -> "+tr.Text, 200),
|
||||||
|
Data: map[string]any{
|
||||||
|
"audio_file": audio,
|
||||||
|
"language": language,
|
||||||
|
"translate": translate,
|
||||||
|
"diarize": diarize,
|
||||||
|
"prompt": prompt,
|
||||||
|
"result_text": tr.Text,
|
||||||
|
"segments_count": len(tr.Segments),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return tr, err
|
return tr, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
laudio "github.com/mudler/LocalAI/pkg/audio"
|
laudio "github.com/mudler/LocalAI/pkg/audio"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||||
@@ -60,6 +62,12 @@ func ModelTTS(
|
|||||||
modelPath = modelConfig.Model // skip this step if it fails?????
|
modelPath = modelConfig.Model // skip this step if it fails?????
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var startTime time.Time
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
startTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
res, err := ttsModel.TTS(context.Background(), &proto.TTSRequest{
|
res, err := ttsModel.TTS(context.Background(), &proto.TTSRequest{
|
||||||
Text: text,
|
Text: text,
|
||||||
Model: modelPath,
|
Model: modelPath,
|
||||||
@@ -67,6 +75,31 @@ func ModelTTS(
|
|||||||
Dst: filePath,
|
Dst: filePath,
|
||||||
Language: &language,
|
Language: &language,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
} else if !res.Success {
|
||||||
|
errStr = fmt.Sprintf("TTS error: %s", res.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
Type: trace.BackendTraceTTS,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: trace.TruncateString(text, 200),
|
||||||
|
Error: errStr,
|
||||||
|
Data: map[string]any{
|
||||||
|
"text": text,
|
||||||
|
"voice": voice,
|
||||||
|
"language": language,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
@@ -115,6 +148,12 @@ func ModelTTSStream(
|
|||||||
modelPath = modelConfig.Model // skip this step if it fails?????
|
modelPath = modelConfig.Model // skip this step if it fails?????
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var startTime time.Time
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
startTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
var sampleRate uint32 = 16000 // default
|
var sampleRate uint32 = 16000 // default
|
||||||
headerSent := false
|
headerSent := false
|
||||||
var callbackErr error
|
var callbackErr error
|
||||||
@@ -171,6 +210,34 @@ func ModelTTSStream(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
resultErr := err
|
||||||
|
if callbackErr != nil {
|
||||||
|
resultErr = callbackErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
errStr := ""
|
||||||
|
if resultErr != nil {
|
||||||
|
errStr = resultErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: time.Since(startTime),
|
||||||
|
Type: trace.BackendTraceTTS,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: trace.TruncateString(text, 200),
|
||||||
|
Error: errStr,
|
||||||
|
Data: map[string]any{
|
||||||
|
"text": text,
|
||||||
|
"voice": voice,
|
||||||
|
"language": language,
|
||||||
|
"streaming": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if callbackErr != nil {
|
if callbackErr != nil {
|
||||||
return callbackErr
|
return callbackErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
|
|
||||||
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
"github.com/mudler/LocalAI/pkg/grpc/proto"
|
||||||
model "github.com/mudler/LocalAI/pkg/model"
|
model "github.com/mudler/LocalAI/pkg/model"
|
||||||
@@ -37,5 +40,46 @@ func VideoGeneration(height, width int32, prompt, negativePrompt, startImage, en
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if appConfig.EnableTracing {
|
||||||
|
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
|
||||||
|
|
||||||
|
traceData := map[string]any{
|
||||||
|
"prompt": prompt,
|
||||||
|
"negative_prompt": negativePrompt,
|
||||||
|
"height": height,
|
||||||
|
"width": width,
|
||||||
|
"num_frames": numFrames,
|
||||||
|
"fps": fps,
|
||||||
|
"seed": seed,
|
||||||
|
"cfg_scale": cfgScale,
|
||||||
|
"step": step,
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
originalFn := fn
|
||||||
|
fn = func() error {
|
||||||
|
err := originalFn()
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.RecordBackendTrace(trace.BackendTrace{
|
||||||
|
Timestamp: startTime,
|
||||||
|
Duration: duration,
|
||||||
|
Type: trace.BackendTraceVideoGeneration,
|
||||||
|
ModelName: modelConfig.Name,
|
||||||
|
Backend: modelConfig.Backend,
|
||||||
|
Summary: trace.TruncateString(prompt, 200),
|
||||||
|
Error: errStr,
|
||||||
|
Data: traceData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return fn, nil
|
return fn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ type RunCMD struct {
|
|||||||
WatchdogIdleTimeout string `env:"LOCALAI_WATCHDOG_IDLE_TIMEOUT,WATCHDOG_IDLE_TIMEOUT" default:"15m" help:"Threshold beyond which an idle backend should be stopped" group:"backends"`
|
WatchdogIdleTimeout string `env:"LOCALAI_WATCHDOG_IDLE_TIMEOUT,WATCHDOG_IDLE_TIMEOUT" default:"15m" help:"Threshold beyond which an idle backend should be stopped" group:"backends"`
|
||||||
EnableWatchdogBusy bool `env:"LOCALAI_WATCHDOG_BUSY,WATCHDOG_BUSY" default:"false" help:"Enable watchdog for stopping backends that are busy longer than the watchdog-busy-timeout" group:"backends"`
|
EnableWatchdogBusy bool `env:"LOCALAI_WATCHDOG_BUSY,WATCHDOG_BUSY" default:"false" help:"Enable watchdog for stopping backends that are busy longer than the watchdog-busy-timeout" group:"backends"`
|
||||||
WatchdogBusyTimeout string `env:"LOCALAI_WATCHDOG_BUSY_TIMEOUT,WATCHDOG_BUSY_TIMEOUT" default:"5m" help:"Threshold beyond which a busy backend should be stopped" group:"backends"`
|
WatchdogBusyTimeout string `env:"LOCALAI_WATCHDOG_BUSY_TIMEOUT,WATCHDOG_BUSY_TIMEOUT" default:"5m" help:"Threshold beyond which a busy backend should be stopped" group:"backends"`
|
||||||
|
WatchdogInterval string `env:"LOCALAI_WATCHDOG_INTERVAL,WATCHDOG_INTERVAL" default:"500ms" help:"Interval between watchdog checks (e.g., 500ms, 5s, 1m) (default: 500ms)" group:"backends"`
|
||||||
EnableMemoryReclaimer bool `env:"LOCALAI_MEMORY_RECLAIMER,MEMORY_RECLAIMER,LOCALAI_GPU_RECLAIMER,GPU_RECLAIMER" default:"false" help:"Enable memory threshold monitoring to auto-evict backends when memory usage exceeds threshold (uses GPU VRAM if available, otherwise RAM)" group:"backends"`
|
EnableMemoryReclaimer bool `env:"LOCALAI_MEMORY_RECLAIMER,MEMORY_RECLAIMER,LOCALAI_GPU_RECLAIMER,GPU_RECLAIMER" default:"false" help:"Enable memory threshold monitoring to auto-evict backends when memory usage exceeds threshold (uses GPU VRAM if available, otherwise RAM)" group:"backends"`
|
||||||
MemoryReclaimerThreshold float64 `env:"LOCALAI_MEMORY_RECLAIMER_THRESHOLD,MEMORY_RECLAIMER_THRESHOLD,LOCALAI_GPU_RECLAIMER_THRESHOLD,GPU_RECLAIMER_THRESHOLD" default:"0.95" help:"Memory usage threshold (0.0-1.0) that triggers backend eviction (default 0.95 = 95%%)" group:"backends"`
|
MemoryReclaimerThreshold float64 `env:"LOCALAI_MEMORY_RECLAIMER_THRESHOLD,MEMORY_RECLAIMER_THRESHOLD,LOCALAI_GPU_RECLAIMER_THRESHOLD,GPU_RECLAIMER_THRESHOLD" default:"0.95" help:"Memory usage threshold (0.0-1.0) that triggers backend eviction (default 0.95 = 95%%)" group:"backends"`
|
||||||
ForceEvictionWhenBusy bool `env:"LOCALAI_FORCE_EVICTION_WHEN_BUSY,FORCE_EVICTION_WHEN_BUSY" default:"false" help:"Force eviction even when models have active API calls (default: false for safety)" group:"backends"`
|
ForceEvictionWhenBusy bool `env:"LOCALAI_FORCE_EVICTION_WHEN_BUSY,FORCE_EVICTION_WHEN_BUSY" default:"false" help:"Force eviction even when models have active API calls (default: false for safety)" group:"backends"`
|
||||||
@@ -83,7 +84,7 @@ type RunCMD struct {
|
|||||||
EnableTracing bool `env:"LOCALAI_ENABLE_TRACING,ENABLE_TRACING" help:"Enable API tracing" group:"api"`
|
EnableTracing bool `env:"LOCALAI_ENABLE_TRACING,ENABLE_TRACING" help:"Enable API tracing" group:"api"`
|
||||||
TracingMaxItems int `env:"LOCALAI_TRACING_MAX_ITEMS" default:"1024" help:"Maximum number of traces to keep" group:"api"`
|
TracingMaxItems int `env:"LOCALAI_TRACING_MAX_ITEMS" default:"1024" help:"Maximum number of traces to keep" group:"api"`
|
||||||
AgentJobRetentionDays int `env:"LOCALAI_AGENT_JOB_RETENTION_DAYS,AGENT_JOB_RETENTION_DAYS" default:"30" help:"Number of days to keep agent job history (default: 30)" group:"api"`
|
AgentJobRetentionDays int `env:"LOCALAI_AGENT_JOB_RETENTION_DAYS,AGENT_JOB_RETENTION_DAYS" default:"30" help:"Number of days to keep agent job history (default: 30)" group:"api"`
|
||||||
OpenResponsesStoreTTL string `env:"LOCALAI_OPEN_RESPONSES_STORE_TTL,OPEN_RESPONSES_STORE_TTL" default:"0" help:"TTL for Open Responses store (e.g., 1h, 30m, 0 = no expiration)" group:"api"`
|
OpenResponsesStoreTTL string `env:"LOCALAI_OPEN_RESPONSES_STORE_TTL,OPEN_RESPONSES_STORE_TTL" default:"0" help:"TTL for Open Responses store (e.g., 1h, 30m, 0 = no expiration)" group:"api"`
|
||||||
|
|
||||||
Version bool
|
Version bool
|
||||||
}
|
}
|
||||||
@@ -215,6 +216,13 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
|
|||||||
}
|
}
|
||||||
opts = append(opts, config.SetWatchDogBusyTimeout(dur))
|
opts = append(opts, config.SetWatchDogBusyTimeout(dur))
|
||||||
}
|
}
|
||||||
|
if r.WatchdogInterval != "" {
|
||||||
|
dur, err := time.ParseDuration(r.WatchdogInterval)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opts = append(opts, config.SetWatchDogInterval(dur))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle memory reclaimer (uses GPU VRAM if available, otherwise RAM)
|
// Handle memory reclaimer (uses GPU VRAM if available, otherwise RAM)
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ type TranscriptCMD struct {
|
|||||||
ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"`
|
ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"`
|
||||||
BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"`
|
BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"`
|
||||||
Prompt string `short:"p" help:"Previous transcribed text or words that hint at what the model should expect"`
|
Prompt string `short:"p" help:"Previous transcribed text or words that hint at what the model should expect"`
|
||||||
ResponseFormat schema.TranscriptionResponseFormatType `short:"f" default:"" help:"Response format for Whisper models, can be one of (txt, lrc, srt, vtt, json, json_verbose)"`
|
ResponseFormat schema.TranscriptionResponseFormatType `short:"f" default:"" help:"Response format for Whisper models, can be one of (txt, lrc, srt, vtt, json, verbose_json)"`
|
||||||
PrettyPrint bool `help:"Used with response_format json or json_verbose for pretty printing"`
|
PrettyPrint bool `help:"Used with response_format json or verbose_json for pretty printing"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TranscriptCMD) Run(ctx *cliContext.Context) error {
|
func (t *TranscriptCMD) Run(ctx *cliContext.Context) error {
|
||||||
|
|||||||
@@ -98,10 +98,11 @@ func NewApplicationConfig(o ...AppOption) *ApplicationConfig {
|
|||||||
Context: context.Background(),
|
Context: context.Background(),
|
||||||
UploadLimitMB: 15,
|
UploadLimitMB: 15,
|
||||||
Debug: true,
|
Debug: true,
|
||||||
AgentJobRetentionDays: 30, // Default: 30 days
|
AgentJobRetentionDays: 30, // Default: 30 days
|
||||||
LRUEvictionMaxRetries: 30, // Default: 30 retries
|
LRUEvictionMaxRetries: 30, // Default: 30 retries
|
||||||
LRUEvictionRetryInterval: 1 * time.Second, // Default: 1 second
|
LRUEvictionRetryInterval: 1 * time.Second, // Default: 1 second
|
||||||
TracingMaxItems: 1024,
|
WatchDogInterval: 500 * time.Millisecond, // Default: 500ms
|
||||||
|
TracingMaxItems: 1024,
|
||||||
PathWithoutAuth: []string{
|
PathWithoutAuth: []string{
|
||||||
"/static/",
|
"/static/",
|
||||||
"/generated-audio/",
|
"/generated-audio/",
|
||||||
@@ -208,6 +209,12 @@ func SetWatchDogIdleTimeout(t time.Duration) AppOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetWatchDogInterval(t time.Duration) AppOption {
|
||||||
|
return func(o *ApplicationConfig) {
|
||||||
|
o.WatchDogInterval = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// EnableMemoryReclaimer enables memory threshold monitoring.
|
// EnableMemoryReclaimer enables memory threshold monitoring.
|
||||||
// When enabled, the watchdog will evict backends if memory usage exceeds the threshold.
|
// When enabled, the watchdog will evict backends if memory usage exceeds the threshold.
|
||||||
// Works with GPU VRAM if available, otherwise uses system RAM.
|
// Works with GPU VRAM if available, otherwise uses system RAM.
|
||||||
@@ -642,7 +649,7 @@ func (o *ApplicationConfig) ToRuntimeSettings() RuntimeSettings {
|
|||||||
AutoloadBackendGalleries: &autoloadBackendGalleries,
|
AutoloadBackendGalleries: &autoloadBackendGalleries,
|
||||||
ApiKeys: &apiKeys,
|
ApiKeys: &apiKeys,
|
||||||
AgentJobRetentionDays: &agentJobRetentionDays,
|
AgentJobRetentionDays: &agentJobRetentionDays,
|
||||||
OpenResponsesStoreTTL: &openResponsesStoreTTL,
|
OpenResponsesStoreTTL: &openResponsesStoreTTL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ type AgentConfig struct {
|
|||||||
EnablePlanning bool `yaml:"enable_planning,omitempty" json:"enable_planning,omitempty"`
|
EnablePlanning bool `yaml:"enable_planning,omitempty" json:"enable_planning,omitempty"`
|
||||||
EnableMCPPrompts bool `yaml:"enable_mcp_prompts,omitempty" json:"enable_mcp_prompts,omitempty"`
|
EnableMCPPrompts bool `yaml:"enable_mcp_prompts,omitempty" json:"enable_mcp_prompts,omitempty"`
|
||||||
EnablePlanReEvaluator bool `yaml:"enable_plan_re_evaluator,omitempty" json:"enable_plan_re_evaluator,omitempty"`
|
EnablePlanReEvaluator bool `yaml:"enable_plan_re_evaluator,omitempty" json:"enable_plan_re_evaluator,omitempty"`
|
||||||
|
DisableSinkState bool `yaml:"disable_sink_state,omitempty" json:"disable_sink_state,omitempty"`
|
||||||
|
LoopDetection int `yaml:"loop_detection,omitempty" json:"loop_detection,omitempty"`
|
||||||
|
MaxAdjustmentAttempts int `yaml:"max_adjustment_attempts,omitempty" json:"max_adjustment_attempts,omitempty"`
|
||||||
|
ForceReasoningTool bool `yaml:"force_reasoning_tool,omitempty" json:"force_reasoning_tool,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *MCPConfig) MCPConfigFromYAML() (MCPGenericConfig[MCPRemoteServers], MCPGenericConfig[MCPSTDIOServers], error) {
|
func (c *MCPConfig) MCPConfigFromYAML() (MCPGenericConfig[MCPRemoteServers], MCPGenericConfig[MCPSTDIOServers], error) {
|
||||||
@@ -704,7 +708,7 @@ func (c *ModelConfig) BuildCogitoOptions() []cogito.Option {
|
|||||||
|
|
||||||
// Apply agent configuration options
|
// Apply agent configuration options
|
||||||
if c.Agent.EnableReasoning {
|
if c.Agent.EnableReasoning {
|
||||||
cogitoOpts = append(cogitoOpts, cogito.EnableToolReasoner)
|
cogitoOpts = append(cogitoOpts, cogito.WithForceReasoning())
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Agent.EnablePlanning {
|
if c.Agent.EnablePlanning {
|
||||||
@@ -727,5 +731,21 @@ func (c *ModelConfig) BuildCogitoOptions() []cogito.Option {
|
|||||||
cogitoOpts = append(cogitoOpts, cogito.WithMaxAttempts(c.Agent.MaxAttempts))
|
cogitoOpts = append(cogitoOpts, cogito.WithMaxAttempts(c.Agent.MaxAttempts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Agent.DisableSinkState {
|
||||||
|
cogitoOpts = append(cogitoOpts, cogito.DisableSinkState)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Agent.LoopDetection != 0 {
|
||||||
|
cogitoOpts = append(cogitoOpts, cogito.WithLoopDetection(c.Agent.LoopDetection))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Agent.MaxAdjustmentAttempts != 0 {
|
||||||
|
cogitoOpts = append(cogitoOpts, cogito.WithMaxAdjustmentAttempts(c.Agent.MaxAdjustmentAttempts))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Agent.ForceReasoningTool {
|
||||||
|
cogitoOpts = append(cogitoOpts, cogito.WithForceReasoningTool())
|
||||||
|
}
|
||||||
|
|
||||||
return cogitoOpts
|
return cogitoOpts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,39 @@ const (
|
|||||||
runFile = "run.sh"
|
runFile = "run.sh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Environment variables for configurable fallback URI patterns
|
||||||
|
const (
|
||||||
|
// Default fallback tag values
|
||||||
|
defaultLatestTag = "latest"
|
||||||
|
defaultMasterTag = "master"
|
||||||
|
defaultDevSuffix = "development"
|
||||||
|
|
||||||
|
// Environment variable names
|
||||||
|
envLatestTag = "LOCALAI_BACKEND_IMAGES_RELEASE_TAG"
|
||||||
|
envMasterTag = "LOCALAI_BACKEND_IMAGES_BRANCH_TAG"
|
||||||
|
envDevSuffix = "LOCALAI_BACKEND_DEV_SUFFIX"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getFallbackTagValues returns the configurable fallback tag values from environment variables
|
||||||
|
func getFallbackTagValues() (latestTag, masterTag, devSuffix string) {
|
||||||
|
latestTag = os.Getenv(envLatestTag)
|
||||||
|
masterTag = os.Getenv(envMasterTag)
|
||||||
|
devSuffix = os.Getenv(envDevSuffix)
|
||||||
|
|
||||||
|
// Use defaults if environment variables are not set
|
||||||
|
if latestTag == "" {
|
||||||
|
latestTag = defaultLatestTag
|
||||||
|
}
|
||||||
|
if masterTag == "" {
|
||||||
|
masterTag = defaultMasterTag
|
||||||
|
}
|
||||||
|
if devSuffix == "" {
|
||||||
|
devSuffix = defaultDevSuffix
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestTag, masterTag, devSuffix
|
||||||
|
}
|
||||||
|
|
||||||
// backendCandidate represents an installed concrete backend option for a given alias
|
// backendCandidate represents an installed concrete backend option for a given alias
|
||||||
type backendCandidate struct {
|
type backendCandidate struct {
|
||||||
name string
|
name string
|
||||||
@@ -139,6 +172,9 @@ func InstallBackendFromGallery(ctx context.Context, galleries []config.Gallery,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func InstallBackend(ctx context.Context, systemState *system.SystemState, modelLoader *model.ModelLoader, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error {
|
func InstallBackend(ctx context.Context, systemState *system.SystemState, modelLoader *model.ModelLoader, config *GalleryBackend, downloadStatus func(string, string, string, float64)) error {
|
||||||
|
// Get configurable fallback tag values from environment variables
|
||||||
|
latestTag, masterTag, devSuffix := getFallbackTagValues()
|
||||||
|
|
||||||
// Create base path if it doesn't exist
|
// Create base path if it doesn't exist
|
||||||
err := os.MkdirAll(systemState.Backend.BackendsPath, 0750)
|
err := os.MkdirAll(systemState.Backend.BackendsPath, 0750)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -166,6 +202,12 @@ func InstallBackend(ctx context.Context, systemState *system.SystemState, modelL
|
|||||||
} else {
|
} else {
|
||||||
xlog.Debug("Downloading backend", "uri", config.URI, "backendPath", backendPath)
|
xlog.Debug("Downloading backend", "uri", config.URI, "backendPath", backendPath)
|
||||||
if err := uri.DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err != nil {
|
if err := uri.DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err != nil {
|
||||||
|
// Clean up the partially downloaded backend directory on failure
|
||||||
|
xlog.Debug("Backend download failed, cleaning up", "backendPath", backendPath, "error", err)
|
||||||
|
if cleanupErr := os.RemoveAll(backendPath); cleanupErr != nil {
|
||||||
|
xlog.Warn("Failed to clean up backend directory", "backendPath", backendPath, "error", cleanupErr)
|
||||||
|
}
|
||||||
|
|
||||||
success := false
|
success := false
|
||||||
// Try to download from mirrors
|
// Try to download from mirrors
|
||||||
for _, mirror := range config.Mirrors {
|
for _, mirror := range config.Mirrors {
|
||||||
@@ -182,6 +224,36 @@ func InstallBackend(ctx context.Context, systemState *system.SystemState, modelL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try fallback: replace latestTag + "-" with masterTag + "-" in the URI
|
||||||
|
fallbackURI := strings.Replace(string(config.URI), latestTag + "-", masterTag + "-", 1)
|
||||||
|
if fallbackURI != string(config.URI) {
|
||||||
|
xlog.Debug("Trying fallback URI", "original", config.URI, "fallback", fallbackURI)
|
||||||
|
if err := downloader.URI(fallbackURI).DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err == nil {
|
||||||
|
xlog.Debug("Downloaded backend using fallback URI", "uri", fallbackURI, "backendPath", backendPath)
|
||||||
|
success = true
|
||||||
|
} else {
|
||||||
|
// Try another fallback: add "-" + devSuffix suffix to the backend name
|
||||||
|
// For example: master-gpu-nvidia-cuda-13-ace-step -> master-gpu-nvidia-cuda-13-ace-step-development
|
||||||
|
if !strings.Contains(fallbackURI, "-" + devSuffix) {
|
||||||
|
// Extract backend name from URI and add -development
|
||||||
|
parts := strings.Split(fallbackURI, "-")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
// Find where the backend name ends (usually the last part before the tag)
|
||||||
|
// Pattern: quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-13-ace-step
|
||||||
|
lastDash := strings.LastIndex(fallbackURI, "-")
|
||||||
|
if lastDash > 0 {
|
||||||
|
devFallbackURI := fallbackURI[:lastDash] + "-" + devSuffix
|
||||||
|
xlog.Debug("Trying development fallback URI", "fallback", devFallbackURI)
|
||||||
|
if err := downloader.URI(devFallbackURI).DownloadFileWithContext(ctx, backendPath, "", 1, 1, downloadStatus); err == nil {
|
||||||
|
xlog.Debug("Downloaded backend using development fallback URI", "uri", devFallbackURI, "backendPath", backendPath)
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !success {
|
if !success {
|
||||||
xlog.Error("Failed to download backend", "uri", config.URI, "backendPath", backendPath, "error", err)
|
xlog.Error("Failed to download backend", "uri", config.URI, "backendPath", backendPath, "error", err)
|
||||||
return fmt.Errorf("failed to download backend %q: %v", config.URI, err)
|
return fmt.Errorf("failed to download backend %q: %v", config.URI, err)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/mudler/LocalAI/pkg/system"
|
"github.com/mudler/LocalAI/pkg/system"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/mudler/LocalAI/pkg/xsync"
|
"github.com/mudler/LocalAI/pkg/xsync"
|
||||||
"github.com/mudler/xlog"
|
"github.com/mudler/xlog"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetGalleryConfigFromURL[T any](url string, basePath string) (T, error) {
|
func GetGalleryConfigFromURL[T any](url string, basePath string) (T, error) {
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"dario.cat/mergo"
|
||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
. "github.com/mudler/LocalAI/core/gallery"
|
. "github.com/mudler/LocalAI/core/gallery"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("Gallery", func() {
|
var _ = Describe("Gallery", func() {
|
||||||
@@ -462,4 +463,60 @@ var _ = Describe("Gallery", func() {
|
|||||||
Expect(result).To(BeNil())
|
Expect(result).To(BeNil())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("YAML merge with nested maps", func() {
|
||||||
|
It("should handle YAML anchors and merges with nested overrides (regression test for nanbeige4.1)", func() {
|
||||||
|
// This tests the fix for the panic that occurred with yaml.v2:
|
||||||
|
// yaml.v2 produces map[interface{}]interface{} for nested maps
|
||||||
|
// which caused mergo.Merge to panic with "value of type interface {} is not assignable to type string"
|
||||||
|
// The exact YAML structure from gallery/index.yaml nanbeige4.1 entries
|
||||||
|
yamlContent := `---
|
||||||
|
- &nanbeige4
|
||||||
|
name: "nanbeige4.1-3b-q8"
|
||||||
|
overrides:
|
||||||
|
parameters:
|
||||||
|
model: nanbeige4.1-3b-q8_0.gguf
|
||||||
|
- !!merge <<: *nanbeige4
|
||||||
|
name: "nanbeige4.1-3b-q4"
|
||||||
|
overrides:
|
||||||
|
parameters:
|
||||||
|
model: nanbeige4.1-3b-q4_k_m.gguf
|
||||||
|
`
|
||||||
|
var models []GalleryModel
|
||||||
|
err := yaml.Unmarshal([]byte(yamlContent), &models)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(models).To(HaveLen(2))
|
||||||
|
|
||||||
|
// Verify first model
|
||||||
|
Expect(models[0].Name).To(Equal("nanbeige4.1-3b-q8"))
|
||||||
|
Expect(models[0].Overrides).NotTo(BeNil())
|
||||||
|
Expect(models[0].Overrides["parameters"]).To(BeAssignableToTypeOf(map[string]interface{}{}))
|
||||||
|
params := models[0].Overrides["parameters"].(map[string]interface{})
|
||||||
|
Expect(params["model"]).To(Equal("nanbeige4.1-3b-q8_0.gguf"))
|
||||||
|
|
||||||
|
// Verify second model (merged)
|
||||||
|
Expect(models[1].Name).To(Equal("nanbeige4.1-3b-q4"))
|
||||||
|
Expect(models[1].Overrides).NotTo(BeNil())
|
||||||
|
Expect(models[1].Overrides["parameters"]).To(BeAssignableToTypeOf(map[string]interface{}{}))
|
||||||
|
params = models[1].Overrides["parameters"].(map[string]interface{})
|
||||||
|
Expect(params["model"]).To(Equal("nanbeige4.1-3b-q4_k_m.gguf"))
|
||||||
|
|
||||||
|
// Simulate the mergo.Merge call that was failing in models.go:251
|
||||||
|
// This should not panic with yaml.v3
|
||||||
|
configMap := make(map[string]interface{})
|
||||||
|
configMap["name"] = "test"
|
||||||
|
configMap["backend"] = "llama-cpp"
|
||||||
|
configMap["parameters"] = map[string]interface{}{
|
||||||
|
"model": "original.gguf",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mergo.Merge(&configMap, models[1].Overrides, mergo.WithOverride)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(configMap["parameters"]).NotTo(BeNil())
|
||||||
|
|
||||||
|
// Verify the merge worked correctly
|
||||||
|
mergedParams := configMap["parameters"].(map[string]interface{})
|
||||||
|
Expect(mergedParams["model"]).To(Equal("nanbeige4.1-3b-q4_k_m.gguf"))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
|
|||||||
return nil, fmt.Errorf("failed to create parent directory for prompt template %q: %v", template.Name, err)
|
return nil, fmt.Errorf("failed to create parent directory for prompt template %q: %v", template.Name, err)
|
||||||
}
|
}
|
||||||
// Create and write file content
|
// Create and write file content
|
||||||
err = os.WriteFile(filePath, []byte(template.Content), 0600)
|
err = os.WriteFile(filePath, []byte(template.Content), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write prompt template %q: %v", template.Name, err)
|
return nil, fmt.Errorf("failed to write prompt template %q: %v", template.Name, err)
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
|
|||||||
return nil, fmt.Errorf("failed to validate updated config YAML: %v", err)
|
return nil, fmt.Errorf("failed to validate updated config YAML: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(configFilePath, updatedConfigYAML, 0600)
|
err = os.WriteFile(configFilePath, updatedConfigYAML, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write updated config file: %v", err)
|
return nil, fmt.Errorf("failed to write updated config file: %v", err)
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
|
|||||||
|
|
||||||
xlog.Debug("Written gallery file", "file", modelFile)
|
xlog.Debug("Written gallery file", "file", modelFile)
|
||||||
|
|
||||||
return &modelConfig, os.WriteFile(modelFile, data, 0600)
|
return &modelConfig, os.WriteFile(modelFile, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryFileName(name string) string {
|
func galleryFileName(name string) string {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import (
|
|||||||
//go:embed static/*
|
//go:embed static/*
|
||||||
var embedDirStatic embed.FS
|
var embedDirStatic embed.FS
|
||||||
|
|
||||||
|
var quietPaths = []string{"/api/operations", "/api/resources", "/healthz", "/readyz"}
|
||||||
|
|
||||||
// @title LocalAI API
|
// @title LocalAI API
|
||||||
// @version 2.0.0
|
// @version 2.0.0
|
||||||
// @description The LocalAI Rest API.
|
// @description The LocalAI Rest API.
|
||||||
@@ -109,10 +111,17 @@ func API(application *application.Application) (*echo.Echo, error) {
|
|||||||
res := c.Response()
|
res := c.Response()
|
||||||
err := next(c)
|
err := next(c)
|
||||||
|
|
||||||
// Fix for #7989: Reduce log verbosity of Web UI polling and resources API
|
// Fix for #7989: Reduce log verbosity of Web UI polling, resources API, and health checks
|
||||||
// If the path is /api/operations or /api/resources and the request was successful (200),
|
// These paths are logged at DEBUG level (hidden by default) instead of INFO.
|
||||||
// we log it at DEBUG level (hidden by default) instead of INFO.
|
isQuietPath := false
|
||||||
if (req.URL.Path == "/api/operations" || req.URL.Path == "/api/resources") && res.Status == 200 {
|
for _, path := range quietPaths {
|
||||||
|
if req.URL.Path == path {
|
||||||
|
isQuietPath = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isQuietPath && res.Status == 200 {
|
||||||
xlog.Debug("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
xlog.Debug("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
||||||
} else {
|
} else {
|
||||||
xlog.Info("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
xlog.Info("HTTP request", "method", req.Method, "path", req.URL.Path, "status", res.Status)
|
||||||
|
|||||||
@@ -916,7 +916,7 @@ parameters:
|
|||||||
|
|
||||||
application, err := application.New(
|
application, err := application.New(
|
||||||
append(commonOpts,
|
append(commonOpts,
|
||||||
config.WithExternalBackend("transformers", os.Getenv("HUGGINGFACE_GRPC")),
|
config.WithExternalBackend("transformers", os.Getenv("TRANSFORMER_BACKEND")),
|
||||||
config.WithContext(c),
|
config.WithContext(c),
|
||||||
config.WithSystemState(systemState),
|
config.WithSystemState(systemState),
|
||||||
)...)
|
)...)
|
||||||
|
|||||||
@@ -125,13 +125,21 @@ func handleAnthropicNonStream(c echo.Context, id string, input *schema.Anthropic
|
|||||||
return sendAnthropicError(c, 500, "api_error", fmt.Sprintf("model inference failed: %v", err))
|
return sendAnthropicError(c, 500, "api_error", fmt.Sprintf("model inference failed: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
prediction, err := predFunc()
|
const maxEmptyRetries = 5
|
||||||
if err != nil {
|
var prediction backend.LLMResponse
|
||||||
xlog.Error("Anthropic prediction failed", "error", err)
|
var result string
|
||||||
return sendAnthropicError(c, 500, "api_error", fmt.Sprintf("prediction failed: %v", err))
|
for attempt := 0; attempt <= maxEmptyRetries; attempt++ {
|
||||||
|
prediction, err = predFunc()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Anthropic prediction failed", "error", err)
|
||||||
|
return sendAnthropicError(c, 500, "api_error", fmt.Sprintf("prediction failed: %v", err))
|
||||||
|
}
|
||||||
|
result = backend.Finetune(*cfg, predInput, prediction.Response)
|
||||||
|
if result != "" || !shouldUseFn {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
xlog.Warn("Anthropic: retrying prediction due to empty backend response", "attempt", attempt+1, "maxRetries", maxEmptyRetries)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := backend.Finetune(*cfg, predInput, prediction.Response)
|
|
||||||
|
|
||||||
// Check if the result contains tool calls
|
// Check if the result contains tool calls
|
||||||
toolCalls := functions.ParseFunctionCall(result, cfg.FunctionsConfig)
|
toolCalls := functions.ParseFunctionCall(result, cfg.FunctionsConfig)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/mudler/LocalAI/core/config"
|
"github.com/mudler/LocalAI/core/config"
|
||||||
httpUtils "github.com/mudler/LocalAI/core/http/middleware"
|
httpUtils "github.com/mudler/LocalAI/core/http/middleware"
|
||||||
"github.com/mudler/LocalAI/internal"
|
"github.com/mudler/LocalAI/internal"
|
||||||
|
"github.com/mudler/LocalAI/pkg/model"
|
||||||
"github.com/mudler/LocalAI/pkg/utils"
|
"github.com/mudler/LocalAI/pkg/utils"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -55,20 +56,22 @@ func GetEditModelPage(cl *config.ModelConfigLoader, appConfig *config.Applicatio
|
|||||||
|
|
||||||
// Render the edit page with the current configuration
|
// Render the edit page with the current configuration
|
||||||
templateData := struct {
|
templateData := struct {
|
||||||
Title string
|
Title string
|
||||||
ModelName string
|
ModelName string
|
||||||
Config *config.ModelConfig
|
Config *config.ModelConfig
|
||||||
ConfigJSON string
|
ConfigJSON string
|
||||||
ConfigYAML string
|
ConfigYAML string
|
||||||
BaseURL string
|
BaseURL string
|
||||||
Version string
|
Version string
|
||||||
|
DisableRuntimeSettings bool
|
||||||
}{
|
}{
|
||||||
Title: "LocalAI - Edit Model " + modelName,
|
Title: "LocalAI - Edit Model " + modelName,
|
||||||
ModelName: modelName,
|
ModelName: modelName,
|
||||||
Config: &modelConfig,
|
Config: &modelConfig,
|
||||||
ConfigYAML: string(configData),
|
ConfigYAML: string(configData),
|
||||||
BaseURL: httpUtils.BaseURL(c),
|
BaseURL: httpUtils.BaseURL(c),
|
||||||
Version: internal.PrintableVersion(),
|
Version: internal.PrintableVersion(),
|
||||||
|
DisableRuntimeSettings: appConfig.DisableRuntimeSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "views/model-editor", templateData)
|
return c.Render(http.StatusOK, "views/model-editor", templateData)
|
||||||
@@ -76,7 +79,7 @@ func GetEditModelPage(cl *config.ModelConfigLoader, appConfig *config.Applicatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EditModelEndpoint handles updating existing model configurations
|
// EditModelEndpoint handles updating existing model configurations
|
||||||
func EditModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.ApplicationConfig) echo.HandlerFunc {
|
func EditModelEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
modelName := c.Param("name")
|
modelName := c.Param("name")
|
||||||
if modelName == "" {
|
if modelName == "" {
|
||||||
@@ -172,6 +175,14 @@ func EditModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.Applicati
|
|||||||
return c.JSON(http.StatusInternalServerError, response)
|
return c.JSON(http.StatusInternalServerError, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shutdown the running model to apply new configuration (e.g., context_size)
|
||||||
|
// The model will be reloaded on the next inference request
|
||||||
|
if err := ml.ShutdownModel(modelName); err != nil {
|
||||||
|
// Log the error but don't fail the request - the config was saved successfully
|
||||||
|
// The model can still be manually reloaded or restarted
|
||||||
|
fmt.Printf("Warning: Failed to shutdown model '%s': %v\n", modelName, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Preload the model
|
// Preload the model
|
||||||
if err := cl.Preload(appConfig.SystemState.Model.ModelsPath); err != nil {
|
if err := cl.Preload(appConfig.SystemState.Model.ModelsPath); err != nil {
|
||||||
response := ModelResponse{
|
response := ModelResponse{
|
||||||
@@ -184,7 +195,7 @@ func EditModelEndpoint(cl *config.ModelConfigLoader, appConfig *config.Applicati
|
|||||||
// Return success response
|
// Return success response
|
||||||
response := ModelResponse{
|
response := ModelResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Message: fmt.Sprintf("Model '%s' updated successfully", modelName),
|
Message: fmt.Sprintf("Model '%s' updated successfully. Model has been reloaded with new configuration.", modelName),
|
||||||
Filename: configPath,
|
Filename: configPath,
|
||||||
Config: req,
|
Config: req,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package localai
|
package localai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -8,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@@ -18,6 +20,7 @@ import (
|
|||||||
"github.com/mudler/LocalAI/core/schema"
|
"github.com/mudler/LocalAI/core/schema"
|
||||||
"github.com/mudler/LocalAI/core/services"
|
"github.com/mudler/LocalAI/core/services"
|
||||||
"github.com/mudler/LocalAI/pkg/utils"
|
"github.com/mudler/LocalAI/pkg/utils"
|
||||||
|
"github.com/mudler/LocalAI/pkg/vram"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
@@ -37,6 +40,31 @@ func ImportModelURIEndpoint(cl *config.ModelConfigLoader, appConfig *config.Appl
|
|||||||
return fmt.Errorf("failed to discover model config: %w", err)
|
return fmt.Errorf("failed to discover model config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp := schema.GalleryResponse{
|
||||||
|
StatusURL: fmt.Sprintf("%smodels/jobs/%s", httpUtils.BaseURL(c), ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(modelConfig.Files) > 0 {
|
||||||
|
files := make([]vram.FileInput, 0, len(modelConfig.Files))
|
||||||
|
for _, f := range modelConfig.Files {
|
||||||
|
files = append(files, vram.FileInput{URI: f.URI, Size: 0})
|
||||||
|
}
|
||||||
|
estCtx, cancel := context.WithTimeout(c.Request().Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
opts := vram.EstimateOptions{ContextLength: 8192}
|
||||||
|
result, err := vram.Estimate(estCtx, files, opts, vram.DefaultCachedSizeResolver(), vram.DefaultCachedGGUFReader())
|
||||||
|
if err == nil {
|
||||||
|
if result.SizeBytes > 0 {
|
||||||
|
resp.EstimatedSizeBytes = result.SizeBytes
|
||||||
|
resp.EstimatedSizeDisplay = result.SizeDisplay
|
||||||
|
}
|
||||||
|
if result.VRAMBytes > 0 {
|
||||||
|
resp.EstimatedVRAMBytes = result.VRAMBytes
|
||||||
|
resp.EstimatedVRAMDisplay = result.VRAMDisplay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uuid, err := uuid.NewUUID()
|
uuid, err := uuid.NewUUID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -63,10 +91,9 @@ func ImportModelURIEndpoint(cl *config.ModelConfigLoader, appConfig *config.Appl
|
|||||||
BackendGalleries: appConfig.BackendGalleries,
|
BackendGalleries: appConfig.BackendGalleries,
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(200, schema.GalleryResponse{
|
resp.ID = uuid.String()
|
||||||
ID: uuid.String(),
|
resp.StatusURL = fmt.Sprintf("%smodels/jobs/%s", httpUtils.BaseURL(c), uuid.String())
|
||||||
StatusURL: fmt.Sprintf("%smodels/jobs/%s", httpUtils.BaseURL(c), uuid.String()),
|
return c.JSON(200, resp)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
// Build fragment from messages
|
// Build fragment from messages
|
||||||
fragment := cogito.NewEmptyFragment()
|
fragment := cogito.NewEmptyFragment()
|
||||||
for _, message := range input.Messages {
|
for _, message := range input.Messages {
|
||||||
fragment = fragment.AddMessage(message.Role, message.StringContent)
|
fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, port, err := net.SplitHostPort(appConfig.APIAddress)
|
_, port, err := net.SplitHostPort(appConfig.APIAddress)
|
||||||
@@ -162,11 +162,6 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err = defaultLLM.Ask(ctxWithCancellation, f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &schema.OpenAIResponse{
|
resp := &schema.OpenAIResponse{
|
||||||
ID: id,
|
ID: id,
|
||||||
Created: created,
|
Created: created,
|
||||||
@@ -252,17 +247,6 @@ func MCPEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get final response
|
|
||||||
f, err = defaultLLM.Ask(ctxWithCancellation, f)
|
|
||||||
if err != nil {
|
|
||||||
events <- MCPErrorEvent{
|
|
||||||
Type: "error",
|
|
||||||
Message: fmt.Sprintf("Failed to get response: %v", err),
|
|
||||||
}
|
|
||||||
ended <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream final assistant response
|
// Stream final assistant response
|
||||||
content := f.LastMessage().Content
|
content := f.LastMessage().Content
|
||||||
events <- MCPAssistantEvent{
|
events <- MCPAssistantEvent{
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ func TTSEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resample to requested sample rate if specified
|
||||||
|
if input.SampleRate > 0 {
|
||||||
|
filePath, err = utils.AudioResample(filePath, input.SampleRate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Convert generated file to target format
|
// Convert generated file to target format
|
||||||
filePath, err = utils.AudioConvert(filePath, input.Format)
|
filePath, err = utils.AudioConvert(filePath, input.Format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
}
|
}
|
||||||
responses <- initialMessage
|
responses <- initialMessage
|
||||||
|
|
||||||
result, err := handleQuestion(config, cl, req, ml, startupOptions, functionResults, result, prompt)
|
result, err := handleQuestion(config, functionResults, result, prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("error handling question", "error", err)
|
xlog.Error("error handling question", "error", err)
|
||||||
return err
|
return err
|
||||||
@@ -388,6 +388,14 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
shouldUseFn := len(input.Functions) > 0 && config.ShouldUseFunctions()
|
shouldUseFn := len(input.Functions) > 0 && config.ShouldUseFunctions()
|
||||||
strictMode := false
|
strictMode := false
|
||||||
|
|
||||||
|
xlog.Debug("Tool call routing decision",
|
||||||
|
"shouldUseFn", shouldUseFn,
|
||||||
|
"len(input.Functions)", len(input.Functions),
|
||||||
|
"len(input.Tools)", len(input.Tools),
|
||||||
|
"config.ShouldUseFunctions()", config.ShouldUseFunctions(),
|
||||||
|
"config.FunctionToCall()", config.FunctionToCall(),
|
||||||
|
)
|
||||||
|
|
||||||
for _, f := range input.Functions {
|
for _, f := range input.Functions {
|
||||||
if f.Strict {
|
if f.Strict {
|
||||||
strictMode = true
|
strictMode = true
|
||||||
@@ -648,12 +656,13 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
|
|
||||||
xlog.Debug("Thinking start token", "thinkingStartToken", thinkingStartToken, "template", template)
|
xlog.Debug("Thinking start token", "thinkingStartToken", thinkingStartToken, "template", template)
|
||||||
|
|
||||||
|
var emptyRetryNeeded bool
|
||||||
|
|
||||||
tokenCallback := func(s string, c *[]schema.Choice) {
|
tokenCallback := func(s string, c *[]schema.Choice) {
|
||||||
// Prepend thinking token if needed, then extract reasoning from the response
|
// Prepend thinking token if needed, then extract reasoning from the response
|
||||||
reasoning, s := reason.ExtractReasoningWithConfig(s, thinkingStartToken, config.ReasoningConfig)
|
reasoning, s := reason.ExtractReasoningWithConfig(s, thinkingStartToken, config.ReasoningConfig)
|
||||||
|
|
||||||
if !shouldUseFn {
|
if !shouldUseFn {
|
||||||
// no function is called, just reply and use stop as finish reason
|
|
||||||
stopReason := FinishReasonStop
|
stopReason := FinishReasonStop
|
||||||
message := &schema.Message{Role: "assistant", Content: &s}
|
message := &schema.Message{Role: "assistant", Content: &s}
|
||||||
if reasoning != "" {
|
if reasoning != "" {
|
||||||
@@ -671,9 +680,15 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case noActionsToRun:
|
case noActionsToRun:
|
||||||
result, err := handleQuestion(config, cl, input, ml, startupOptions, results, s, predInput)
|
if s == "" && textContentToReturn == "" {
|
||||||
|
xlog.Warn("Backend returned empty content in tool-calling context, will retry")
|
||||||
|
emptyRetryNeeded = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := handleQuestion(config, results, s, predInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Error("error handling question", "error", err)
|
xlog.Error("error handling question", "error", err)
|
||||||
|
emptyRetryNeeded = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,19 +760,42 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
// Echo properly supports context cancellation via c.Request().Context()
|
// Echo properly supports context cancellation via c.Request().Context()
|
||||||
// No workaround needed!
|
// No workaround needed!
|
||||||
|
|
||||||
result, tokenUsage, err := ComputeChoices(
|
const maxEmptyRetries = 5
|
||||||
input,
|
var result []schema.Choice
|
||||||
predInput,
|
var tokenUsage backend.TokenUsage
|
||||||
config,
|
var err error
|
||||||
cl,
|
|
||||||
startupOptions,
|
for attempt := 0; attempt <= maxEmptyRetries; attempt++ {
|
||||||
ml,
|
emptyRetryNeeded = false
|
||||||
tokenCallback,
|
result, tokenUsage, err = ComputeChoices(
|
||||||
nil,
|
input,
|
||||||
)
|
predInput,
|
||||||
|
config,
|
||||||
|
cl,
|
||||||
|
startupOptions,
|
||||||
|
ml,
|
||||||
|
tokenCallback,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil || !emptyRetryNeeded {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
xlog.Warn("Retrying prediction due to empty backend response", "attempt", attempt+1, "maxRetries", maxEmptyRetries)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if emptyRetryNeeded {
|
||||||
|
xlog.Warn("All retries exhausted, backend still returning empty content")
|
||||||
|
stopReason := FinishReasonStop
|
||||||
|
empty := ""
|
||||||
|
result = append(result, schema.Choice{
|
||||||
|
FinishReason: &stopReason,
|
||||||
|
Index: 0,
|
||||||
|
Message: &schema.Message{Role: "assistant", Content: &empty},
|
||||||
|
})
|
||||||
|
}
|
||||||
usage := schema.OpenAIUsage{
|
usage := schema.OpenAIUsage{
|
||||||
PromptTokens: tokenUsage.Prompt,
|
PromptTokens: tokenUsage.Prompt,
|
||||||
CompletionTokens: tokenUsage.Completion,
|
CompletionTokens: tokenUsage.Completion,
|
||||||
@@ -785,7 +823,7 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleQuestion(config *config.ModelConfig, cl *config.ModelConfigLoader, input *schema.OpenAIRequest, ml *model.ModelLoader, o *config.ApplicationConfig, funcResults []functions.FuncCallResults, result, prompt string) (string, error) {
|
func handleQuestion(config *config.ModelConfig, funcResults []functions.FuncCallResults, result, prompt string) (string, error) {
|
||||||
|
|
||||||
if len(funcResults) == 0 && result != "" {
|
if len(funcResults) == 0 && result != "" {
|
||||||
xlog.Debug("nothing function results but we had a message from the LLM")
|
xlog.Debug("nothing function results but we had a message from the LLM")
|
||||||
@@ -818,73 +856,6 @@ func handleQuestion(config *config.ModelConfig, cl *config.ModelConfigLoader, in
|
|||||||
}
|
}
|
||||||
|
|
||||||
xlog.Debug("No action received from LLM, without a message, computing a reply")
|
xlog.Debug("No action received from LLM, without a message, computing a reply")
|
||||||
// Otherwise ask the LLM to understand the JSON output and the context, and return a message
|
|
||||||
// Note: This costs (in term of CPU/GPU) another computation
|
|
||||||
config.Grammar = ""
|
|
||||||
images := []string{}
|
|
||||||
for _, m := range input.Messages {
|
|
||||||
images = append(images, m.StringImages...)
|
|
||||||
}
|
|
||||||
videos := []string{}
|
|
||||||
for _, m := range input.Messages {
|
|
||||||
videos = append(videos, m.StringVideos...)
|
|
||||||
}
|
|
||||||
audios := []string{}
|
|
||||||
for _, m := range input.Messages {
|
|
||||||
audios = append(audios, m.StringAudios...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize tools and tool_choice to JSON strings
|
return "", fmt.Errorf("no action received from LLM, without a message, computing a reply")
|
||||||
toolsJSON := ""
|
|
||||||
if len(input.Tools) > 0 {
|
|
||||||
toolsBytes, err := json.Marshal(input.Tools)
|
|
||||||
if err == nil {
|
|
||||||
toolsJSON = string(toolsBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toolChoiceJSON := ""
|
|
||||||
if input.ToolsChoice != nil {
|
|
||||||
toolChoiceBytes, err := json.Marshal(input.ToolsChoice)
|
|
||||||
if err == nil {
|
|
||||||
toolChoiceJSON = string(toolChoiceBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract logprobs from request
|
|
||||||
// According to OpenAI API: logprobs is boolean, top_logprobs (0-20) controls how many top tokens per position
|
|
||||||
var logprobs *int
|
|
||||||
var topLogprobs *int
|
|
||||||
if input.Logprobs.IsEnabled() {
|
|
||||||
// If logprobs is enabled, use top_logprobs if provided, otherwise default to 1
|
|
||||||
if input.TopLogprobs != nil {
|
|
||||||
topLogprobs = input.TopLogprobs
|
|
||||||
// For backend compatibility, set logprobs to the top_logprobs value
|
|
||||||
logprobs = input.TopLogprobs
|
|
||||||
} else {
|
|
||||||
// Default to 1 if logprobs is true but top_logprobs not specified
|
|
||||||
val := 1
|
|
||||||
logprobs = &val
|
|
||||||
topLogprobs = &val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract logit_bias from request
|
|
||||||
// According to OpenAI API: logit_bias is a map of token IDs (as strings) to bias values (-100 to 100)
|
|
||||||
var logitBias map[string]float64
|
|
||||||
if len(input.LogitBias) > 0 {
|
|
||||||
logitBias = input.LogitBias
|
|
||||||
}
|
|
||||||
|
|
||||||
predFunc, err := backend.ModelInference(input.Context, prompt, input.Messages, images, videos, audios, ml, config, cl, o, nil, toolsJSON, toolChoiceJSON, logprobs, topLogprobs, logitBias)
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("model inference failed", "error", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
prediction, err := predFunc()
|
|
||||||
if err != nil {
|
|
||||||
xlog.Error("prediction failed", "error", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return backend.Finetune(*config, prompt, prediction.Response), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,19 @@ import (
|
|||||||
model "github.com/mudler/LocalAI/pkg/model"
|
model "github.com/mudler/LocalAI/pkg/model"
|
||||||
"github.com/mudler/LocalAI/pkg/reasoning"
|
"github.com/mudler/LocalAI/pkg/reasoning"
|
||||||
"github.com/mudler/LocalAI/pkg/sound"
|
"github.com/mudler/LocalAI/pkg/sound"
|
||||||
|
"github.com/mudler/LocalAI/pkg/utils"
|
||||||
|
|
||||||
"github.com/mudler/xlog"
|
"github.com/mudler/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// XXX: Presently it seems all ASR/VAD backends use 16Khz. If a backend uses 24Khz then it will likely still work, but have reduced performance
|
// XXX: Presently it seems all ASR/VAD backends use 16Khz. If a backend uses 24Khz then it will likely still work, but have reduced performance
|
||||||
localSampleRate = 16000
|
localSampleRate = 16000
|
||||||
defaultRemoteSampleRate = 24000
|
defaultRemoteSampleRate = 24000
|
||||||
|
// Maximum audio buffer size in bytes (100MB) to prevent memory exhaustion
|
||||||
|
maxAudioBufferSize = 100 * 1024 * 1024
|
||||||
|
// Maximum WebSocket message size in bytes (10MB) to prevent DoS attacks
|
||||||
|
maxWebSocketMessageSize = 10 * 1024 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
// A model can be "emulated" that is: transcribe audio to text -> feed text to the LLM -> generate audio as result
|
// A model can be "emulated" that is: transcribe audio to text -> feed text to the LLM -> generate audio as result
|
||||||
@@ -73,6 +78,7 @@ type Session struct {
|
|||||||
// The pipeline model config or the config for an any-to-any model
|
// The pipeline model config or the config for an any-to-any model
|
||||||
ModelConfig *config.ModelConfig
|
ModelConfig *config.ModelConfig
|
||||||
InputSampleRate int
|
InputSampleRate int
|
||||||
|
MaxOutputTokens types.IntOrInf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) FromClient(session *types.SessionUnion) {
|
func (s *Session) FromClient(session *types.SessionUnion) {
|
||||||
@@ -94,12 +100,13 @@ func (s *Session) ToServer() types.SessionUnion {
|
|||||||
} else {
|
} else {
|
||||||
return types.SessionUnion{
|
return types.SessionUnion{
|
||||||
Realtime: &types.RealtimeSession{
|
Realtime: &types.RealtimeSession{
|
||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
Object: "realtime.session",
|
Object: "realtime.session",
|
||||||
Model: s.Model,
|
Model: s.Model,
|
||||||
Instructions: s.Instructions,
|
Instructions: s.Instructions,
|
||||||
Tools: s.Tools,
|
Tools: s.Tools,
|
||||||
ToolChoice: s.ToolChoice,
|
ToolChoice: s.ToolChoice,
|
||||||
|
MaxOutputTokens: s.MaxOutputTokens,
|
||||||
Audio: &types.RealtimeSessionAudio{
|
Audio: &types.RealtimeSessionAudio{
|
||||||
Input: &types.SessionAudioInput{
|
Input: &types.SessionAudioInput{
|
||||||
TurnDetection: s.TurnDetection,
|
TurnDetection: s.TurnDetection,
|
||||||
@@ -167,6 +174,9 @@ func Realtime(application *application.Application) echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
defer ws.Close()
|
defer ws.Close()
|
||||||
|
|
||||||
|
// Set maximum message size to prevent DoS attacks
|
||||||
|
ws.SetReadLimit(maxWebSocketMessageSize)
|
||||||
|
|
||||||
// Extract query parameters from Echo context before passing to websocket handler
|
// Extract query parameters from Echo context before passing to websocket handler
|
||||||
model := c.QueryParam("model")
|
model := c.QueryParam("model")
|
||||||
|
|
||||||
@@ -370,8 +380,17 @@ func registerRealtime(application *application.Application, model string) func(c
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append to InputAudioBuffer
|
// Check buffer size limits before appending
|
||||||
session.AudioBufferLock.Lock()
|
session.AudioBufferLock.Lock()
|
||||||
|
newSize := len(session.InputAudioBuffer) + len(decodedAudio)
|
||||||
|
if newSize > maxAudioBufferSize {
|
||||||
|
session.AudioBufferLock.Unlock()
|
||||||
|
xlog.Error("audio buffer size limit exceeded", "current_size", len(session.InputAudioBuffer), "incoming_size", len(decodedAudio), "limit", maxAudioBufferSize)
|
||||||
|
sendError(c, "buffer_size_exceeded", fmt.Sprintf("Audio buffer size limit exceeded (max %d bytes)", maxAudioBufferSize), "", "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append to InputAudioBuffer
|
||||||
session.InputAudioBuffer = append(session.InputAudioBuffer, decodedAudio...)
|
session.InputAudioBuffer = append(session.InputAudioBuffer, decodedAudio...)
|
||||||
session.AudioBufferLock.Unlock()
|
session.AudioBufferLock.Unlock()
|
||||||
|
|
||||||
@@ -677,6 +696,10 @@ func updateSession(session *Session, update *types.SessionUnion, cl *config.Mode
|
|||||||
session.ToolChoice = rt.ToolChoice
|
session.ToolChoice = rt.ToolChoice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rt.MaxOutputTokens != 0 {
|
||||||
|
session.MaxOutputTokens = rt.MaxOutputTokens
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,18 +755,18 @@ func handleVAD(session *Session, conv *Conversation, c *LockedWebsocket, done ch
|
|||||||
audioLength := float64(len(aints)) / localSampleRate
|
audioLength := float64(len(aints)) / localSampleRate
|
||||||
|
|
||||||
// TODO: When resetting the buffer we should retain a small postfix
|
// TODO: When resetting the buffer we should retain a small postfix
|
||||||
// TODO: The OpenAI documentation seems to suggest that only the client decides when to clear the buffer
|
|
||||||
if len(segments) == 0 && audioLength > silenceThreshold {
|
if len(segments) == 0 && audioLength > silenceThreshold {
|
||||||
session.AudioBufferLock.Lock()
|
session.AudioBufferLock.Lock()
|
||||||
session.InputAudioBuffer = nil
|
session.InputAudioBuffer = nil
|
||||||
session.AudioBufferLock.Unlock()
|
session.AudioBufferLock.Unlock()
|
||||||
xlog.Debug("Detected silence for a while, clearing audio buffer")
|
|
||||||
|
|
||||||
sendEvent(c, types.InputAudioBufferClearedEvent{
|
// NOTE: OpenAI doesn't send this message unless the client requests it
|
||||||
ServerEventBase: types.ServerEventBase{
|
// xlog.Debug("Detected silence for a while, clearing audio buffer")
|
||||||
EventID: "event_TODO",
|
// sendEvent(c, types.InputAudioBufferClearedEvent{
|
||||||
},
|
// ServerEventBase: types.ServerEventBase{
|
||||||
})
|
// EventID: "event_TODO",
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
|
||||||
continue
|
continue
|
||||||
} else if len(segments) == 0 {
|
} else if len(segments) == 0 {
|
||||||
@@ -913,6 +936,7 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
|
|||||||
tools := session.Tools
|
tools := session.Tools
|
||||||
toolChoice := session.ToolChoice
|
toolChoice := session.ToolChoice
|
||||||
instructions := session.Instructions
|
instructions := session.Instructions
|
||||||
|
maxOutputTokens := session.MaxOutputTokens
|
||||||
// Overrides
|
// Overrides
|
||||||
if overrides != nil {
|
if overrides != nil {
|
||||||
if overrides.Tools != nil {
|
if overrides.Tools != nil {
|
||||||
@@ -924,8 +948,29 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
|
|||||||
if overrides.Instructions != "" {
|
if overrides.Instructions != "" {
|
||||||
instructions = overrides.Instructions
|
instructions = overrides.Instructions
|
||||||
}
|
}
|
||||||
|
if overrides.MaxOutputTokens != 0 {
|
||||||
|
maxOutputTokens = overrides.MaxOutputTokens
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply MaxOutputTokens to model config if specified
|
||||||
|
// Save original value to restore after prediction
|
||||||
|
var originalMaxTokens *int
|
||||||
|
if config != nil {
|
||||||
|
originalMaxTokens = config.Maxtokens
|
||||||
|
if maxOutputTokens != 0 && !maxOutputTokens.IsInf() {
|
||||||
|
tokenValue := int(maxOutputTokens)
|
||||||
|
config.Maxtokens = &tokenValue
|
||||||
|
xlog.Debug("Applied max_output_tokens to config", "value", tokenValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Defer restoration of original value
|
||||||
|
defer func() {
|
||||||
|
if config != nil {
|
||||||
|
config.Maxtokens = originalMaxTokens
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
var conversationHistory schema.Messages
|
var conversationHistory schema.Messages
|
||||||
conversationHistory = append(conversationHistory, schema.Message{
|
conversationHistory = append(conversationHistory, schema.Message{
|
||||||
Role: string(types.MessageRoleSystem),
|
Role: string(types.MessageRoleSystem),
|
||||||
@@ -949,7 +994,12 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
|
|||||||
case types.MessageContentTypeInputAudio:
|
case types.MessageContentTypeInputAudio:
|
||||||
textContent += content.Transcript
|
textContent += content.Transcript
|
||||||
case types.MessageContentTypeInputImage:
|
case types.MessageContentTypeInputImage:
|
||||||
msg.StringImages = append(msg.StringImages, content.ImageURL)
|
img, err := utils.GetContentURIAsBase64(content.ImageURL)
|
||||||
|
if err != nil {
|
||||||
|
xlog.Warn("Failed to process image", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg.StringImages = append(msg.StringImages, img)
|
||||||
imgIndex++
|
imgIndex++
|
||||||
nrOfImgsInMessage++
|
nrOfImgsInMessage++
|
||||||
}
|
}
|
||||||
@@ -996,6 +1046,27 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
|
|||||||
Content: content.Text,
|
Content: content.Text,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else if item.FunctionCall != nil {
|
||||||
|
conversationHistory = append(conversationHistory, schema.Message{
|
||||||
|
Role: string(types.MessageRoleAssistant),
|
||||||
|
ToolCalls: []schema.ToolCall{
|
||||||
|
{
|
||||||
|
ID: item.FunctionCall.CallID,
|
||||||
|
Type: "function",
|
||||||
|
FunctionCall: schema.FunctionCall{
|
||||||
|
Name: item.FunctionCall.Name,
|
||||||
|
Arguments: item.FunctionCall.Arguments,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if item.FunctionCallOutput != nil {
|
||||||
|
conversationHistory = append(conversationHistory, schema.Message{
|
||||||
|
Role: "tool",
|
||||||
|
Name: item.FunctionCallOutput.CallID,
|
||||||
|
Content: item.FunctionCallOutput.Output,
|
||||||
|
StringContent: item.FunctionCallOutput.Output,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conv.Lock.Unlock()
|
conv.Lock.Unlock()
|
||||||
@@ -1028,13 +1099,34 @@ func triggerResponse(session *Session, conv *Conversation, c *LockedWebsocket, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
xlog.Debug("Function config for parsing", "function_name_key", config.FunctionsConfig.FunctionNameKey, "function_arguments_key", config.FunctionsConfig.FunctionArgumentsKey)
|
xlog.Debug("Function config for parsing", "function_name_key", config.FunctionsConfig.FunctionNameKey, "function_arguments_key", config.FunctionsConfig.FunctionArgumentsKey)
|
||||||
|
xlog.Debug("LLM raw response", "text", pred.Response, "response_length", len(pred.Response), "usage", pred.Usage)
|
||||||
|
|
||||||
|
// Safely dereference pointer fields for logging
|
||||||
|
maxTokens := "nil"
|
||||||
|
if config.Maxtokens != nil {
|
||||||
|
maxTokens = fmt.Sprintf("%d", *config.Maxtokens)
|
||||||
|
}
|
||||||
|
contextSize := "nil"
|
||||||
|
if config.ContextSize != nil {
|
||||||
|
contextSize = fmt.Sprintf("%d", *config.ContextSize)
|
||||||
|
}
|
||||||
|
xlog.Debug("Model parameters", "max_tokens", maxTokens, "context_size", contextSize, "stopwords", config.StopWords)
|
||||||
|
|
||||||
rawResponse := pred.Response
|
rawResponse := pred.Response
|
||||||
if config.TemplateConfig.ReplyPrefix != "" {
|
if config.TemplateConfig.ReplyPrefix != "" {
|
||||||
rawResponse = config.TemplateConfig.ReplyPrefix + rawResponse
|
rawResponse = config.TemplateConfig.ReplyPrefix + rawResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
reasoningText, responseWithoutReasoning := reasoning.ExtractReasoningWithConfig(rawResponse, "", config.ReasoningConfig)
|
// Detect thinking start token from template for reasoning extraction
|
||||||
|
var template string
|
||||||
|
if config.TemplateConfig.UseTokenizerTemplate {
|
||||||
|
template = config.GetModelTemplate()
|
||||||
|
} else {
|
||||||
|
template = config.TemplateConfig.Chat
|
||||||
|
}
|
||||||
|
thinkingStartToken := reasoning.DetectThinkingStartToken(template, &config.ReasoningConfig)
|
||||||
|
|
||||||
|
reasoningText, responseWithoutReasoning := reasoning.ExtractReasoningWithConfig(rawResponse, thinkingStartToken, config.ReasoningConfig)
|
||||||
xlog.Debug("LLM Response", "reasoning", reasoningText, "response_without_reasoning", responseWithoutReasoning)
|
xlog.Debug("LLM Response", "reasoning", reasoningText, "response_without_reasoning", responseWithoutReasoning)
|
||||||
|
|
||||||
textContent := functions.ParseTextContent(responseWithoutReasoning, config.FunctionsConfig)
|
textContent := functions.ParseTextContent(responseWithoutReasoning, config.FunctionsConfig)
|
||||||
|
|||||||
@@ -194,7 +194,40 @@ func (m *wrappedModel) Predict(ctx context.Context, messages schema.Messages, im
|
|||||||
|
|
||||||
var toolsJSON string
|
var toolsJSON string
|
||||||
if len(tools) > 0 {
|
if len(tools) > 0 {
|
||||||
b, _ := json.Marshal(tools)
|
// Convert tools to OpenAI Chat Completions format (nested)
|
||||||
|
// as expected by most backends (including llama.cpp)
|
||||||
|
var chatTools []functions.Tool
|
||||||
|
for _, t := range tools {
|
||||||
|
if t.Function != nil {
|
||||||
|
var params map[string]interface{}
|
||||||
|
switch p := t.Function.Parameters.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
params = p
|
||||||
|
case string:
|
||||||
|
if err := json.Unmarshal([]byte(p), ¶ms); 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, ¶ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chatTools = append(chatTools, functions.Tool{
|
||||||
|
Type: "function",
|
||||||
|
Function: functions.Function{
|
||||||
|
Name: t.Function.Name,
|
||||||
|
Description: t.Function.Description,
|
||||||
|
Parameters: params,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(chatTools)
|
||||||
toolsJSON = string(b)
|
toolsJSON = string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ type ToolFunction struct {
|
|||||||
// The description of the function, including guidance on when and how to call it, and guidance about what to tell the user when calling (if anything).
|
// The description of the function, including guidance on when and how to call it, and guidance about what to tell the user when calling (if anything).
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
// The type of the tool, i.e. function.
|
// The jsonschema representing the parameters
|
||||||
Parameters any `json:"parameters"`
|
Parameters any `json:"parameters,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t ToolFunction) ToolType() ToolType {
|
func (t ToolFunction) ToolType() ToolType {
|
||||||
|
|||||||
@@ -279,6 +279,18 @@ func convertORInputToMessages(input interface{}, cfg *config.ModelConfig) ([]sch
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
messages = append(messages, msg)
|
messages = append(messages, msg)
|
||||||
|
case "reasoning":
|
||||||
|
msg, err := convertORReasoningItemToMessage(itemMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
messages = append(messages, msg)
|
||||||
|
case "function_call":
|
||||||
|
msg, err := convertORFunctionCallItemToMessage(itemMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
messages = append(messages, msg)
|
||||||
case "function_call_output":
|
case "function_call_output":
|
||||||
// Convert function call output to tool role message
|
// Convert function call output to tool role message
|
||||||
callID, _ := itemMap["call_id"].(string)
|
callID, _ := itemMap["call_id"].(string)
|
||||||
@@ -323,12 +335,59 @@ func convertORInputToMessages(input interface{}, cfg *config.ModelConfig) ([]sch
|
|||||||
messages = append(messages, msg)
|
messages = append(messages, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return messages, nil
|
return mergeContiguousAssistantMessages(messages), nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported input type: %T", input)
|
return nil, fmt.Errorf("unsupported input type: %T", input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertORReasoningItemToMessage converts an Open Responses reasoning item to an assistant Message fragment (for merging).
|
||||||
|
func convertORReasoningItemToMessage(itemMap map[string]interface{}) (schema.Message, error) {
|
||||||
|
var reasoning string
|
||||||
|
if content := itemMap["content"]; content != nil {
|
||||||
|
if s, ok := content.(string); ok {
|
||||||
|
reasoning = s
|
||||||
|
} else if parts, ok := content.([]interface{}); ok {
|
||||||
|
for _, p := range parts {
|
||||||
|
if partMap, ok := p.(map[string]interface{}); ok {
|
||||||
|
if t, _ := partMap["type"].(string); (t == "output_text" || t == "input_text") && partMap["text"] != nil {
|
||||||
|
if tStr, ok := partMap["text"].(string); ok {
|
||||||
|
reasoning += tStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schema.Message{Role: "assistant", Reasoning: stringPtr(reasoning)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertORFunctionCallItemToMessage converts an Open Responses function_call item to an assistant Message fragment (for merging).
|
||||||
|
func convertORFunctionCallItemToMessage(itemMap map[string]interface{}) (schema.Message, error) {
|
||||||
|
callID, _ := itemMap["call_id"].(string)
|
||||||
|
name, _ := itemMap["name"].(string)
|
||||||
|
arguments, _ := itemMap["arguments"].(string)
|
||||||
|
if callID == "" {
|
||||||
|
callID = fmt.Sprintf("call_%s", name)
|
||||||
|
}
|
||||||
|
return schema.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []schema.ToolCall{{
|
||||||
|
Index: 0,
|
||||||
|
ID: callID,
|
||||||
|
Type: "function",
|
||||||
|
FunctionCall: schema.FunctionCall{Name: name, Arguments: arguments},
|
||||||
|
}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringPtr(s string) *string {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
// convertORItemToMessage converts a single ORItemField to a Message
|
// convertORItemToMessage converts a single ORItemField to a Message
|
||||||
// responseID is the ID of the response where this item was found (for logging/debugging)
|
// responseID is the ID of the response where this item was found (for logging/debugging)
|
||||||
func convertORItemToMessage(item *schema.ORItemField, responseID string) (schema.Message, error) {
|
func convertORItemToMessage(item *schema.ORItemField, responseID string) (schema.Message, error) {
|
||||||
@@ -366,19 +425,52 @@ func convertORItemToMessage(item *schema.ORItemField, responseID string) (schema
|
|||||||
Content: outputStr,
|
Content: outputStr,
|
||||||
StringContent: outputStr,
|
StringContent: outputStr,
|
||||||
}, nil
|
}, nil
|
||||||
|
case "reasoning":
|
||||||
|
reasoning := extractReasoningContentFromORItem(item)
|
||||||
|
return schema.Message{Role: "assistant", Reasoning: stringPtr(reasoning)}, nil
|
||||||
|
case "function_call":
|
||||||
|
callID := item.CallID
|
||||||
|
if callID == "" {
|
||||||
|
callID = fmt.Sprintf("call_%s", item.Name)
|
||||||
|
}
|
||||||
|
return schema.Message{
|
||||||
|
Role: "assistant",
|
||||||
|
ToolCalls: []schema.ToolCall{{
|
||||||
|
Index: 0,
|
||||||
|
ID: callID,
|
||||||
|
Type: "function",
|
||||||
|
FunctionCall: schema.FunctionCall{Name: item.Name, Arguments: item.Arguments},
|
||||||
|
}},
|
||||||
|
}, nil
|
||||||
default:
|
default:
|
||||||
return schema.Message{}, fmt.Errorf("unsupported item type for conversion: %s (from response %s)", item.Type, responseID)
|
return schema.Message{}, fmt.Errorf("unsupported item type for conversion: %s (from response %s)", item.Type, responseID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertOROutputItemsToMessages converts Open Responses output items to internal Messages
|
func extractReasoningContentFromORItem(item *schema.ORItemField) string {
|
||||||
|
if contentParts, ok := item.Content.([]schema.ORContentPart); ok {
|
||||||
|
var s string
|
||||||
|
for _, part := range contentParts {
|
||||||
|
if part.Type == "output_text" || part.Type == "input_text" {
|
||||||
|
s += part.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if s, ok := item.Content.(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertOROutputItemsToMessages converts Open Responses output items to internal Messages.
|
||||||
|
// Contiguous assistant items (message, reasoning, function_call) are merged into a single message.
|
||||||
func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.Message, error) {
|
func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.Message, error) {
|
||||||
var messages []schema.Message
|
var messages []schema.Message
|
||||||
|
|
||||||
for _, item := range outputItems {
|
for _, item := range outputItems {
|
||||||
switch item.Type {
|
switch item.Type {
|
||||||
case "message":
|
case "message":
|
||||||
// Convert message item to assistant message
|
|
||||||
var textContent string
|
var textContent string
|
||||||
if contentParts, ok := item.Content.([]schema.ORContentPart); ok && len(contentParts) > 0 {
|
if contentParts, ok := item.Content.([]schema.ORContentPart); ok && len(contentParts) > 0 {
|
||||||
for _, part := range contentParts {
|
for _, part := range contentParts {
|
||||||
@@ -392,9 +484,23 @@ func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.
|
|||||||
StringContent: textContent,
|
StringContent: textContent,
|
||||||
Content: textContent,
|
Content: textContent,
|
||||||
})
|
})
|
||||||
|
case "reasoning":
|
||||||
|
reasoning := extractReasoningContentFromORItem(&item)
|
||||||
|
messages = append(messages, schema.Message{Role: "assistant", Reasoning: stringPtr(reasoning)})
|
||||||
case "function_call":
|
case "function_call":
|
||||||
// Function calls are handled separately - they become tool calls in the next turn
|
msg := schema.Message{
|
||||||
// For now, we skip them as they're part of the model's output, not input
|
Role: "assistant",
|
||||||
|
ToolCalls: []schema.ToolCall{{
|
||||||
|
Index: 0,
|
||||||
|
ID: item.CallID,
|
||||||
|
Type: "function",
|
||||||
|
FunctionCall: schema.FunctionCall{Name: item.Name, Arguments: item.Arguments},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
if msg.ToolCalls[0].ID == "" {
|
||||||
|
msg.ToolCalls[0].ID = fmt.Sprintf("call_%s", item.Name)
|
||||||
|
}
|
||||||
|
messages = append(messages, msg)
|
||||||
case "function_call_output":
|
case "function_call_output":
|
||||||
// Convert function call output to tool role message
|
// Convert function call output to tool role message
|
||||||
var outputStr string
|
var outputStr string
|
||||||
@@ -414,7 +520,74 @@ func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages, nil
|
return mergeContiguousAssistantMessages(messages), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeContiguousAssistantMessages merges contiguous assistant messages into one.
|
||||||
|
// Many chat templates expect content, reasoning, and tool calls in a single assistant message
|
||||||
|
// (see e.g. llama.cpp PR 19773). This avoids creating separate messages per input item.
|
||||||
|
func mergeContiguousAssistantMessages(messages []schema.Message) []schema.Message {
|
||||||
|
if len(messages) == 0 {
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
var out []schema.Message
|
||||||
|
var acc *schema.Message
|
||||||
|
for i := range messages {
|
||||||
|
m := &messages[i]
|
||||||
|
if m.Role != "assistant" {
|
||||||
|
flushAssistantAccumulator(&out, &acc)
|
||||||
|
out = append(out, *m)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if acc == nil {
|
||||||
|
acc = &schema.Message{Role: "assistant"}
|
||||||
|
}
|
||||||
|
if m.StringContent != "" {
|
||||||
|
if acc.StringContent != "" {
|
||||||
|
acc.StringContent += "\n" + m.StringContent
|
||||||
|
} else {
|
||||||
|
acc.StringContent = m.StringContent
|
||||||
|
}
|
||||||
|
if acc.Content == nil {
|
||||||
|
acc.Content = m.Content
|
||||||
|
} else if _, ok := m.Content.(string); ok {
|
||||||
|
acc.Content = acc.StringContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Reasoning != nil && *m.Reasoning != "" {
|
||||||
|
if acc.Reasoning == nil {
|
||||||
|
acc.Reasoning = m.Reasoning
|
||||||
|
} else {
|
||||||
|
combined := *acc.Reasoning + "\n" + *m.Reasoning
|
||||||
|
acc.Reasoning = &combined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(m.ToolCalls) > 0 {
|
||||||
|
acc.ToolCalls = append(acc.ToolCalls, m.ToolCalls...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flushAssistantAccumulator(&out, &acc)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func flushAssistantAccumulator(out *[]schema.Message, acc **schema.Message) {
|
||||||
|
if acc == nil || *acc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m := *acc
|
||||||
|
if m.StringContent == "" && (m.Reasoning == nil || *m.Reasoning == "") && len(m.ToolCalls) == 0 {
|
||||||
|
*acc = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if m.Content == nil {
|
||||||
|
m.Content = m.StringContent
|
||||||
|
}
|
||||||
|
// Re-index tool calls after merge (each may have been 0)
|
||||||
|
for i := range m.ToolCalls {
|
||||||
|
m.ToolCalls[i].Index = i
|
||||||
|
}
|
||||||
|
*out = append(*out, *m)
|
||||||
|
*acc = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertORMessageItem converts an Open Responses message item to internal Message
|
// convertORMessageItem converts an Open Responses message item to internal Message
|
||||||
@@ -627,13 +800,26 @@ func handleBackgroundNonStream(ctx context.Context, store *ResponseStore, respon
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
prediction, err := predFunc()
|
const maxEmptyRetries = 5
|
||||||
if err != nil {
|
var prediction backend.LLMResponse
|
||||||
return nil, fmt.Errorf("prediction failed: %w", err)
|
var result string
|
||||||
|
for attempt := 0; attempt <= maxEmptyRetries; attempt++ {
|
||||||
|
prediction, err = predFunc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("prediction failed: %w", err)
|
||||||
|
}
|
||||||
|
result = backend.Finetune(*cfg, predInput, prediction.Response)
|
||||||
|
if result != "" || !shouldUseFn {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
xlog.Warn("Open Responses background: retrying prediction due to empty backend response", "attempt", attempt+1, "maxRetries", maxEmptyRetries)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := backend.Finetune(*cfg, predInput, prediction.Response)
|
|
||||||
|
|
||||||
// Parse tool calls if using functions (same logic as regular handler)
|
// Parse tool calls if using functions (same logic as regular handler)
|
||||||
var outputItems []schema.ORItemField
|
var outputItems []schema.ORItemField
|
||||||
var toolCalls []schema.ToolCall
|
var toolCalls []schema.ToolCall
|
||||||
@@ -927,7 +1113,7 @@ func handleBackgroundMCPResponse(ctx context.Context, store *ResponseStore, resp
|
|||||||
// Build fragment from messages
|
// Build fragment from messages
|
||||||
fragment := cogito.NewEmptyFragment()
|
fragment := cogito.NewEmptyFragment()
|
||||||
for _, message := range openAIReq.Messages {
|
for _, message := range openAIReq.Messages {
|
||||||
fragment = fragment.AddMessage(message.Role, message.StringContent)
|
fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
|
||||||
}
|
}
|
||||||
fragmentPtr := &fragment
|
fragmentPtr := &fragment
|
||||||
|
|
||||||
@@ -1004,12 +1190,6 @@ func handleBackgroundMCPNonStream(ctx context.Context, store *ResponseStore, res
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get final response
|
|
||||||
f, err = defaultLLM.Ask(ctx, f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert fragment to Open Responses format
|
// Convert fragment to Open Responses format
|
||||||
fPtr := &f
|
fPtr := &f
|
||||||
outputItems := convertCogitoFragmentToORItems(fPtr)
|
outputItems := convertCogitoFragmentToORItems(fPtr)
|
||||||
@@ -1186,21 +1366,6 @@ func handleBackgroundMCPStream(ctx context.Context, store *ResponseStore, respon
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get final response
|
|
||||||
f, err = defaultLLM.Ask(ctx, f)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
ended <- ctx.Err()
|
|
||||||
case events <- map[string]interface{}{
|
|
||||||
"type": "error",
|
|
||||||
"message": fmt.Sprintf("Failed to get response: %v", err),
|
|
||||||
}:
|
|
||||||
ended <- err
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream final assistant message
|
// Stream final assistant message
|
||||||
content := f.LastMessage().Content
|
content := f.LastMessage().Content
|
||||||
messageID := fmt.Sprintf("msg_%s", uuid.New().String())
|
messageID := fmt.Sprintf("msg_%s", uuid.New().String())
|
||||||
@@ -1323,13 +1488,21 @@ func handleOpenResponsesNonStream(c echo.Context, responseID string, createdAt i
|
|||||||
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("model inference failed: %v", err), "")
|
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("model inference failed: %v", err), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
prediction, err := predFunc()
|
const maxEmptyRetries = 5
|
||||||
if err != nil {
|
var prediction backend.LLMResponse
|
||||||
xlog.Error("Open Responses prediction failed", "error", err)
|
var result string
|
||||||
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("prediction failed: %v", err), "")
|
for attempt := 0; attempt <= maxEmptyRetries; attempt++ {
|
||||||
|
prediction, err = predFunc()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Error("Open Responses prediction failed", "error", err)
|
||||||
|
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("prediction failed: %v", err), "")
|
||||||
|
}
|
||||||
|
result = backend.Finetune(*cfg, predInput, prediction.Response)
|
||||||
|
if result != "" || !shouldUseFn {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
xlog.Warn("Open Responses: retrying prediction due to empty backend response", "attempt", attempt+1, "maxRetries", maxEmptyRetries)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := backend.Finetune(*cfg, predInput, prediction.Response)
|
|
||||||
xlog.Debug("Open Responses - Raw model result", "result", result, "shouldUseFn", shouldUseFn)
|
xlog.Debug("Open Responses - Raw model result", "result", result, "shouldUseFn", shouldUseFn)
|
||||||
|
|
||||||
// Detect if thinking token is already in prompt or template
|
// Detect if thinking token is already in prompt or template
|
||||||
@@ -2505,7 +2678,7 @@ func handleMCPResponse(c echo.Context, responseID string, createdAt int64, input
|
|||||||
// Build fragment from messages
|
// Build fragment from messages
|
||||||
fragment := cogito.NewEmptyFragment()
|
fragment := cogito.NewEmptyFragment()
|
||||||
for _, message := range openAIReq.Messages {
|
for _, message := range openAIReq.Messages {
|
||||||
fragment = fragment.AddMessage(message.Role, message.StringContent)
|
fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
|
||||||
}
|
}
|
||||||
fragmentPtr := &fragment
|
fragmentPtr := &fragment
|
||||||
|
|
||||||
@@ -2580,12 +2753,6 @@ func handleMCPNonStream(c echo.Context, responseID string, createdAt int64, inpu
|
|||||||
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("failed to execute tools: %v", err), "")
|
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("failed to execute tools: %v", err), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get final response
|
|
||||||
f, err = defaultLLM.Ask(ctx, f)
|
|
||||||
if err != nil {
|
|
||||||
return sendOpenResponsesError(c, 500, "model_error", fmt.Sprintf("failed to get response: %v", err), "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert fragment to Open Responses format
|
// Convert fragment to Open Responses format
|
||||||
fPtr := &f
|
fPtr := &f
|
||||||
outputItems := convertCogitoFragmentToORItems(fPtr)
|
outputItems := convertCogitoFragmentToORItems(fPtr)
|
||||||
@@ -2730,17 +2897,6 @@ func handleMCPStream(c echo.Context, responseID string, createdAt int64, input *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get final response
|
|
||||||
f, err = defaultLLM.Ask(ctx, f)
|
|
||||||
if err != nil {
|
|
||||||
events <- map[string]interface{}{
|
|
||||||
"type": "error",
|
|
||||||
"message": fmt.Sprintf("Failed to get response: %v", err),
|
|
||||||
}
|
|
||||||
ended <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream final assistant message
|
// Stream final assistant message
|
||||||
content := f.LastMessage().Content
|
content := f.LastMessage().Content
|
||||||
messageID := fmt.Sprintf("msg_%s", uuid.New().String())
|
messageID := fmt.Sprintf("msg_%s", uuid.New().String())
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ func RegisterLocalAIRoutes(router *echo.Echo,
|
|||||||
// Import model page
|
// Import model page
|
||||||
router.GET("/import-model", func(c echo.Context) error {
|
router.GET("/import-model", func(c echo.Context) error {
|
||||||
return c.Render(200, "views/model-editor", map[string]interface{}{
|
return c.Render(200, "views/model-editor", map[string]interface{}{
|
||||||
"Title": "LocalAI - Import Model",
|
"Title": "LocalAI - Import Model",
|
||||||
"BaseURL": middleware.BaseURL(c),
|
"BaseURL": middleware.BaseURL(c),
|
||||||
"Version": internal.PrintableVersion(),
|
"Version": internal.PrintableVersion(),
|
||||||
|
"DisableRuntimeSettings": appConfig.DisableRuntimeSettings,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ func RegisterLocalAIRoutes(router *echo.Echo,
|
|||||||
router.POST("/models/import-uri", localai.ImportModelURIEndpoint(cl, appConfig, galleryService, opcache))
|
router.POST("/models/import-uri", localai.ImportModelURIEndpoint(cl, appConfig, galleryService, opcache))
|
||||||
|
|
||||||
// Custom model edit endpoint
|
// Custom model edit endpoint
|
||||||
router.POST("/models/edit/:name", localai.EditModelEndpoint(cl, appConfig))
|
router.POST("/models/edit/:name", localai.EditModelEndpoint(cl, ml, appConfig))
|
||||||
|
|
||||||
// Reload models endpoint
|
// Reload models endpoint
|
||||||
router.POST("/models/reload", localai.ReloadModelsEndpoint(cl, appConfig))
|
router.POST("/models/reload", localai.ReloadModelsEndpoint(cl, appConfig))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/mudler/LocalAI/core/http/endpoints/localai"
|
"github.com/mudler/LocalAI/core/http/endpoints/localai"
|
||||||
"github.com/mudler/LocalAI/core/http/middleware"
|
"github.com/mudler/LocalAI/core/http/middleware"
|
||||||
"github.com/mudler/LocalAI/core/services"
|
"github.com/mudler/LocalAI/core/services"
|
||||||
|
"github.com/mudler/LocalAI/core/trace"
|
||||||
"github.com/mudler/LocalAI/internal"
|
"github.com/mudler/LocalAI/internal"
|
||||||
"github.com/mudler/LocalAI/pkg/model"
|
"github.com/mudler/LocalAI/pkg/model"
|
||||||
)
|
)
|
||||||
@@ -430,4 +431,13 @@ func RegisterUIRoutes(app *echo.Echo,
|
|||||||
return c.NoContent(204)
|
return c.NoContent(204)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.GET("/api/backend-traces", func(c echo.Context) error {
|
||||||
|
return c.JSON(200, trace.GetBackendTraces())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.POST("/api/backend-traces/clear", func(c echo.Context) error {
|
||||||
|
trace.ClearBackendTraces()
|
||||||
|
return c.NoContent(204)
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@@ -20,6 +25,7 @@ import (
|
|||||||
"github.com/mudler/LocalAI/core/p2p"
|
"github.com/mudler/LocalAI/core/p2p"
|
||||||
"github.com/mudler/LocalAI/core/services"
|
"github.com/mudler/LocalAI/core/services"
|
||||||
"github.com/mudler/LocalAI/pkg/model"
|
"github.com/mudler/LocalAI/pkg/model"
|
||||||
|
"github.com/mudler/LocalAI/pkg/vram"
|
||||||
"github.com/mudler/LocalAI/pkg/xsysinfo"
|
"github.com/mudler/LocalAI/pkg/xsysinfo"
|
||||||
"github.com/mudler/xlog"
|
"github.com/mudler/xlog"
|
||||||
)
|
)
|
||||||
@@ -32,6 +38,25 @@ const (
|
|||||||
ascSortOrder = "asc"
|
ascSortOrder = "asc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// getDirectorySize calculates the total size of files in a directory
|
||||||
|
func getDirectorySize(path string) (int64, error) {
|
||||||
|
var totalSize int64
|
||||||
|
entries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
totalSize += info.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterUIAPIRoutes registers JSON API routes for the web UI
|
// RegisterUIAPIRoutes registers JSON API routes for the web UI
|
||||||
func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig, galleryService *services.GalleryService, opcache *services.OpCache, applicationInstance *application.Application) {
|
func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig, galleryService *services.GalleryService, opcache *services.OpCache, applicationInstance *application.Application) {
|
||||||
|
|
||||||
@@ -242,6 +267,22 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
|
|||||||
modelsJSON := make([]map[string]interface{}, 0, len(models))
|
modelsJSON := make([]map[string]interface{}, 0, len(models))
|
||||||
seenIDs := make(map[string]bool)
|
seenIDs := make(map[string]bool)
|
||||||
|
|
||||||
|
weightExts := map[string]bool{".gguf": true, ".safetensors": true, ".bin": true, ".pt": true}
|
||||||
|
hasWeightFiles := func(files []gallery.File) bool {
|
||||||
|
for _, f := range files {
|
||||||
|
ext := strings.ToLower(path.Ext(path.Base(f.URI)))
|
||||||
|
if weightExts[ext] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimateTimeout = 3 * time.Second
|
||||||
|
const estimateConcurrency = 3
|
||||||
|
sem := make(chan struct{}, estimateConcurrency)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for _, m := range models {
|
for _, m := range models {
|
||||||
modelID := m.ID()
|
modelID := m.ID()
|
||||||
|
|
||||||
@@ -265,7 +306,7 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
|
|||||||
|
|
||||||
_, trustRemoteCodeExists := m.Overrides["trust_remote_code"]
|
_, trustRemoteCodeExists := m.Overrides["trust_remote_code"]
|
||||||
|
|
||||||
modelsJSON = append(modelsJSON, map[string]interface{}{
|
obj := map[string]interface{}{
|
||||||
"id": modelID,
|
"id": modelID,
|
||||||
"name": m.Name,
|
"name": m.Name,
|
||||||
"description": m.Description,
|
"description": m.Description,
|
||||||
@@ -280,9 +321,48 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
|
|||||||
"isDeletion": isDeletionOp,
|
"isDeletion": isDeletionOp,
|
||||||
"trustRemoteCode": trustRemoteCodeExists,
|
"trustRemoteCode": trustRemoteCodeExists,
|
||||||
"additionalFiles": m.AdditionalFiles,
|
"additionalFiles": m.AdditionalFiles,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if hasWeightFiles(m.AdditionalFiles) {
|
||||||
|
files := make([]gallery.File, len(m.AdditionalFiles))
|
||||||
|
copy(files, m.AdditionalFiles)
|
||||||
|
wg.Add(1)
|
||||||
|
go func(files []gallery.File, out map[string]interface{}) {
|
||||||
|
defer wg.Done()
|
||||||
|
sem <- struct{}{}
|
||||||
|
defer func() { <-sem }()
|
||||||
|
inputs := make([]vram.FileInput, 0, len(files))
|
||||||
|
for _, f := range files {
|
||||||
|
ext := strings.ToLower(path.Ext(path.Base(f.URI)))
|
||||||
|
if weightExts[ext] {
|
||||||
|
inputs = append(inputs, vram.FileInput{URI: f.URI, Size: 0})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(inputs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), estimateTimeout)
|
||||||
|
defer cancel()
|
||||||
|
opts := vram.EstimateOptions{ContextLength: 8192}
|
||||||
|
result, err := vram.Estimate(ctx, inputs, opts, vram.DefaultCachedSizeResolver(), vram.DefaultCachedGGUFReader())
|
||||||
|
if err == nil {
|
||||||
|
if result.SizeBytes > 0 {
|
||||||
|
out["estimated_size_bytes"] = result.SizeBytes
|
||||||
|
out["estimated_size_display"] = result.SizeDisplay
|
||||||
|
}
|
||||||
|
if result.VRAMBytes > 0 {
|
||||||
|
out["estimated_vram_bytes"] = result.VRAMBytes
|
||||||
|
out["estimated_vram_display"] = result.VRAMDisplay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(files, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
modelsJSON = append(modelsJSON, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
prevPage := pageNum - 1
|
prevPage := pageNum - 1
|
||||||
nextPage := pageNum + 1
|
nextPage := pageNum + 1
|
||||||
if prevPage < 1 {
|
if prevPage < 1 {
|
||||||
@@ -297,6 +377,8 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
|
|||||||
modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
|
modelsWithoutConfig, _ := services.ListModels(cl, ml, config.NoFilterFn, services.LOOSE_ONLY)
|
||||||
installedModelsCount := len(modelConfigs) + len(modelsWithoutConfig)
|
installedModelsCount := len(modelConfigs) + len(modelsWithoutConfig)
|
||||||
|
|
||||||
|
ramInfo, _ := xsysinfo.GetSystemRAMInfo()
|
||||||
|
|
||||||
return c.JSON(200, map[string]interface{}{
|
return c.JSON(200, map[string]interface{}{
|
||||||
"models": modelsJSON,
|
"models": modelsJSON,
|
||||||
"repositories": appConfig.Galleries,
|
"repositories": appConfig.Galleries,
|
||||||
@@ -305,6 +387,9 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
|
|||||||
"taskTypes": taskTypes,
|
"taskTypes": taskTypes,
|
||||||
"availableModels": totalModels,
|
"availableModels": totalModels,
|
||||||
"installedModels": installedModelsCount,
|
"installedModels": installedModelsCount,
|
||||||
|
"ramTotal": ramInfo.Total,
|
||||||
|
"ramUsed": ramInfo.Used,
|
||||||
|
"ramUsagePercent": ramInfo.UsagePercent,
|
||||||
"currentPage": pageNum,
|
"currentPage": pageNum,
|
||||||
"totalPages": totalPages,
|
"totalPages": totalPages,
|
||||||
"prevPage": prevPage,
|
"prevPage": prevPage,
|
||||||
@@ -936,12 +1021,15 @@ func RegisterUIAPIRoutes(app *echo.Echo, cl *config.ModelConfigLoader, ml *model
|
|||||||
watchdogInterval = appConfig.WatchDogInterval.String()
|
watchdogInterval = appConfig.WatchDogInterval.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storageSize, _ := getDirectorySize(appConfig.SystemState.Model.ModelsPath)
|
||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"type": resourceInfo.Type, // "gpu" or "ram"
|
"type": resourceInfo.Type, // "gpu" or "ram"
|
||||||
"available": resourceInfo.Available,
|
"available": resourceInfo.Available,
|
||||||
"gpus": resourceInfo.GPUs,
|
"gpus": resourceInfo.GPUs,
|
||||||
"ram": resourceInfo.RAM,
|
"ram": resourceInfo.RAM,
|
||||||
"aggregate": resourceInfo.Aggregate,
|
"aggregate": resourceInfo.Aggregate,
|
||||||
|
"storage_size": storageSize,
|
||||||
"reclaimer_enabled": appConfig.MemoryReclaimerEnabled,
|
"reclaimer_enabled": appConfig.MemoryReclaimerEnabled,
|
||||||
"reclaimer_threshold": appConfig.MemoryReclaimerThreshold,
|
"reclaimer_threshold": appConfig.MemoryReclaimerThreshold,
|
||||||
"watchdog_interval": watchdogInterval,
|
"watchdog_interval": watchdogInterval,
|
||||||
|
|||||||
@@ -1148,6 +1148,9 @@ async function promptGPT(systemPrompt, input) {
|
|||||||
|
|
||||||
messages = chatStore.messages();
|
messages = chatStore.messages();
|
||||||
|
|
||||||
|
// Exclude thinking/reasoning from API payload (backend chat templates expect only system/user/assistant)
|
||||||
|
messages = messages.filter((m) => m.role !== "thinking" && m.role !== "reasoning");
|
||||||
|
|
||||||
// if systemPrompt isn't empty, push it at the start of messages
|
// if systemPrompt isn't empty, push it at the start of messages
|
||||||
if (systemPrompt) {
|
if (systemPrompt) {
|
||||||
messages.unshift({
|
messages.unshift({
|
||||||
@@ -2530,12 +2533,14 @@ document.addEventListener("alpine:init", () => {
|
|||||||
messages() {
|
messages() {
|
||||||
const chat = this.activeChat();
|
const chat = this.activeChat();
|
||||||
if (!chat) return [];
|
if (!chat) return [];
|
||||||
return chat.history.map((message) => ({
|
return chat.history
|
||||||
role: message.role,
|
.filter((message) => message.role !== "thinking" && message.role !== "reasoning")
|
||||||
content: message.content,
|
.map((message) => ({
|
||||||
image: message.image,
|
role: message.role,
|
||||||
audio: message.audio,
|
content: message.content,
|
||||||
}));
|
image: message.image,
|
||||||
|
audio: message.audio,
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
// Getter for active chat history to ensure reactivity
|
// Getter for active chat history to ensure reactivity
|
||||||
|
|||||||
@@ -418,6 +418,337 @@ textarea.input-success {
|
|||||||
animation: nodeGlow 3s ease-in-out infinite;
|
animation: nodeGlow 3s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Sidebar Navigation
|
||||||
|
============================================ */
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
background: var(--sidebar-bg);
|
||||||
|
border-right: 1px solid var(--sidebar-border);
|
||||||
|
box-shadow: var(--shadow-sidebar);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 40;
|
||||||
|
transition: transform var(--duration-normal) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--color-border-divider);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo img {
|
||||||
|
height: 2rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo-text {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section-title {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 0.75rem 1.25rem 0.25rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--weight-normal);
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
border-left-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
border-left-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item i,
|
||||||
|
.nav-item .nav-icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .nav-label {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item .nav-chevron {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
transition: transform var(--duration-fast) var(--ease-default);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.expanded .nav-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown submenu */
|
||||||
|
.nav-submenu {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height var(--duration-normal) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-submenu.open {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-submenu .nav-item {
|
||||||
|
padding-left: 2.75rem;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-submenu .nav-item i,
|
||||||
|
.nav-submenu .nav-item .nav-icon {
|
||||||
|
width: 1rem;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar footer with theme toggle */
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-top: 1px solid var(--color-border-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
margin: 0.125rem 0.5rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-label i {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle switch */
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 1.375rem;
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 1.125rem;
|
||||||
|
height: 1.125rem;
|
||||||
|
background: var(--color-text-secondary);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.active {
|
||||||
|
background: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch.active::after {
|
||||||
|
transform: translateX(1.125rem);
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile overlay */
|
||||||
|
.sidebar-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 35;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity var(--duration-normal) var(--ease-default),
|
||||||
|
visibility var(--duration-normal) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-overlay.open {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile menu button */
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
z-index: 50;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn:hover {
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
color: var(--color-primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide menu button when sidebar is open */
|
||||||
|
.mobile-menu-btn[style*="opacity: 0"] {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile close button inside sidebar */
|
||||||
|
.sidebar-close-btn {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close-btn:hover {
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Tables
|
||||||
|
============================================ */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr {
|
||||||
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--weight-semibold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
|
transition: background-color var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:hover {
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table container */
|
||||||
|
.table-container {
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Responsive Adjustments
|
Responsive Adjustments
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -439,3 +770,36 @@ textarea.input-success {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tablet and mobile - sidebar becomes overlay */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
z-index: 45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding-right: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-overlay.open + .sidebar,
|
||||||
|
.sidebar.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,76 @@
|
|||||||
|
/* Layout Structure */
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-body, 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif);
|
font-family: var(--font-body, 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
transition: background-color var(--duration-normal) var(--ease-default),
|
||||||
|
color var(--duration-normal) var(--ease-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-layout {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: var(--sidebar-width);
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
transition: margin-left var(--duration-normal) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content-inner {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--color-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pages without sidebar (e.g. login): center content */
|
||||||
|
.app-layout.no-sidebar .main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat page: fix viewport height so messages scroll and input stays fixed at bottom */
|
||||||
|
.app-layout.chat-layout {
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.main-content.chat-layout {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.main-content-inner.chat-layout {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet and mobile */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safe area for notched devices (e.g. iOS) - use on fixed bottom bars / modals */
|
||||||
|
@supports (padding: env(safe-area-inset-bottom)) {
|
||||||
|
.pb-safe {
|
||||||
|
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chat-container { height: 90vh; display: flex; flex-direction: column; }
|
.chat-container { height: 90vh; display: flex; flex-direction: column; }
|
||||||
.chat-messages { overflow-y: auto; flex-grow: 1; }
|
.chat-messages { overflow-y: auto; flex-grow: 1; }
|
||||||
.htmx-indicator{
|
.htmx-indicator{
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
/* LocalAI Theme - CSS Variables System */
|
/* LocalAI Theme - CSS Variables System */
|
||||||
/* Based on logo color palette: cyan, teal, navy, purple */
|
/* Based on logo color palette: cyan, teal, navy, purple */
|
||||||
|
|
||||||
:root {
|
/* Dark Theme (Default) - Charcoal Gray Style */
|
||||||
/* Base Colors */
|
:root,
|
||||||
--color-bg-primary: #0F172A; /* Deep navy background */
|
[data-theme="dark"],
|
||||||
--color-bg-secondary: #1E293B; /* Elevated surfaces */
|
.dark {
|
||||||
--color-bg-tertiary: #1E293B; /* Cards, panels */
|
/* Base Colors - Charcoal Gray */
|
||||||
--color-bg-overlay: rgba(15, 23, 42, 0.8); /* Modals, overlays */
|
--color-bg-primary: #121212; /* Main background */
|
||||||
|
--color-bg-secondary: #1A1A1A; /* Elevated surfaces */
|
||||||
|
--color-bg-tertiary: #222222; /* Cards, panels */
|
||||||
|
--color-bg-overlay: rgba(18, 18, 18, 0.95); /* Modals, overlays */
|
||||||
|
|
||||||
|
/* Override tw-elements dark background */
|
||||||
|
background-color: #121212 !important;
|
||||||
|
|
||||||
/* Brand Colors */
|
/* Brand Colors */
|
||||||
--color-primary: #38BDF8; /* Cyan - primary actions */
|
--color-primary: #38BDF8; /* Cyan - primary actions */
|
||||||
@@ -32,16 +38,16 @@
|
|||||||
--color-text-secondary: #94A3B8; /* Secondary text */
|
--color-text-secondary: #94A3B8; /* Secondary text */
|
||||||
--color-text-muted: #64748B; /* Tertiary text */
|
--color-text-muted: #64748B; /* Tertiary text */
|
||||||
--color-text-disabled: #475569; /* Disabled text */
|
--color-text-disabled: #475569; /* Disabled text */
|
||||||
--color-text-inverse: #0F172A; /* Text on light backgrounds */
|
--color-text-inverse: #FFFFFF; /* Text on light backgrounds */
|
||||||
|
|
||||||
/* Border Colors - Minimal System */
|
/* Border Colors - Visible on charcoal */
|
||||||
--color-border-subtle: rgba(148, 163, 184, 0.08); /* Minimal borders */
|
--color-border-subtle: rgba(255, 255, 255, 0.08); /* Minimal borders */
|
||||||
--color-border-default: rgba(148, 163, 184, 0.12); /* Default borders */
|
--color-border-default: rgba(255, 255, 255, 0.12); /* Default borders */
|
||||||
--color-border-strong: rgba(56, 189, 248, 0.2); /* Focus borders */
|
--color-border-strong: rgba(56, 189, 248, 0.3); /* Focus borders */
|
||||||
--color-border-divider: rgba(148, 163, 184, 0.06); /* Section dividers */
|
--color-border-divider: rgba(255, 255, 255, 0.05); /* Section dividers */
|
||||||
--color-border-primary: rgba(56, 189, 248, 0.15); /* Primary borders (reduced opacity) */
|
--color-border-primary: rgba(56, 189, 248, 0.2); /* Primary borders */
|
||||||
--color-border-secondary: rgba(148, 163, 184, 0.1);
|
--color-border-secondary: rgba(255, 255, 255, 0.1);
|
||||||
--color-border-focus: rgba(56, 189, 248, 0.3); /* Focus borders (reduced) */
|
--color-border-focus: rgba(56, 189, 248, 0.4); /* Focus borders */
|
||||||
|
|
||||||
/* Status Colors */
|
/* Status Colors */
|
||||||
--color-success: #14B8A6; /* Use teal for success (aligned with logo) */
|
--color-success: #14B8A6; /* Use teal for success (aligned with logo) */
|
||||||
@@ -55,17 +61,18 @@
|
|||||||
|
|
||||||
/* Gradient Definitions */
|
/* Gradient Definitions */
|
||||||
--gradient-primary: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
|
--gradient-primary: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
|
||||||
--gradient-hero: linear-gradient(135deg, #0F172A 0%, #1E293B 50%, #0F172A 100%);
|
--gradient-hero: linear-gradient(135deg, #121212 0%, #1A1A1A 50%, #121212 100%);
|
||||||
--gradient-card: linear-gradient(135deg, rgba(56, 189, 248, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%);
|
--gradient-card: linear-gradient(135deg, rgba(56, 189, 248, 0.04) 0%, rgba(139, 92, 246, 0.04) 100%);
|
||||||
--gradient-text: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
|
--gradient-text: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 50%, #14B8A6 100%);
|
||||||
|
|
||||||
/* Shadows - Minimal System */
|
/* Shadows - Charcoal theme */
|
||||||
--shadow-none: none;
|
--shadow-none: none;
|
||||||
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.1);
|
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
|
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.35);
|
||||||
--shadow-glow: 0 0 0 1px rgba(56, 189, 248, 0.1), 0 0 8px rgba(56, 189, 248, 0.15); /* Minimal glow */
|
--shadow-glow: 0 0 0 1px rgba(56, 189, 248, 0.15), 0 0 12px rgba(56, 189, 248, 0.2);
|
||||||
|
--shadow-sidebar: 1px 0 3px rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
/* Animation Timing - Minimal */
|
/* Animation Timing - Minimal */
|
||||||
--duration-instant: 100ms;
|
--duration-instant: 100ms;
|
||||||
@@ -109,5 +116,83 @@
|
|||||||
--width-5xl: 64rem; /* 1024px */
|
--width-5xl: 64rem; /* 1024px */
|
||||||
--width-6xl: 72rem; /* 1152px */
|
--width-6xl: 72rem; /* 1152px */
|
||||||
--width-7xl: 80rem; /* 1280px */
|
--width-7xl: 80rem; /* 1280px */
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar-width: 220px;
|
||||||
|
--sidebar-bg: var(--color-bg-primary);
|
||||||
|
--sidebar-border: var(--color-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Theme */
|
||||||
|
[data-theme="light"] {
|
||||||
|
/* Base Colors */
|
||||||
|
--color-bg-primary: #F8FAFC; /* Soft gray background */
|
||||||
|
--color-bg-secondary: #FFFFFF; /* Elevated surfaces */
|
||||||
|
--color-bg-tertiary: #FFFFFF; /* Cards, panels */
|
||||||
|
--color-bg-overlay: rgba(248, 250, 252, 0.9); /* Modals, overlays */
|
||||||
|
|
||||||
|
/* Brand Colors - Slightly adjusted for light backgrounds */
|
||||||
|
--color-primary: #0EA5E9; /* Slightly darker cyan for better contrast */
|
||||||
|
--color-primary-hover: #0284C7; /* Darker on hover */
|
||||||
|
--color-primary-active: #0369A1; /* Active state */
|
||||||
|
--color-primary-text: #FFFFFF; /* Text on primary background */
|
||||||
|
--color-primary-light: rgba(14, 165, 233, 0.08); /* Light cyan backgrounds */
|
||||||
|
--color-primary-border: rgba(14, 165, 233, 0.2); /* Cyan borders */
|
||||||
|
|
||||||
|
--color-secondary: #0D9488; /* Teal - secondary actions */
|
||||||
|
--color-secondary-hover: #0F766E; /* Darker teal on hover */
|
||||||
|
--color-secondary-light: rgba(13, 148, 136, 0.1);
|
||||||
|
|
||||||
|
--color-accent: #7C3AED; /* Purple - special states */
|
||||||
|
--color-accent-hover: #6D28D9; /* Darker purple on hover */
|
||||||
|
--color-accent-light: rgba(124, 58, 237, 0.1);
|
||||||
|
|
||||||
|
--color-accent-purple: #A78BFA; /* Light purple for gradients */
|
||||||
|
--color-accent-teal: #2DD4BF; /* Light teal for gradients */
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--color-text-primary: #1E293B; /* Primary text - dark slate */
|
||||||
|
--color-text-secondary: #64748B; /* Secondary text */
|
||||||
|
--color-text-muted: #94A3B8; /* Tertiary text */
|
||||||
|
--color-text-disabled: #CBD5E1; /* Disabled text */
|
||||||
|
--color-text-inverse: #FFFFFF; /* Text on dark backgrounds */
|
||||||
|
|
||||||
|
/* Border Colors */
|
||||||
|
--color-border-subtle: rgba(15, 23, 42, 0.06); /* Minimal borders */
|
||||||
|
--color-border-default: rgba(15, 23, 42, 0.1); /* Default borders */
|
||||||
|
--color-border-strong: rgba(14, 165, 233, 0.3); /* Focus borders */
|
||||||
|
--color-border-divider: rgba(15, 23, 42, 0.04); /* Section dividers */
|
||||||
|
--color-border-primary: rgba(14, 165, 233, 0.2); /* Primary borders */
|
||||||
|
--color-border-secondary: rgba(15, 23, 42, 0.08);
|
||||||
|
--color-border-focus: rgba(14, 165, 233, 0.4); /* Focus borders */
|
||||||
|
|
||||||
|
/* Status Colors - Adjusted for light theme */
|
||||||
|
--color-success: #0D9488;
|
||||||
|
--color-success-light: rgba(13, 148, 136, 0.1);
|
||||||
|
--color-warning: #D97706;
|
||||||
|
--color-warning-light: rgba(217, 119, 6, 0.1);
|
||||||
|
--color-error: #DC2626;
|
||||||
|
--color-error-light: rgba(220, 38, 38, 0.1);
|
||||||
|
--color-info: #0EA5E9;
|
||||||
|
--color-info-light: rgba(14, 165, 233, 0.1);
|
||||||
|
|
||||||
|
/* Gradient Definitions */
|
||||||
|
--gradient-primary: linear-gradient(135deg, #0EA5E9 0%, #7C3AED 50%, #0D9488 100%);
|
||||||
|
--gradient-hero: linear-gradient(135deg, #F8FAFC 0%, #FFFFFF 50%, #F8FAFC 100%);
|
||||||
|
--gradient-card: linear-gradient(135deg, rgba(14, 165, 233, 0.03) 0%, rgba(124, 58, 237, 0.03) 100%);
|
||||||
|
--gradient-text: linear-gradient(135deg, #0EA5E9 0%, #7C3AED 50%, #0D9488 100%);
|
||||||
|
|
||||||
|
/* Shadows - More visible in light theme */
|
||||||
|
--shadow-none: none;
|
||||||
|
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
||||||
|
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-glow: 0 0 0 1px rgba(14, 165, 233, 0.15), 0 0 8px rgba(14, 165, 233, 0.2);
|
||||||
|
--shadow-sidebar: 1px 0 3px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar-bg: #FFFFFF;
|
||||||
|
--sidebar-border: rgba(15, 23, 42, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,50 +2,50 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[#101827] text-[#E5E7EB]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner">
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8 flex-grow">
|
<div class="container mx-auto px-4 py-8 flex-grow">
|
||||||
<!-- Error Section -->
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8 mb-10">
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-10">
|
|
||||||
<div class="max-w-4xl mx-auto text-center">
|
<div class="max-w-4xl mx-auto text-center">
|
||||||
<div class="mb-6 text-6xl text-[#38BDF8]">
|
<div class="mb-6 text-6xl text-[var(--color-primary)]">
|
||||||
<i class="fas fa-exclamation-circle"></i>
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="hero-title mb-4">
|
<h1 class="hero-title mb-4">
|
||||||
404 - Page Not Found
|
404 - Page Not Found
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-[#94A3B8] mb-6">The page you're looking for doesn't exist or has been moved</p>
|
<p class="text-xl text-[var(--color-text-secondary)] mb-6">The page you're looking for doesn't exist or has been moved</p>
|
||||||
<div class="flex flex-wrap justify-center gap-4">
|
<div class="flex flex-wrap justify-center gap-2">
|
||||||
<a href="./"
|
<a href="./" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
class="inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] font-semibold py-3 px-6 rounded-lg transition-colors">
|
<i class="fas fa-home"></i>
|
||||||
<i class="fas fa-home mr-2"></i>
|
|
||||||
<span>Return Home</span>
|
<span>Return Home</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="browse/"
|
<a href="browse/" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white font-semibold py-3 px-6 rounded-lg transition-colors">
|
<i class="fas fa-images"></i>
|
||||||
<i class="fas fa-images mr-2"></i>
|
|
||||||
<span>Browse Gallery</span>
|
<span>Browse Gallery</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Additional Information -->
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8">
|
||||||
<div class="bg-[#1E293B] border border-[#1E293B] rounded-xl p-8">
|
|
||||||
<div class="text-center max-w-3xl mx-auto">
|
<div class="text-center max-w-3xl mx-auto">
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-500/10 border border-yellow-500/20 mb-4">
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-warning-light)] border border-[var(--color-warning)]/20 mb-4">
|
||||||
<i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i>
|
<i class="text-[var(--color-warning)] text-2xl fa-solid fa-triangle-exclamation"></i>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl md:text-3xl font-semibold text-[#E5E7EB] mb-4">Looking for resources?</h2>
|
<h2 class="text-2xl md:text-3xl font-semibold text-[var(--color-text-primary)] mb-4">Looking for resources?</h2>
|
||||||
<p class="text-lg text-[#94A3B8] mb-6">Visit our <a class="text-[#38BDF8] hover:text-[#8B5CF6] underline underline-offset-2 transition-colors" href="browse">🖼️ Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[#38BDF8] hover:text-[#8B5CF6] underline underline-offset-2 transition-colors"> <i class="fa-solid fa-book"></i> Getting started documentation</a></p>
|
<p class="text-lg text-[var(--color-text-secondary)] mb-6">Visit our <a class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors" href="browse">Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors">Getting started documentation</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "views/partials/footer" .}}
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[#101827] text-[#E5E7EB]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen" x-data="jobDetails()" x-init="init()">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner" x-data="jobDetails()" x-init="init()">
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8 flex-grow max-w-6xl">
|
<div class="container mx-auto px-4 py-8 flex-grow max-w-6xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<p class="hero-subtitle">Live job status, reasoning traces, and execution details</p>
|
<p class="hero-subtitle">Live job status, reasoning traces, and execution details</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/agent-jobs" class="text-[#94A3B8] hover:text-[#E5E7EB]">
|
<a href="/agent-jobs" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
|
||||||
<i class="fas fa-arrow-left mr-2"></i>Back to Jobs
|
<i class="fas fa-arrow-left mr-2"></i>Back to Jobs
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
<!-- Job Status Card -->
|
<!-- Job Status Card -->
|
||||||
<div class="card p-8 mb-8">
|
<div class="card p-8 mb-8">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB]">Job Status</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)]">Job Status</h2>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<span :class="{
|
<span :class="{
|
||||||
'bg-yellow-500': job.status === 'pending',
|
'bg-yellow-500': job.status === 'pending',
|
||||||
@@ -37,75 +39,75 @@
|
|||||||
}"
|
}"
|
||||||
class="px-4 py-2 rounded-lg text-sm font-semibold text-white"
|
class="px-4 py-2 rounded-lg text-sm font-semibold text-white"
|
||||||
x-text="job.status ? job.status.toUpperCase() : 'LOADING...'"></span>
|
x-text="job.status ? job.status.toUpperCase() : 'LOADING...'"></span>
|
||||||
<button x-show="job.status === 'pending' || job.status === 'running'"
|
<button type="button" x-show="job.status === 'pending' || job.status === 'running'"
|
||||||
@click="cancelJob()"
|
@click="cancelJob()"
|
||||||
class="btn-primary"
|
class="inline-flex items-center gap-1.5 text-xs text-red-400/90 hover:text-red-400 bg-transparent hover:bg-red-500/10 border border-[var(--color-border-subtle)] hover:border-red-500/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
style="background: var(--color-error);">
|
<i class="fas fa-stop"></i>
|
||||||
<i class="fas fa-stop mr-2"></i>Cancel
|
<span>Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Job ID</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Job ID</label>
|
||||||
<div class="font-mono text-[#E5E7EB] mt-1" x-text="job.id || '-'"></div>
|
<div class="font-mono text-[var(--color-text-primary)] mt-1" x-text="job.id || '-'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Task</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Task</label>
|
||||||
<div class="text-[#E5E7EB] mt-1" x-text="task ? task.name : (job.task_id || '-')"></div>
|
<div class="text-[var(--color-text-primary)] mt-1" x-text="task ? task.name : (job.task_id || '-')"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Created</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Created</label>
|
||||||
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.created_at)"></div>
|
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.created_at)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Started</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Started</label>
|
||||||
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.started_at)"></div>
|
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.started_at)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Completed</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Completed</label>
|
||||||
<div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.completed_at)"></div>
|
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.completed_at)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Triggered By</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Triggered By</label>
|
||||||
<div class="text-[#E5E7EB] mt-1" x-text="job.triggered_by || '-'"></div>
|
<div class="text-[var(--color-text-primary)] mt-1" x-text="job.triggered_by || '-'"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Agent Prompt Template -->
|
<!-- Agent Prompt Template -->
|
||||||
<div class="card p-8 mb-8" x-show="task && task.prompt">
|
<div class="card p-8 mb-8" x-show="task && task.prompt">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Agent Prompt Template</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Agent Prompt Template</h2>
|
||||||
<p class="text-sm text-[#94A3B8] mb-4">The original prompt template from the task definition.</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">The original prompt template from the task definition.</p>
|
||||||
<div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap font-mono text-sm" x-text="task.prompt"></div>
|
<div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap font-mono text-sm" x-text="task.prompt"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cron Parameters -->
|
<!-- Cron Parameters -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.triggered_by === 'cron' && task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.triggered_by === 'cron' && task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Parameters</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Cron Parameters</h2>
|
||||||
<p class="text-sm text-[#94A3B8] mb-4">Parameters configured for cron-triggered executions of this task.</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">Parameters configured for cron-triggered executions of this task.</p>
|
||||||
<pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] text-sm overflow-x-auto" x-text="JSON.stringify(task.cron_parameters, null, 2)"></pre>
|
<pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm overflow-x-auto" x-text="JSON.stringify(task.cron_parameters, null, 2)"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Parameters -->
|
<!-- Parameters -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.parameters && Object.keys(job.parameters).length > 0">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.parameters && Object.keys(job.parameters).length > 0">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Job Parameters</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Job Parameters</h2>
|
||||||
<p class="text-sm text-[#94A3B8] mb-4">Parameters used for this specific job execution.</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">Parameters used for this specific job execution.</p>
|
||||||
<pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] text-sm overflow-x-auto" x-text="JSON.stringify(job.parameters, null, 2)"></pre>
|
<pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm overflow-x-auto" x-text="JSON.stringify(job.parameters, null, 2)"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rendered Job Prompt -->
|
<!-- Rendered Job Prompt -->
|
||||||
<div class="card p-8 mb-8" x-show="task && task.prompt">
|
<div class="card p-8 mb-8" x-show="task && task.prompt">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Rendered Job Prompt</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Rendered Job Prompt</h2>
|
||||||
<p class="text-sm text-[#94A3B8] mb-4">The prompt with parameters substituted, as it was sent to the agent.</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">The prompt with parameters substituted, as it was sent to the agent.</p>
|
||||||
<div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap" x-text="getRenderedPrompt()"></div>
|
<div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap" x-text="getRenderedPrompt()"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result -->
|
<!-- Result -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.result">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.result">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Result</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Result</h2>
|
||||||
<div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap" x-text="job.result"></div>
|
<div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap" x-text="job.result"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Error -->
|
<!-- Error -->
|
||||||
@@ -115,18 +117,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reasoning Traces & Actions -->
|
<!-- Reasoning Traces & Actions -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Execution Traces</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Execution Traces</h2>
|
||||||
<div x-show="!traces || traces.length === 0" class="text-[#94A3B8] text-center py-8">
|
<div x-show="!traces || traces.length === 0" class="text-[var(--color-text-secondary)] text-center py-8">
|
||||||
<i class="fas fa-info-circle text-2xl mb-2"></i>
|
<i class="fas fa-info-circle text-2xl mb-2"></i>
|
||||||
<p>No execution traces available yet. Traces will appear here as the job executes.</p>
|
<p>No execution traces available yet. Traces will appear here as the job executes.</p>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="traces && traces.length > 0" class="space-y-4">
|
<div x-show="traces && traces.length > 0" class="space-y-4">
|
||||||
<template x-for="(trace, index) in traces" :key="index">
|
<template x-for="(trace, index) in traces" :key="index">
|
||||||
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
|
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<span class="text-xs text-[#94A3B8] font-mono" x-text="'Step ' + (index + 1)"></span>
|
<span class="text-xs text-[var(--color-text-secondary)] font-mono" x-text="'Step ' + (index + 1)"></span>
|
||||||
<span class="text-xs px-2 py-1 rounded"
|
<span class="text-xs px-2 py-1 rounded"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-blue-500/20 text-blue-400': trace.type === 'reasoning',
|
'bg-blue-500/20 text-blue-400': trace.type === 'reasoning',
|
||||||
@@ -136,14 +138,14 @@
|
|||||||
}"
|
}"
|
||||||
x-text="trace.type"></span>
|
x-text="trace.type"></span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs text-[#94A3B8]" x-text="formatTime(trace.timestamp)"></span>
|
<span class="text-xs text-[var(--color-text-secondary)]" x-text="formatTime(trace.timestamp)"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[#E5E7EB] text-sm" x-text="trace.content"></div>
|
<div class="text-[var(--color-text-primary)] text-sm" x-text="trace.content"></div>
|
||||||
<div x-show="trace.tool_name" class="mt-2 text-xs text-[#94A3B8]">
|
<div x-show="trace.tool_name" class="mt-2 text-xs text-[var(--color-text-secondary)]">
|
||||||
<span class="font-semibold">Tool:</span> <span x-text="trace.tool_name"></span>
|
<span class="font-semibold">Tool:</span> <span x-text="trace.tool_name"></span>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="trace.arguments" class="mt-2">
|
<div x-show="trace.arguments" class="mt-2">
|
||||||
<pre class="text-xs text-[#94A3B8] bg-[#0A0E1A] p-2 rounded overflow-x-auto" x-text="JSON.stringify(trace.arguments, null, 2)"></pre>
|
<pre class="text-xs text-[var(--color-text-secondary)] bg-[var(--color-bg-tertiary)] p-2 rounded overflow-x-auto" x-text="JSON.stringify(trace.arguments, null, 2)"></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -151,16 +153,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Webhook Status -->
|
<!-- Webhook Status -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.webhook_sent !== undefined || job.webhook_error">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.webhook_sent !== undefined || job.webhook_error">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhook Status</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhook Status</h2>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<span :class="job.webhook_sent && !job.webhook_error ? 'text-green-400' : (job.webhook_error ? 'text-yellow-400' : 'text-gray-400')">
|
<span :class="job.webhook_sent && !job.webhook_error ? 'text-[var(--color-success)]' : (job.webhook_error ? 'text-[var(--color-warning)]' : 'text-[var(--color-text-muted)]')">
|
||||||
<i class="fas" :class="job.webhook_sent && !job.webhook_error ? 'fa-check-circle' : (job.webhook_error ? 'fa-exclamation-triangle' : 'fa-clock')"></i>
|
<i class="fas" :class="job.webhook_sent && !job.webhook_error ? 'fa-check-circle' : (job.webhook_error ? 'fa-exclamation-triangle' : 'fa-clock')"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[#E5E7EB]"
|
<span class="text-[var(--color-text-primary)]"
|
||||||
x-text="job.webhook_sent && !job.webhook_error ? 'All webhooks sent successfully' : (job.webhook_error ? 'Webhook delivery had errors' : 'Webhook pending')"></span>
|
x-text="job.webhook_sent && !job.webhook_error ? 'All webhooks sent successfully' : (job.webhook_error ? 'Webhook delivery had errors' : 'Webhook pending')"></span>
|
||||||
<span x-show="job.webhook_sent_at" class="text-[#94A3B8] text-sm" x-text="'at ' + formatDate(job.webhook_sent_at)"></span>
|
<span x-show="job.webhook_sent_at" class="text-[var(--color-text-secondary)] text-sm" x-text="'at ' + formatDate(job.webhook_sent_at)"></span>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="job.webhook_error" class="bg-red-900/20 border border-red-500/20 rounded-lg p-4">
|
<div x-show="job.webhook_error" class="bg-red-900/20 border border-red-500/20 rounded-lg p-4">
|
||||||
<div class="flex items-start space-x-2">
|
<div class="flex items-start space-x-2">
|
||||||
@@ -320,7 +322,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
@@ -3,9 +3,11 @@
|
|||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen" x-data="agentJobs()" x-init="init()">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner" x-data="agentJobs()" x-init="init()">
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8 flex-grow">
|
<div class="container mx-auto px-4 py-8 flex-grow">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -140,13 +142,13 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<template x-for="model in availableModels" :key="model.name">
|
<template x-for="model in availableModels" :key="model.name">
|
||||||
<div class="flex items-center justify-between p-3 bg-[#0A0E1A] rounded-lg border border-[var(--color-primary-border)]/10">
|
<div class="flex items-center justify-between p-3 bg-[var(--color-bg-secondary)] rounded-lg border border-[var(--color-border-subtle)]">
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<i class="fas fa-cube text-[var(--color-primary)]"></i>
|
<i class="fas fa-cube text-[var(--color-primary)]"></i>
|
||||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="model.name"></span>
|
<span class="text-[var(--color-text-primary)] font-medium" x-text="model.name"></span>
|
||||||
</div>
|
</div>
|
||||||
<a :href="'/models/edit/' + model.name"
|
<a :href="'/models/edit/' + model.name"
|
||||||
class="inline-flex items-center bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-lg transition-colors text-sm">
|
class="inline-flex items-center bg-[var(--color-warning)] hover:bg-[var(--color-warning)]/80 text-white px-4 py-2 rounded-lg transition-colors text-sm">
|
||||||
<i class="fas fa-edit mr-2"></i>
|
<i class="fas fa-edit mr-2"></i>
|
||||||
Configure MCP
|
Configure MCP
|
||||||
</a>
|
</a>
|
||||||
@@ -759,7 +761,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
@@ -2,20 +2,22 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[#101827] text-[#E5E7EB]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen" x-data="taskDetails()" x-init="init()">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner" x-data="taskDetails()" x-init="init()">
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8 flex-grow max-w-6xl">
|
<div class="container mx-auto px-4 py-8 flex-grow max-w-6xl">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="hero-title">
|
<h1 class="hero-title">
|
||||||
<span x-text="isNewTask ? 'Create Task' : (isEditMode ? 'Edit Task' : 'Task Details')"></span>
|
<span x-text="isNewTask ? 'Create Task' : (isEditMode ? 'Edit Task' : 'Task Details')"></span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-[#94A3B8]" x-text="isNewTask ? 'Create a new agent task' : (task ? task.name : 'Loading...')"></p>
|
<p class="text-lg text-[var(--color-text-secondary)]" x-text="isNewTask ? 'Create a new agent task' : (task ? task.name : 'Loading...')"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<template x-if="!isNewTask && !isEditMode">
|
<template x-if="!isNewTask && !isEditMode">
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
<template x-if="isEditMode || isNewTask">
|
<template x-if="isEditMode || isNewTask">
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<button @click="cancelEdit()"
|
<button @click="cancelEdit()"
|
||||||
class="bg-[#1E293B] hover:bg-[#2D3A4F] text-white px-4 py-2 rounded-lg transition-colors">
|
class="bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-white px-4 py-2 rounded-lg transition-colors">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button @click="saveTask()"
|
<button @click="saveTask()"
|
||||||
@@ -46,7 +48,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<a href="/agent-jobs" class="text-[#94A3B8] hover:text-[#E5E7EB] px-4 py-2">
|
<a href="/agent-jobs" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-4 py-2">
|
||||||
<i class="fas fa-arrow-left mr-2"></i>Back
|
<i class="fas fa-arrow-left mr-2"></i>Back
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,62 +59,62 @@
|
|||||||
<template x-if="isEditMode || isNewTask">
|
<template x-if="isEditMode || isNewTask">
|
||||||
<form @submit.prevent="saveTask()" class="space-y-8">
|
<form @submit.prevent="saveTask()" class="space-y-8">
|
||||||
<!-- Basic Information -->
|
<!-- Basic Information -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Basic Information</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Basic Information</h2>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Name *</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Name *</label>
|
||||||
<input type="text" x-model="taskForm.name" required
|
<input type="text" x-model="taskForm.name" required
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50">
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Description</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Description</label>
|
||||||
<textarea x-model="taskForm.description" rows="3"
|
<textarea x-model="taskForm.description" rows="3"
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Model *</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Model *</label>
|
||||||
<select x-model="taskForm.model" required
|
<select x-model="taskForm.model" required
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50">
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
|
||||||
<option value="">Select a model with MCP configuration...</option>
|
<option value="">Select a model with MCP configuration...</option>
|
||||||
{{ range .ModelsConfig }}
|
{{ range .ModelsConfig }}
|
||||||
{{ $cfg := . }}
|
{{ $cfg := . }}
|
||||||
{{ $hasMCP := or (ne $cfg.MCP.Servers "") (ne $cfg.MCP.Stdio "") }}
|
{{ $hasMCP := or (ne $cfg.MCP.Servers "") (ne $cfg.MCP.Stdio "") }}
|
||||||
{{ if $hasMCP }}
|
{{ if $hasMCP }}
|
||||||
<option value="{{$cfg.Name}}" class="bg-[#1E293B] text-[#E5E7EB]">{{$cfg.Name}}</option>
|
<option value="{{$cfg.Name}}" class="bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)]">{{$cfg.Name}}</option>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</select>
|
</select>
|
||||||
<p class="text-sm text-[#94A3B8] mt-1">Only models with MCP configuration are shown</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mt-1">Only models with MCP configuration are shown</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" x-model="taskForm.enabled"
|
<input type="checkbox" x-model="taskForm.enabled"
|
||||||
class="mr-2">
|
class="mr-2">
|
||||||
<span class="text-[#E5E7EB]">Enabled</span>
|
<span class="text-[var(--color-text-primary)]">Enabled</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Prompt Template -->
|
<!-- Prompt Template -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Prompt Template</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Prompt Template</h2>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Prompt *</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Prompt *</label>
|
||||||
<p class="text-sm text-[#94A3B8] mb-4">
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||||
Use Go template syntax with <code class="bg-[#101827] px-1.5 py-0.5 rounded text-[#38BDF8]">{{"{{"}}.param{{"}}"}}</code> for dynamic parameters.
|
Use Go template syntax with <code class="bg-[var(--color-bg-primary)] px-1.5 py-0.5 rounded text-[var(--color-primary)]">{{"{{"}}.param{{"}}"}}</code> for dynamic parameters.
|
||||||
Parameters are provided when executing the job and will be substituted into the prompt.
|
Parameters are provided when executing the job and will be substituted into the prompt.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Example Prompt -->
|
<!-- Example Prompt -->
|
||||||
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4 mb-4">
|
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4 mb-4">
|
||||||
<p class="text-xs text-[#94A3B8] mb-2 font-semibold">Example Prompt:</p>
|
<p class="text-xs text-[var(--color-text-secondary)] mb-2 font-semibold">Example Prompt:</p>
|
||||||
<pre class="text-xs text-[#E5E7EB] font-mono whitespace-pre-wrap">You are a helpful assistant. The user's name is {{"{{"}}.user_name{{"}}"}} and they work as a {{"{{"}}.job_title{{"}}"}}.
|
<pre class="text-xs text-[var(--color-text-primary)] font-mono whitespace-pre-wrap">You are a helpful assistant. The user's name is {{"{{"}}.user_name{{"}}"}} and they work as a {{"{{"}}.job_title{{"}}"}}.
|
||||||
|
|
||||||
Please help them with the following task: {{"{{"}}.task_description{{"}}"}}
|
Please help them with the following task: {{"{{"}}.task_description{{"}}"}}
|
||||||
|
|
||||||
@@ -121,8 +123,8 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
|
|
||||||
<textarea x-model="taskForm.prompt" required rows="12"
|
<textarea x-model="taskForm.prompt" required rows="12"
|
||||||
placeholder="Enter your prompt template here. Use {{.parameter_name}} to reference parameters that will be provided when the job executes."
|
placeholder="Enter your prompt template here. Use {{.parameter_name}} to reference parameters that will be provided when the job executes."
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<p class="text-xs text-[#94A3B8] mt-2">
|
<p class="text-xs text-[var(--color-text-secondary)] mt-2">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
The prompt will be processed as a Go template. All parameters passed during job execution will be available as template variables.
|
The prompt will be processed as a Go template. All parameters passed during job execution will be available as template variables.
|
||||||
</p>
|
</p>
|
||||||
@@ -130,25 +132,25 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cron Schedule -->
|
<!-- Cron Schedule -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Schedule (Optional)</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Cron Schedule (Optional)</h2>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Cron Expression</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Cron Expression</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
x-model="taskForm.cron"
|
x-model="taskForm.cron"
|
||||||
@blur="validateCron(taskForm.cron)"
|
@blur="validateCron(taskForm.cron)"
|
||||||
@input="cronError = ''"
|
@input="cronError = ''"
|
||||||
placeholder="0 0 * * * (daily at midnight)"
|
placeholder="0 0 * * * (daily at midnight)"
|
||||||
:class="cronError ? 'w-full bg-[#101827] border border-red-500 rounded px-4 py-2 text-[#E5E7EB] focus:border-red-500 focus:ring-2 focus:ring-red-500/50' : 'w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50'">
|
:class="cronError ? 'w-full bg-[var(--color-bg-primary)] border border-red-500 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-red-500 focus:ring-2 focus:ring-red-500/50' : 'w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50'">
|
||||||
<p class="text-sm text-[#94A3B8] mt-1">Standard 5-field cron format (minute hour day month weekday)</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mt-1">Standard 5-field cron format (minute hour day month weekday)</p>
|
||||||
<p x-show="cronError" class="text-sm text-red-400 mt-2" x-text="cronError"></p>
|
<p x-show="cronError" class="text-sm text-red-400 mt-2" x-text="cronError"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cron Parameters -->
|
<!-- Cron Parameters -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Cron Parameters (Optional)</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Cron Parameters (Optional)</label>
|
||||||
<p class="text-sm text-[#94A3B8] mb-3">
|
<p class="text-sm text-[var(--color-text-secondary)] mb-3">
|
||||||
Parameters to use when executing jobs triggered by cron. These will be used to template the prompt.
|
Parameters to use when executing jobs triggered by cron. These will be used to template the prompt.
|
||||||
Enter as key-value pairs (one per line, format: key=value).
|
Enter as key-value pairs (one per line, format: key=value).
|
||||||
</p>
|
</p>
|
||||||
@@ -156,27 +158,27 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
@input="updateCronParameters()"
|
@input="updateCronParameters()"
|
||||||
rows="6"
|
rows="6"
|
||||||
placeholder="user_name=Alice job_title=Software Engineer task_description=Daily status report"
|
placeholder="user_name=Alice job_title=Software Engineer task_description=Daily status report"
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<p class="text-xs text-[#94A3B8] mt-1">
|
<p class="text-xs text-[var(--color-text-secondary)] mt-1">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">user_name=Alice</code>
|
Example: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">user_name=Alice</code>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multimedia Sources Configuration -->
|
<!-- Multimedia Sources Configuration -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Multimedia Sources (Optional)</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Multimedia Sources (Optional)</h2>
|
||||||
<p class="text-sm text-[#94A3B8] mb-4">
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||||
Configure multimedia sources (images, videos, audios, files) to fetch when cron jobs execute.
|
Configure multimedia sources (images, videos, audios, files) to fetch when cron jobs execute.
|
||||||
Each source can have custom headers for authentication/authorization. These will be fetched and included in the job execution.
|
Each source can have custom headers for authentication/authorization. These will be fetched and included in the job execution.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<template x-for="(source, index) in taskForm.multimedia_sources" :key="index">
|
<template x-for="(source, index) in taskForm.multimedia_sources" :key="index">
|
||||||
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10">
|
<div class="bg-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h3 class="text-lg font-semibold text-[#E5E7EB]">Multimedia Source <span x-text="index + 1"></span></h3>
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Multimedia Source <span x-text="index + 1"></span></h3>
|
||||||
<button type="button" @click="taskForm.multimedia_sources.splice(index, 1)"
|
<button type="button" @click="taskForm.multimedia_sources.splice(index, 1)"
|
||||||
class="text-red-400 hover:text-red-300">
|
class="text-red-400 hover:text-red-300">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
@@ -184,9 +186,9 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Type *</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Type *</label>
|
||||||
<select x-model="source.type" required
|
<select x-model="source.type" required
|
||||||
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50">
|
class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
|
||||||
<option value="">Select type...</option>
|
<option value="">Select type...</option>
|
||||||
<option value="image">Image</option>
|
<option value="image">Image</option>
|
||||||
<option value="video">Video</option>
|
<option value="video">Video</option>
|
||||||
@@ -195,40 +197,40 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">URL *</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">URL *</label>
|
||||||
<input type="url" x-model="source.url" required
|
<input type="url" x-model="source.url" required
|
||||||
placeholder="https://example.com/image.png"
|
placeholder="https://example.com/image.png"
|
||||||
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50">
|
class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
|
||||||
<p class="text-xs text-[#94A3B8] mt-1">URL where multimedia content will be fetched from</p>
|
<p class="text-xs text-[var(--color-text-secondary)] mt-1">URL where multimedia content will be fetched from</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Headers (JSON)</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Headers (JSON)</label>
|
||||||
<textarea x-model="source.headers_json" rows="3"
|
<textarea x-model="source.headers_json" rows="3"
|
||||||
placeholder='{"Authorization": "Bearer token"}'
|
placeholder='{"Authorization": "Bearer token"}'
|
||||||
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<p class="text-xs text-[#94A3B8] mt-1">Custom headers for the HTTP request (e.g., Authorization)</p>
|
<p class="text-xs text-[var(--color-text-secondary)] mt-1">Custom headers for the HTTP request (e.g., Authorization)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<button type="button" @click="addMultimediaSource()"
|
<button type="button" @click="addMultimediaSource()"
|
||||||
class="w-full bg-[#101827] hover:bg-[#0A0E1A] border border-[#38BDF8]/20 border-dashed rounded-lg p-4 text-[#94A3B8] hover:text-[#E5E7EB] transition-colors">
|
class="w-full bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 border-dashed rounded-lg p-4 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
|
||||||
<i class="fas fa-plus mr-2"></i>Add Multimedia Source
|
<i class="fas fa-plus mr-2"></i>Add Multimedia Source
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Webhook Configuration -->
|
<!-- Webhook Configuration -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhooks (Optional)</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhooks (Optional)</h2>
|
||||||
<p class="text-sm text-[#94A3B8] mb-4">
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||||
Configure webhook URLs to receive notifications when jobs complete. You can add multiple webhooks, each with custom headers and HTTP methods.
|
Configure webhook URLs to receive notifications when jobs complete. You can add multiple webhooks, each with custom headers and HTTP methods.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<template x-for="(webhook, index) in taskForm.webhooks" :key="index">
|
<template x-for="(webhook, index) in taskForm.webhooks" :key="index">
|
||||||
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10">
|
<div class="bg-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h3 class="text-lg font-semibold text-[#E5E7EB]">Webhook <span x-text="index + 1"></span></h3>
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Webhook <span x-text="index + 1"></span></h3>
|
||||||
<button type="button" @click="taskForm.webhooks.splice(index, 1)"
|
<button type="button" @click="taskForm.webhooks.splice(index, 1)"
|
||||||
class="text-red-400 hover:text-red-300">
|
class="text-red-400 hover:text-red-300">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
@@ -236,35 +238,35 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">URL *</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">URL *</label>
|
||||||
<input type="url" x-model="webhook.url" required
|
<input type="url" x-model="webhook.url" required
|
||||||
placeholder="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
|
placeholder="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
|
||||||
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50">
|
class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
|
||||||
<p class="text-xs text-[#94A3B8] mt-1">URL where webhook notifications will be sent</p>
|
<p class="text-xs text-[var(--color-text-secondary)] mt-1">URL where webhook notifications will be sent</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">HTTP Method</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">HTTP Method</label>
|
||||||
<select x-model="webhook.method"
|
<select x-model="webhook.method"
|
||||||
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50">
|
class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50">
|
||||||
<option value="POST">POST</option>
|
<option value="POST">POST</option>
|
||||||
<option value="PUT">PUT</option>
|
<option value="PUT">PUT</option>
|
||||||
<option value="PATCH">PATCH</option>
|
<option value="PATCH">PATCH</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Headers (JSON)</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Headers (JSON)</label>
|
||||||
<textarea x-model="webhook.headers_json" rows="3"
|
<textarea x-model="webhook.headers_json" rows="3"
|
||||||
placeholder='{"Authorization": "Bearer token", "Content-Type": "application/json"}'
|
placeholder='{"Authorization": "Bearer token", "Content-Type": "application/json"}'
|
||||||
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<p class="text-xs text-[#94A3B8] mt-1">Custom headers for the webhook request (e.g., Authorization)</p>
|
<p class="text-xs text-[var(--color-text-secondary)] mt-1">Custom headers for the webhook request (e.g., Authorization)</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-[#E5E7EB] mb-2">Custom Payload Template (Optional)</label>
|
<label class="block text-[var(--color-text-primary)] mb-2">Custom Payload Template (Optional)</label>
|
||||||
<p class="text-xs text-[#94A3B8] mb-2">Customize the webhook payload using Go template syntax. Available variables: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Job</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Task</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Result</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Error</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Status</code></p>
|
<p class="text-xs text-[var(--color-text-secondary)] mb-2">Customize the webhook payload using Go template syntax. Available variables: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Job</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Task</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Result</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Error</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Status</code></p>
|
||||||
<p class="text-xs text-[#94A3B8] mb-2">Note: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Error</code> will be empty string if job succeeded, or contain the error message if it failed. Use this to handle both success and failure cases in a single webhook.</p>
|
<p class="text-xs text-[var(--color-text-secondary)] mb-2">Note: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Error</code> will be empty string if job succeeded, or contain the error message if it failed. Use this to handle both success and failure cases in a single webhook.</p>
|
||||||
<div class="bg-[#0A0E1A] border border-[#38BDF8]/10 rounded-lg p-3 mb-2">
|
<div class="bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/10 rounded-lg p-3 mb-2">
|
||||||
<p class="text-xs text-[#94A3B8] mb-1 font-semibold">Example (Slack with error handling):</p>
|
<p class="text-xs text-[var(--color-text-secondary)] mb-1 font-semibold">Example (Slack with error handling):</p>
|
||||||
<pre class="text-xs text-[#E5E7EB] font-mono whitespace-pre-wrap">{
|
<pre class="text-xs text-[var(--color-text-primary)] font-mono whitespace-pre-wrap">{
|
||||||
"text": "Job {{.Job.ID}} {{if .Error}}failed{{else}}completed{{end}}",
|
"text": "Job {{.Job.ID}} {{if .Error}}failed{{else}}completed{{end}}",
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{
|
{
|
||||||
@@ -279,13 +281,13 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
</div>
|
</div>
|
||||||
<textarea x-model="webhook.payload_template" rows="5"
|
<textarea x-model="webhook.payload_template" rows="5"
|
||||||
placeholder='{"text": "Job {{.Job.ID}} completed with status {{.Status}}", "error": "{{.Error}}"}'
|
placeholder='{"text": "Job {{.Job.ID}} completed with status {{.Status}}", "error": "{{.Error}}"}'
|
||||||
class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<button type="button" @click="addWebhook()"
|
<button type="button" @click="addWebhook()"
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 hover:border-[#38BDF8]/40 rounded px-4 py-3 text-[#38BDF8] transition-colors">
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 hover:border-[var(--color-primary)]/40 rounded px-4 py-3 text-[var(--color-primary)] transition-colors">
|
||||||
<i class="fas fa-plus mr-2"></i>Add Webhook
|
<i class="fas fa-plus mr-2"></i>Add Webhook
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -297,15 +299,15 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
<!-- Task Information (always visible when not in edit mode and not creating new task) -->
|
<!-- Task Information (always visible when not in edit mode and not creating new task) -->
|
||||||
<div x-show="!isEditMode && !isNewTask" x-cloak>
|
<div x-show="!isEditMode && !isNewTask" x-cloak>
|
||||||
<!-- Task Information -->
|
<!-- Task Information -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Task Information</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Task Information</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Name</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Name</label>
|
||||||
<div class="text-[#E5E7EB] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></div>
|
<div class="text-[var(--color-text-primary)] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Status</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Status</label>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<span :class="task && task.enabled ? 'bg-green-500' : 'bg-gray-500'"
|
<span :class="task && task.enabled ? 'bg-green-500' : 'bg-gray-500'"
|
||||||
class="px-2 py-1 rounded text-xs text-white"
|
class="px-2 py-1 rounded text-xs text-white"
|
||||||
@@ -313,10 +315,10 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Model</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Model</label>
|
||||||
<div class="mt-1 flex items-center space-x-2">
|
<div class="mt-1 flex items-center space-x-2">
|
||||||
<a :href="task ? '/chat/' + task.model + '?mcp=true' : '#'"
|
<a :href="task ? '/chat/' + task.model + '?mcp=true' : '#'"
|
||||||
class="text-[#38BDF8] hover:text-[#38BDF8]/80 hover:underline"
|
class="text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline"
|
||||||
x-text="task ? task.model : '-'"></a>
|
x-text="task ? task.model : '-'"></a>
|
||||||
<a :href="task ? '/models/edit/' + task.model : '#'"
|
<a :href="task ? '/models/edit/' + task.model : '#'"
|
||||||
class="text-yellow-400 hover:text-yellow-300"
|
class="text-yellow-400 hover:text-yellow-300"
|
||||||
@@ -326,47 +328,47 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Cron Schedule</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Cron Schedule</label>
|
||||||
<div class="text-[#E5E7EB] mt-1 font-mono text-sm" x-text="task && task.cron ? task.cron : '-'"></div>
|
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm" x-text="task && task.cron ? task.cron : '-'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2" x-show="task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0">
|
<div class="md:col-span-2" x-show="task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0">
|
||||||
<label class="text-[#94A3B8] text-sm">Cron Parameters</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Cron Parameters</label>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<template x-for="(value, key) in task.cron_parameters" :key="key">
|
<template x-for="(value, key) in task.cron_parameters" :key="key">
|
||||||
<div class="text-[#E5E7EB] text-sm mb-1">
|
<div class="text-[var(--color-text-primary)] text-sm mb-1">
|
||||||
<span class="font-semibold text-[#38BDF8]" x-text="key + ':'"></span>
|
<span class="font-semibold text-[var(--color-primary)]" x-text="key + ':'"></span>
|
||||||
<span x-text="value"></span>
|
<span x-text="value"></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="text-[#94A3B8] text-sm">Description</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Description</label>
|
||||||
<div class="text-[#E5E7EB] mt-1" x-text="task && task.description ? task.description : 'No description'"></div>
|
<div class="text-[var(--color-text-primary)] mt-1" x-text="task && task.description ? task.description : 'No description'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="text-[#94A3B8] text-sm">Prompt Template</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Prompt Template</label>
|
||||||
<pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] text-sm mt-1 whitespace-pre-wrap" x-text="task ? task.prompt : '-'"></pre>
|
<pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm mt-1 whitespace-pre-wrap" x-text="task ? task.prompt : '-'"></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Usage Examples -->
|
<!-- API Usage Examples -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="task && task.id">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="task && task.id">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">API Usage Examples</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">API Usage Examples</h2>
|
||||||
<p class="text-sm text-[#94A3B8] mb-4">
|
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
||||||
Use these curl commands to interact with this task programmatically.
|
Use these curl commands to interact with this task programmatically.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Execute Task by ID -->
|
<!-- Execute Task by ID -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
|
||||||
<i class="fas fa-play text-[#38BDF8] mr-2"></i>
|
<i class="fas fa-play text-[var(--color-primary)] mr-2"></i>
|
||||||
Execute Task by ID
|
Execute Task by ID
|
||||||
</h3>
|
</h3>
|
||||||
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
|
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
|
||||||
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \
|
<pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||||
-d '{
|
-d '{
|
||||||
@@ -382,12 +384,12 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
|
|
||||||
<!-- Execute Task by Name -->
|
<!-- Execute Task by Name -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
|
||||||
<i class="fas fa-code text-[#38BDF8] mr-2"></i>
|
<i class="fas fa-code text-[var(--color-primary)] mr-2"></i>
|
||||||
Execute Task by Name
|
Execute Task by Name
|
||||||
</h3>
|
</h3>
|
||||||
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
|
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
|
||||||
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/tasks/<span x-text="task ? task.name : 'task-name'"></span>/execute \
|
<pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/tasks/<span x-text="task ? task.name : 'task-name'"></span>/execute \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||||
-d '{
|
-d '{
|
||||||
@@ -396,7 +398,7 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
"task_description": "Analyze sales data"
|
"task_description": "Analyze sales data"
|
||||||
}'</code></pre>
|
}'</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-[#94A3B8] mt-2">
|
<p class="text-xs text-[var(--color-text-secondary)] mt-2">
|
||||||
<i class="fas fa-info-circle mr-1"></i>
|
<i class="fas fa-info-circle mr-1"></i>
|
||||||
The request body should be a JSON object where keys are parameter names and values are strings.
|
The request body should be a JSON object where keys are parameter names and values are strings.
|
||||||
If no body is provided, the task will execute with empty parameters.
|
If no body is provided, the task will execute with empty parameters.
|
||||||
@@ -405,12 +407,12 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
|
|
||||||
<!-- Execute Task with Multimedia -->
|
<!-- Execute Task with Multimedia -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
|
||||||
<i class="fas fa-images text-[#38BDF8] mr-2"></i>
|
<i class="fas fa-images text-[var(--color-primary)] mr-2"></i>
|
||||||
Execute Task with Multimedia (Images)
|
Execute Task with Multimedia (Images)
|
||||||
</h3>
|
</h3>
|
||||||
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
|
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
|
||||||
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \
|
<pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||||
-d '{
|
-d '{
|
||||||
@@ -425,53 +427,53 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
]
|
]
|
||||||
}'</code></pre>
|
}'</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-[#94A3B8] mt-2">
|
<p class="text-xs text-[var(--color-text-secondary)] mt-2">
|
||||||
You can provide multimedia content as URLs or base64-encoded data URIs. Supported types: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">images</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">videos</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">audios</code>, and <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">files</code>.
|
You can provide multimedia content as URLs or base64-encoded data URIs. Supported types: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">images</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">videos</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">audios</code>, and <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">files</code>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Check Job Status -->
|
<!-- Check Job Status -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center">
|
||||||
<i class="fas fa-info-circle text-[#38BDF8] mr-2"></i>
|
<i class="fas fa-info-circle text-[var(--color-primary)] mr-2"></i>
|
||||||
Check Job Status
|
Check Job Status
|
||||||
</h3>
|
</h3>
|
||||||
<div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
|
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4">
|
||||||
<pre class="text-xs text-[#E5E7EB] font-mono overflow-x-auto"><code>curl -X GET {{ .BaseURL }}api/agent/jobs/JOB_ID \
|
<pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X GET {{ .BaseURL }}api/agent/jobs/JOB_ID \
|
||||||
-H "Authorization: Bearer YOUR_API_KEY"</code></pre>
|
-H "Authorization: Bearer YOUR_API_KEY"</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-[#94A3B8] mt-2">
|
<p class="text-xs text-[var(--color-text-secondary)] mt-2">
|
||||||
After executing a task, you will receive a <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">job_id</code> in the response. Use it to query the job's status and results.
|
After executing a task, you will receive a <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">job_id</code> in the response. Use it to query the job's status and results.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Webhook Configuration (View Mode) -->
|
<!-- Webhook Configuration (View Mode) -->
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="task && task.id && task.webhooks && task.webhooks.length > 0">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="task && task.id && task.webhooks && task.webhooks.length > 0">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhook Configuration</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhook Configuration</h2>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<template x-for="(webhook, index) in task.webhooks" :key="index">
|
<template x-for="(webhook, index) in task.webhooks" :key="index">
|
||||||
<div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10">
|
<div class="bg-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10">
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<h3 class="text-lg font-semibold text-[#E5E7EB]">Webhook <span x-text="index + 1"></span></h3>
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Webhook <span x-text="index + 1"></span></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">URL</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">URL</label>
|
||||||
<div class="text-[#E5E7EB] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div>
|
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[#94A3B8] text-sm">Method</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Method</label>
|
||||||
<div class="text-[#E5E7EB] mt-1 font-mono text-sm" x-text="webhook.method || 'POST'"></div>
|
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm" x-text="webhook.method || 'POST'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="webhook.headers && Object.keys(webhook.headers).length > 0">
|
<div x-show="webhook.headers && Object.keys(webhook.headers).length > 0">
|
||||||
<label class="text-[#94A3B8] text-sm">Headers</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Headers</label>
|
||||||
<pre class="bg-[#0A0E1A] p-3 rounded text-[#E5E7EB] text-xs mt-1 overflow-x-auto" x-text="JSON.stringify(webhook.headers, null, 2)"></pre>
|
<pre class="bg-[var(--color-bg-tertiary)] p-3 rounded text-[var(--color-text-primary)] text-xs mt-1 overflow-x-auto" x-text="JSON.stringify(webhook.headers, null, 2)"></pre>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="webhook.payload_template">
|
<div x-show="webhook.payload_template">
|
||||||
<label class="text-[#94A3B8] text-sm">Payload Template</label>
|
<label class="text-[var(--color-text-secondary)] text-sm">Payload Template</label>
|
||||||
<pre class="bg-[#0A0E1A] p-3 rounded text-[#E5E7EB] text-xs mt-1 whitespace-pre-wrap overflow-x-auto" x-text="webhook.payload_template"></pre>
|
<pre class="bg-[var(--color-bg-tertiary)] p-3 rounded text-[var(--color-text-primary)] text-xs mt-1 whitespace-pre-wrap overflow-x-auto" x-text="webhook.payload_template"></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -482,12 +484,12 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
|
|
||||||
<!-- Jobs for this Task (visible when not creating new task and not in edit mode) -->
|
<!-- Jobs for this Task (visible when not creating new task and not in edit mode) -->
|
||||||
<template x-if="!isNewTask && !isEditMode">
|
<template x-if="!isNewTask && !isEditMode">
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8">
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB]">Job History</h2>
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)]">Job History</h2>
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<select x-model="jobFilter" @change="fetchJobs()"
|
<select x-model="jobFilter" @change="fetchJobs()"
|
||||||
class="bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB]">
|
class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)]">
|
||||||
<option value="">All Status</option>
|
<option value="">All Status</option>
|
||||||
<option value="pending">Pending</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="running">Running</option>
|
<option value="running">Running</option>
|
||||||
@@ -505,20 +507,20 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-[#38BDF8]/20">
|
<tr class="border-b border-[var(--color-primary)]/20">
|
||||||
<th class="text-left py-3 px-4 text-[#94A3B8]">Job ID</th>
|
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Job ID</th>
|
||||||
<th class="text-left py-3 px-4 text-[#94A3B8]">Status</th>
|
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Status</th>
|
||||||
<th class="text-left py-3 px-4 text-[#94A3B8]">Created</th>
|
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Created</th>
|
||||||
<th class="text-left py-3 px-4 text-[#94A3B8]">Triggered By</th>
|
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Triggered By</th>
|
||||||
<th class="text-left py-3 px-4 text-[#94A3B8]">Actions</th>
|
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="job in jobs" :key="job.id">
|
<template x-for="job in jobs" :key="job.id">
|
||||||
<tr class="border-b border-[#38BDF8]/10 hover:bg-[#101827]">
|
<tr class="border-b border-[var(--color-primary)]/10 hover:bg-[var(--color-bg-primary)]">
|
||||||
<td class="py-3 px-4">
|
<td class="py-3 px-4">
|
||||||
<a :href="'/agent-jobs/jobs/' + job.id"
|
<a :href="'/agent-jobs/jobs/' + job.id"
|
||||||
class="font-mono text-sm text-[#38BDF8] hover:text-[#38BDF8]/80 hover:underline"
|
class="font-mono text-sm text-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline"
|
||||||
x-text="job.id.substring(0, 8) + '...'"
|
x-text="job.id.substring(0, 8) + '...'"
|
||||||
:title="job.id"></a>
|
:title="job.id"></a>
|
||||||
</td>
|
</td>
|
||||||
@@ -533,8 +535,8 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
class="px-2 py-1 rounded text-xs text-white"
|
class="px-2 py-1 rounded text-xs text-white"
|
||||||
x-text="job.status"></span>
|
x-text="job.status"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4 text-[#94A3B8] text-sm" x-text="formatDate(job.created_at)"></td>
|
<td class="py-3 px-4 text-[var(--color-text-secondary)] text-sm" x-text="formatDate(job.created_at)"></td>
|
||||||
<td class="py-3 px-4 text-[#94A3B8] text-sm" x-text="job.triggered_by || '-'"></td>
|
<td class="py-3 px-4 text-[var(--color-text-secondary)] text-sm" x-text="job.triggered_by || '-'"></td>
|
||||||
<td class="py-3 px-4">
|
<td class="py-3 px-4">
|
||||||
<button x-show="job.status === 'pending' || job.status === 'running'"
|
<button x-show="job.status === 'pending' || job.status === 'running'"
|
||||||
@click="cancelJob(job.id)"
|
@click="cancelJob(job.id)"
|
||||||
@@ -546,7 +548,7 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<tr x-show="jobs.length === 0">
|
<tr x-show="jobs.length === 0">
|
||||||
<td colspan="5" class="py-8 text-center text-[#94A3B8]">No jobs found for this task</td>
|
<td colspan="5" class="py-8 text-center text-[var(--color-text-secondary)]">No jobs found for this task</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -560,11 +562,11 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
x-cloak
|
x-cloak
|
||||||
@click.away="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
|
@click.away="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
|
||||||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
<div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col" @click.stop>
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col" @click.stop>
|
||||||
<div class="flex justify-between items-center p-8 pb-6 border-b border-[#38BDF8]/20">
|
<div class="flex justify-between items-center p-8 pb-6 border-b border-[var(--color-primary)]/20">
|
||||||
<h3 class="text-2xl font-semibold text-[#E5E7EB]">Execute Task</h3>
|
<h3 class="text-2xl font-semibold text-[var(--color-text-primary)]">Execute Task</h3>
|
||||||
<button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
|
<button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
|
||||||
class="text-[#94A3B8] hover:text-[#E5E7EB]">
|
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
|
||||||
<i class="fas fa-times text-xl"></i>
|
<i class="fas fa-times text-xl"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -572,20 +574,20 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
<div class="flex flex-col flex-1 min-h-0">
|
<div class="flex flex-col flex-1 min-h-0">
|
||||||
<div class="flex-1 overflow-y-auto px-8 py-6 space-y-4">
|
<div class="flex-1 overflow-y-auto px-8 py-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Task</label>
|
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Task</label>
|
||||||
<div class="text-[#94A3B8]" x-text="task.name"></div>
|
<div class="text-[var(--color-text-secondary)]" x-text="task.name"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs for Parameters and Multimedia -->
|
<!-- Tabs for Parameters and Multimedia -->
|
||||||
<div class="border-b border-[#38BDF8]/20">
|
<div class="border-b border-[var(--color-primary)]/20">
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<button @click="executeModalTab = 'parameters'"
|
<button @click="executeModalTab = 'parameters'"
|
||||||
:class="executeModalTab === 'parameters' ? 'border-b-2 border-[#38BDF8] text-[#38BDF8]' : 'text-[#94A3B8] hover:text-[#E5E7EB]'"
|
:class="executeModalTab === 'parameters' ? 'border-b-2 border-[var(--color-primary)] text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'"
|
||||||
class="px-4 py-2 font-medium transition-colors">
|
class="px-4 py-2 font-medium transition-colors">
|
||||||
Parameters
|
Parameters
|
||||||
</button>
|
</button>
|
||||||
<button @click="executeModalTab = 'multimedia'"
|
<button @click="executeModalTab = 'multimedia'"
|
||||||
:class="executeModalTab === 'multimedia' ? 'border-b-2 border-[#38BDF8] text-[#38BDF8]' : 'text-[#94A3B8] hover:text-[#E5E7EB]'"
|
:class="executeModalTab === 'multimedia' ? 'border-b-2 border-[var(--color-primary)] text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'"
|
||||||
class="px-4 py-2 font-medium transition-colors">
|
class="px-4 py-2 font-medium transition-colors">
|
||||||
Multimedia
|
Multimedia
|
||||||
</button>
|
</button>
|
||||||
@@ -594,75 +596,75 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
|
|
||||||
<!-- Parameters Tab -->
|
<!-- Parameters Tab -->
|
||||||
<div x-show="executeModalTab === 'parameters'">
|
<div x-show="executeModalTab === 'parameters'">
|
||||||
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Parameters</label>
|
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Parameters</label>
|
||||||
<p class="text-xs text-[#94A3B8] mb-3">
|
<p class="text-xs text-[var(--color-text-secondary)] mb-3">
|
||||||
Enter parameters as key-value pairs (one per line, format: key=value).
|
Enter parameters as key-value pairs (one per line, format: key=value).
|
||||||
These will be used to template the prompt.
|
These will be used to template the prompt.
|
||||||
</p>
|
</p>
|
||||||
<textarea x-model="executionParametersText"
|
<textarea x-model="executionParametersText"
|
||||||
rows="6"
|
rows="6"
|
||||||
placeholder="user_name=Alice job_title=Software Engineer task_description=Review code changes"
|
placeholder="user_name=Alice job_title=Software Engineer task_description=Review code changes"
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<p class="text-xs text-[#94A3B8] mt-1">
|
<p class="text-xs text-[var(--color-text-secondary)] mt-1">
|
||||||
Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">user_name=Alice</code>
|
Example: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">user_name=Alice</code>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Multimedia Tab -->
|
<!-- Multimedia Tab -->
|
||||||
<div x-show="executeModalTab === 'multimedia'" class="space-y-4">
|
<div x-show="executeModalTab === 'multimedia'" class="space-y-4">
|
||||||
<p class="text-xs text-[#94A3B8] mb-3">
|
<p class="text-xs text-[var(--color-text-secondary)] mb-3">
|
||||||
Provide multimedia content as URLs or base64-encoded data URIs. You can also upload files which will be converted to base64.
|
Provide multimedia content as URLs or base64-encoded data URIs. You can also upload files which will be converted to base64.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Images -->
|
<!-- Images -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Images</label>
|
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Images</label>
|
||||||
<textarea x-model="executionMultimedia.images"
|
<textarea x-model="executionMultimedia.images"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="https://example.com/image.png data:image/png;base64,iVBORw0KG..."
|
placeholder="https://example.com/image.png data:image/png;base64,iVBORw0KG..."
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<input type="file" @change="handleFileUpload($event, 'image')" accept="image/*" multiple
|
<input type="file" @change="handleFileUpload($event, 'image')" accept="image/*" multiple
|
||||||
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80">
|
class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary)]/80">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Videos -->
|
<!-- Videos -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Videos</label>
|
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Videos</label>
|
||||||
<textarea x-model="executionMultimedia.videos"
|
<textarea x-model="executionMultimedia.videos"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="https://example.com/video.mp4 data:video/mp4;base64,..."
|
placeholder="https://example.com/video.mp4 data:video/mp4;base64,..."
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<input type="file" @change="handleFileUpload($event, 'video')" accept="video/*" multiple
|
<input type="file" @change="handleFileUpload($event, 'video')" accept="video/*" multiple
|
||||||
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80">
|
class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary)]/80">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Audios -->
|
<!-- Audios -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Audios</label>
|
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Audios</label>
|
||||||
<textarea x-model="executionMultimedia.audios"
|
<textarea x-model="executionMultimedia.audios"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="https://example.com/audio.mp3 data:audio/mpeg;base64,..."
|
placeholder="https://example.com/audio.mp3 data:audio/mpeg;base64,..."
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<input type="file" @change="handleFileUpload($event, 'audio')" accept="audio/*" multiple
|
<input type="file" @change="handleFileUpload($event, 'audio')" accept="audio/*" multiple
|
||||||
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80">
|
class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary)]/80">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Files -->
|
<!-- Files -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#E5E7EB] mb-2">Files</label>
|
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Files</label>
|
||||||
<textarea x-model="executionMultimedia.files"
|
<textarea x-model="executionMultimedia.files"
|
||||||
rows="3"
|
rows="3"
|
||||||
placeholder="https://example.com/file.pdf data:application/pdf;base64,..."
|
placeholder="https://example.com/file.pdf data:application/pdf;base64,..."
|
||||||
class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
|
class="w-full bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)] font-mono text-sm focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/50"></textarea>
|
||||||
<input type="file" @change="handleFileUpload($event, 'file')" multiple
|
<input type="file" @change="handleFileUpload($event, 'file')" multiple
|
||||||
class="mt-2 text-sm text-[#94A3B8] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#38BDF8] file:text-white hover:file:bg-[#38BDF8]/80">
|
class="mt-2 text-sm text-[var(--color-text-secondary)] file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[var(--color-primary)] file:text-white hover:file:bg-[var(--color-primary)]/80">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end space-x-4 p-8 pt-6 border-t border-[#38BDF8]/20 bg-[#1E293B]">
|
<div class="flex justify-end space-x-4 p-8 pt-6 border-t border-[var(--color-primary)]/20 bg-[var(--color-bg-secondary)]">
|
||||||
<button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
|
<button @click="showExecuteTaskModal = false; executionParameters = {}; executionParametersText = ''; executionMultimedia = {images: '', videos: '', audios: '', files: ''}; executeModalTab = 'parameters'"
|
||||||
class="px-4 py-2 bg-[#101827] hover:bg-[#0A0E1A] text-[#E5E7EB] rounded-lg transition-colors">
|
class="px-4 py-2 bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-tertiary)] text-[var(--color-text-primary)] rounded-lg transition-colors">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button @click="executeTaskWithParameters()"
|
<button @click="executeTaskWithParameters()"
|
||||||
@@ -1135,6 +1137,9 @@ Provide a detailed response that addresses their specific needs.</pre>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -2,10 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[#101827] text-[#E5E7EB]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen" x-data="backendsGallery()">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner" x-data="backendsGallery()">
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
|
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
|
||||||
@@ -44,25 +46,25 @@
|
|||||||
Discover and install AI backends to power your models
|
Discover and install AI backends to power your models
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap justify-center items-center gap-6 text-sm md:text-base">
|
<div class="flex flex-wrap justify-center items-center gap-6 text-sm md:text-base">
|
||||||
<div class="flex items-center bg-[#101827] rounded-lg px-4 py-2">
|
<div class="flex items-center bg-[var(--color-bg-primary)] rounded-lg px-4 py-2">
|
||||||
<div class="w-2 h-2 bg-emerald-400 rounded-full mr-2"></div>
|
<div class="w-2 h-2 bg-[var(--color-success)] rounded-full mr-2"></div>
|
||||||
<span class="font-semibold text-emerald-300" x-text="availableBackends"></span>
|
<span class="font-semibold text-[var(--color-success)]" x-text="availableBackends"></span>
|
||||||
<span class="text-[#94A3B8] ml-1">backends available</span>
|
<span class="text-[var(--color-text-secondary)] ml-1">backends available</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="/manage" class="flex items-center bg-[#101827] hover:bg-[#1E293B] rounded-lg px-4 py-2 transition-colors border border-[#8B5CF6]/30 hover:border-[#8B5CF6]/50">
|
<a href="/manage" class="flex items-center bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-secondary)] rounded-lg px-4 py-2 transition-colors border border-[var(--color-accent)]/30 hover:border-[var(--color-accent)]/50">
|
||||||
<div class="w-2 h-2 bg-cyan-400 rounded-full mr-2"></div>
|
<div class="w-2 h-2 bg-[var(--color-primary)] rounded-full mr-2"></div>
|
||||||
<span class="font-semibold text-cyan-300" x-text="installedBackends"></span>
|
<span class="font-semibold text-[var(--color-primary)]" x-text="installedBackends"></span>
|
||||||
<span class="text-[#94A3B8] ml-1">installed</span>
|
<span class="text-[var(--color-text-secondary)] ml-1">installed</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center bg-[#101827] rounded-lg px-4 py-2 border border-[#38BDF8]/30">
|
<div class="flex items-center bg-[var(--color-bg-primary)] rounded-lg px-4 py-2 border border-[var(--color-primary-border)]">
|
||||||
<i class="fas fa-microchip text-[#38BDF8] mr-2"></i>
|
<i class="fas fa-microchip text-[var(--color-primary)] mr-2"></i>
|
||||||
<span class="text-[#94A3B8] mr-1">Capability:</span>
|
<span class="text-[var(--color-text-secondary)] mr-1">Capability:</span>
|
||||||
<span class="font-semibold text-[#38BDF8]" x-text="systemCapability"></span>
|
<span class="font-semibold text-[var(--color-primary)]" x-text="systemCapability"></span>
|
||||||
</div>
|
</div>
|
||||||
<a href="https://localai.io/backends/" target="_blank" class="btn-primary">
|
<a href="https://localai.io/backends/" target="_blank" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-info-circle mr-2"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
<span>Documentation</span>
|
<span>Documentation</span>
|
||||||
<i class="fas fa-external-link-alt ml-2 text-xs"></i>
|
<i class="fas fa-external-link-alt text-[10px]"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,55 +79,55 @@
|
|||||||
class="w-full flex items-center justify-between text-left"
|
class="w-full flex items-center justify-between text-left"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<i class="fas fa-plus-circle text-[#38BDF8] text-lg"></i>
|
<i class="fas fa-plus-circle text-[var(--color-primary)] text-lg"></i>
|
||||||
<h3 class="text-lg font-semibold text-[#E5E7EB]">Install Backend Manually</h3>
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Install Backend Manually</h3>
|
||||||
</div>
|
</div>
|
||||||
<i class="fas text-[#94A3B8] transition-transform duration-200" :class="showManualInstall ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
|
<i class="fas text-[var(--color-text-secondary)] transition-transform duration-200" :class="showManualInstall ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div x-show="showManualInstall" x-collapse>
|
<div x-show="showManualInstall" x-collapse>
|
||||||
<p class="text-sm text-[#94A3B8] mt-4 mb-6">Install a backend from an OCI image, URL, or local path</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mt-4 mb-6">Install a backend from an OCI image, URL, or local path</p>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#94A3B8] mb-2">OCI Image / URL / Path *</label>
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">OCI Image / URL / Path *</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
x-model="externalBackend.uri"
|
x-model="externalBackend.uri"
|
||||||
placeholder="e.g., oci://quay.io/example/backend:latest"
|
placeholder="e.g., oci://quay.io/example/backend:latest"
|
||||||
class="w-full px-4 py-3 text-sm bg-[#101827] border border-[#38BDF8]/30 rounded-lg text-[#E5E7EB] placeholder-[#94A3B8]/50 focus:border-[#38BDF8] focus:outline-none focus:ring-1 focus:ring-[#38BDF8]"
|
class="input w-full px-4 py-3 text-sm"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#94A3B8] mb-2">Name (required for OCI)</label>
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Name (required for OCI)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
x-model="externalBackend.name"
|
x-model="externalBackend.name"
|
||||||
placeholder="e.g., my-backend"
|
placeholder="e.g., my-backend"
|
||||||
class="w-full px-4 py-3 text-sm bg-[#101827] border border-[#38BDF8]/30 rounded-lg text-[#E5E7EB] placeholder-[#94A3B8]/50 focus:border-[#38BDF8] focus:outline-none focus:ring-1 focus:ring-[#38BDF8]"
|
class="input w-full px-4 py-3 text-sm"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[#94A3B8] mb-2">Alias (optional)</label>
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Alias (optional)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
x-model="externalBackend.alias"
|
x-model="externalBackend.alias"
|
||||||
placeholder="e.g., backend-alias"
|
placeholder="e.g., backend-alias"
|
||||||
class="w-full px-4 py-3 text-sm bg-[#101827] border border-[#38BDF8]/30 rounded-lg text-[#E5E7EB] placeholder-[#94A3B8]/50 focus:border-[#38BDF8] focus:outline-none focus:ring-1 focus:ring-[#38BDF8]"
|
class="input w-full px-4 py-3 text-sm"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<button
|
<button type="button"
|
||||||
@click="installExternalBackend()"
|
@click="installExternalBackend()"
|
||||||
:disabled="installingExternal || !externalBackend.uri"
|
:disabled="installingExternal || !externalBackend.uri"
|
||||||
class="inline-flex items-center px-5 py-2.5 rounded-lg bg-[#38BDF8] hover:bg-[#38BDF8]/80 text-sm font-medium text-white transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border-subtle)]"
|
||||||
>
|
>
|
||||||
<i class="mr-2" :class="installingExternal ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
|
<i class="text-[10px]" :class="installingExternal ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i>
|
||||||
<span x-text="installingExternal ? 'Installing...' : 'Install Backend'"></span>
|
<span x-text="installingExternal ? 'Installing...' : 'Install Backend'"></span>
|
||||||
</button>
|
</button>
|
||||||
<span x-show="externalBackendProgress" class="text-sm text-[#94A3B8]" x-text="externalBackendProgress"></span>
|
<span x-show="externalBackendProgress" class="text-sm text-[var(--color-text-secondary)]" x-text="externalBackendProgress"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,13 +137,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<!-- Search Input -->
|
<!-- Search Input -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h3 class="text-xl font-semibold text-[#E5E7EB] mb-4 flex items-center">
|
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
|
||||||
<i class="fas fa-search mr-3 text-[#8B5CF6]"></i>
|
<i class="fas fa-search mr-3 text-[var(--color-accent)]"></i>
|
||||||
Find Backend Components
|
Find Backend Components
|
||||||
</h3>
|
</h3>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none z-10">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none z-10">
|
||||||
<i class="fas fa-search text-[#94A3B8]"></i>
|
<i class="fas fa-search text-[var(--color-text-secondary)]"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
x-model="searchTerm"
|
x-model="searchTerm"
|
||||||
@@ -151,7 +153,7 @@
|
|||||||
type="search"
|
type="search"
|
||||||
placeholder="Search backends by name, description or type...">
|
placeholder="Search backends by name, description or type...">
|
||||||
<span class="absolute right-4 top-4" x-show="loading">
|
<span class="absolute right-4 top-4" x-show="loading">
|
||||||
<svg class="animate-spin h-6 w-6 text-[#8B5CF6]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg class="animate-spin h-6 w-6 text-[var(--color-accent)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -161,33 +163,33 @@
|
|||||||
|
|
||||||
<!-- Filter by Type -->
|
<!-- Filter by Type -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg font-semibold text-white mb-4 flex items-center">
|
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
|
||||||
<i class="fas fa-filter mr-3 text-teal-400"></i>
|
<i class="fas fa-filter mr-3 text-[var(--color-secondary)]"></i>
|
||||||
Filter by Backend Type
|
Filter by Backend Type
|
||||||
</h3>
|
</h3>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
<button @click="filterByTerm('llm')"
|
<button @click="filterByTerm('llm')"
|
||||||
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-indigo-600/20 hover:bg-indigo-600/30 text-indigo-300 border border-indigo-500/30 transition-colors">
|
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-text-primary)] border border-[var(--color-accent)]/30 transition-colors">
|
||||||
<i class="fas fa-brain mr-2"></i>
|
<i class="fas fa-brain mr-2"></i>
|
||||||
<span>LLM</span>
|
<span>LLM</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="filterByTerm('diffusion')"
|
<button @click="filterByTerm('diffusion')"
|
||||||
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 border border-purple-500/30 transition-colors">
|
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-text-primary)] border border-[var(--color-accent)]/30 transition-colors">
|
||||||
<i class="fas fa-image mr-2"></i>
|
<i class="fas fa-image mr-2"></i>
|
||||||
<span>Diffusion</span>
|
<span>Diffusion</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="filterByTerm('tts')"
|
<button @click="filterByTerm('tts')"
|
||||||
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 border border-blue-500/30 transition-colors">
|
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-primary-light)] hover:bg-[var(--color-primary)]/30 text-[var(--color-text-primary)] border border-[var(--color-primary-border)] transition-colors">
|
||||||
<i class="fas fa-microphone mr-2"></i>
|
<i class="fas fa-microphone mr-2"></i>
|
||||||
<span>TTS</span>
|
<span>TTS</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="filterByTerm('whisper')"
|
<button @click="filterByTerm('whisper')"
|
||||||
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-green-600/20 hover:bg-green-600/30 text-green-300 border border-green-500/30 transition-colors">
|
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-success-light)] hover:bg-[var(--color-success)]/30 text-[var(--color-success)] border border-[var(--color-success)]/30 transition-colors">
|
||||||
<i class="fas fa-headphones mr-2"></i>
|
<i class="fas fa-headphones mr-2"></i>
|
||||||
<span>Whisper</span>
|
<span>Whisper</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="filterByTerm('object-detection')"
|
<button @click="filterByTerm('object-detection')"
|
||||||
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-red-600/20 hover:bg-red-600/30 text-red-300 border border-red-500/30 transition-colors">
|
class="flex items-center justify-center rounded-lg px-4 py-3 text-sm font-semibold bg-[var(--color-error-light)] hover:bg-[var(--color-error)]/30 text-[var(--color-error)] border border-[var(--color-error)]/30 transition-colors">
|
||||||
<i class="fas fa-eye mr-2"></i>
|
<i class="fas fa-eye mr-2"></i>
|
||||||
<span>Vision</span>
|
<span>Vision</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -199,97 +201,97 @@
|
|||||||
<!-- Results Section -->
|
<!-- Results Section -->
|
||||||
<div id="search-results" class="transition-all duration-300">
|
<div id="search-results" class="transition-all duration-300">
|
||||||
<div x-show="loading && backends.length === 0" class="text-center py-12">
|
<div x-show="loading && backends.length === 0" class="text-center py-12">
|
||||||
<svg class="animate-spin h-12 w-12 text-emerald-500 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg class="animate-spin h-12 w-12 text-[var(--color-primary)] mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-gray-400">Loading backends...</p>
|
<p class="text-[var(--color-text-secondary)]">Loading backends...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-show="!loading && backends.length === 0" class="text-center py-12">
|
<div x-show="!loading && backends.length === 0" class="text-center py-12">
|
||||||
<i class="fas fa-search text-gray-500 text-4xl mb-4"></i>
|
<i class="fas fa-search text-[var(--color-text-muted)] text-4xl mb-4"></i>
|
||||||
<p class="text-gray-400">No backends found matching your criteria</p>
|
<p class="text-[var(--color-text-secondary)]">No backends found matching your criteria</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Table View -->
|
<!-- Table View -->
|
||||||
<div x-show="backends.length > 0" class="bg-[#1E293B] rounded-2xl border border-[#38BDF8]/20 overflow-hidden shadow-xl backdrop-blur-sm">
|
<div x-show="backends.length > 0" class="bg-[var(--color-bg-secondary)] rounded-2xl border border-[var(--color-border-subtle)] overflow-hidden shadow-xl backdrop-blur-sm">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full">
|
<table class="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gradient-to-r from-[#38BDF8]/20 to-[#8B5CF6]/20 border-b border-[#38BDF8]/30">
|
<tr class="bg-[var(--color-primary-light)] border-b border-[var(--color-border-subtle)]">
|
||||||
<th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Icon</th>
|
<th class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider">Icon</th>
|
||||||
<th @click="setSort('name')"
|
<th @click="setSort('name')"
|
||||||
:class="sortBy === 'name' ? 'bg-[#38BDF8]/20' : ''"
|
:class="sortBy === 'name' ? 'bg-[var(--color-primary-light)]' : ''"
|
||||||
class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors">
|
class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider cursor-pointer hover:bg-[var(--color-bg-primary)] transition-colors">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>Backend Name</span>
|
<span>Backend Name</span>
|
||||||
<i :class="sortBy === 'name' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
|
<i :class="sortBy === 'name' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
|
||||||
:class="sortBy === 'name' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
|
:class="sortBy === 'name' ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'"
|
||||||
class="text-xs"></i>
|
class="text-xs"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Description</th>
|
<th class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider">Description</th>
|
||||||
<th @click="setSort('repository')"
|
<th @click="setSort('repository')"
|
||||||
:class="sortBy === 'repository' ? 'bg-[#38BDF8]/20' : ''"
|
:class="sortBy === 'repository' ? 'bg-[var(--color-primary-light)]' : ''"
|
||||||
class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors">
|
class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider cursor-pointer hover:bg-[var(--color-bg-primary)] transition-colors">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>Repository</span>
|
<span>Repository</span>
|
||||||
<i :class="sortBy === 'repository' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
|
<i :class="sortBy === 'repository' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
|
||||||
:class="sortBy === 'repository' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
|
:class="sortBy === 'repository' ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'"
|
||||||
class="text-xs"></i>
|
class="text-xs"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th @click="setSort('license')"
|
<th @click="setSort('license')"
|
||||||
:class="sortBy === 'license' ? 'bg-[#38BDF8]/20' : ''"
|
:class="sortBy === 'license' ? 'bg-[var(--color-primary-light)]' : ''"
|
||||||
class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors">
|
class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider cursor-pointer hover:bg-[var(--color-bg-primary)] transition-colors">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>License</span>
|
<span>License</span>
|
||||||
<i :class="sortBy === 'license' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
|
<i :class="sortBy === 'license' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
|
||||||
:class="sortBy === 'license' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
|
:class="sortBy === 'license' ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'"
|
||||||
class="text-xs"></i>
|
class="text-xs"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th @click="setSort('status')"
|
<th @click="setSort('status')"
|
||||||
:class="sortBy === 'status' ? 'bg-[#38BDF8]/20' : ''"
|
:class="sortBy === 'status' ? 'bg-[var(--color-primary-light)]' : ''"
|
||||||
class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors">
|
class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider cursor-pointer hover:bg-[var(--color-bg-primary)] transition-colors">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>Status</span>
|
<span>Status</span>
|
||||||
<i :class="sortBy === 'status' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
|
<i :class="sortBy === 'status' ? (sortOrder === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') : 'fas fa-sort'"
|
||||||
:class="sortBy === 'status' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
|
:class="sortBy === 'status' ? 'text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'"
|
||||||
class="text-xs"></i>
|
class="text-xs"></i>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th class="px-6 py-4 text-right text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Actions</th>
|
<th class="px-6 py-4 text-right text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-[#38BDF8]/20">
|
<tbody class="divide-y divide-[var(--color-border-subtle)]">
|
||||||
<template x-for="backend in backends" :key="backend.id">
|
<template x-for="backend in backends" :key="backend.id">
|
||||||
<tr class="hover:bg-[#38BDF8]/10 transition-colors duration-200">
|
<tr class="hover:bg-[var(--color-bg-primary)] transition-colors duration-200">
|
||||||
<!-- Icon -->
|
<!-- Icon -->
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="w-12 h-12 rounded-lg border border-[#38BDF8]/30 flex items-center justify-center bg-[#101827]">
|
<div class="w-12 h-12 rounded-lg border border-[var(--color-border-subtle)] flex items-center justify-center bg-[var(--color-bg-primary)]">
|
||||||
<img x-show="backend.icon"
|
<img x-show="backend.icon"
|
||||||
:src="backend.icon"
|
:src="backend.icon"
|
||||||
class="w-full h-full object-cover rounded-lg"
|
class="w-full h-full object-cover rounded-lg"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
:alt="backend.name">
|
:alt="backend.name">
|
||||||
<i x-show="!backend.icon" class="fas fa-cog text-xl text-[#8B5CF6]"></i>
|
<i x-show="!backend.icon" class="fas fa-cog text-xl text-[var(--color-accent)]"></i>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Backend Name -->
|
<!-- Backend Name -->
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<span class="text-sm font-semibold text-[#E5E7EB]" x-text="backend.name"></span>
|
<span class="text-sm font-semibold text-[var(--color-text-primary)]" x-text="backend.name"></span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<div class="text-sm text-[#94A3B8] max-w-xs truncate" x-text="backend.description" :title="backend.description"></div>
|
<div class="text-sm text-[var(--color-text-secondary)] max-w-xs truncate" x-text="backend.description" :title="backend.description"></div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Repository -->
|
<!-- Repository -->
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#38BDF8]/10 text-[#E5E7EB] border border-[#38BDF8]/30">
|
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-primary-light)] text-[var(--color-text-primary)] border border-[var(--color-primary-border)]">
|
||||||
<i class="fa-brands fa-git-alt mr-1"></i>
|
<i class="fa-brands fa-git-alt mr-1"></i>
|
||||||
<span x-text="backend.gallery"></span>
|
<span x-text="backend.gallery"></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -297,21 +299,21 @@
|
|||||||
|
|
||||||
<!-- License -->
|
<!-- License -->
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<span x-show="backend.license" class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#8B5CF6]/10 text-[#E5E7EB] border border-[#8B5CF6]/30">
|
<span x-show="backend.license" class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-accent-light)] text-[var(--color-text-primary)] border border-[var(--color-accent)]/30">
|
||||||
<i class="fas fa-book mr-1"></i>
|
<i class="fas fa-book mr-1"></i>
|
||||||
<span x-text="backend.license"></span>
|
<span x-text="backend.license"></span>
|
||||||
</span>
|
</span>
|
||||||
<span x-show="!backend.license" class="text-xs text-[#94A3B8]">-</span>
|
<span x-show="!backend.license" class="text-xs text-[var(--color-text-secondary)]">-</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<!-- Processing State -->
|
<!-- Processing State -->
|
||||||
<div x-show="backend.processing" class="min-w-[200px]">
|
<div x-show="backend.processing" class="min-w-[200px]">
|
||||||
<div class="text-xs font-medium text-[#E5E7EB] mb-1">
|
<div class="text-xs font-medium text-[var(--color-text-primary)] mb-1">
|
||||||
<span x-text="backend.isDeletion ? 'Deleting...' : 'Installing...'"></span>
|
<span x-text="backend.isDeletion ? 'Deleting...' : 'Installing...'"></span>
|
||||||
</div>
|
</div>
|
||||||
<div x-show="(jobProgress[backend.jobID] || 0) === 0" class="text-xs text-[#38BDF8]">
|
<div x-show="(jobProgress[backend.jobID] || 0) === 0" class="text-xs text-[var(--color-primary)]">
|
||||||
<i class="fas fa-clock mr-1"></i>Queued
|
<i class="fas fa-clock mr-1"></i>Queued
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-table mt-1">
|
<div class="progress-table mt-1">
|
||||||
@@ -321,7 +323,7 @@
|
|||||||
|
|
||||||
<!-- Installed State -->
|
<!-- Installed State -->
|
||||||
<div x-show="!backend.processing && backend.installed">
|
<div x-show="!backend.processing && backend.installed">
|
||||||
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-green-500/20 text-green-300 border border-green-500/30">
|
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-success-light)] text-[var(--color-success)] border border-[var(--color-success)]/30">
|
||||||
<i class="fas fa-check-circle mr-1"></i>
|
<i class="fas fa-check-circle mr-1"></i>
|
||||||
Installed
|
Installed
|
||||||
</span>
|
</span>
|
||||||
@@ -329,7 +331,7 @@
|
|||||||
|
|
||||||
<!-- Not Installed State -->
|
<!-- Not Installed State -->
|
||||||
<div x-show="!backend.processing && !backend.installed">
|
<div x-show="!backend.processing && !backend.installed">
|
||||||
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#1E293B] text-[#94A3B8] border border-[#38BDF8]/30">
|
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] border border-[var(--color-border-subtle)]">
|
||||||
<i class="fas fa-circle mr-1"></i>
|
<i class="fas fa-circle mr-1"></i>
|
||||||
Not Installed
|
Not Installed
|
||||||
</span>
|
</span>
|
||||||
@@ -341,7 +343,7 @@
|
|||||||
<div class="flex items-center justify-end gap-2">
|
<div class="flex items-center justify-end gap-2">
|
||||||
<!-- Info Button -->
|
<!-- Info Button -->
|
||||||
<button @click="openModal(backend)"
|
<button @click="openModal(backend)"
|
||||||
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[#1E293B] hover:bg-[#38BDF8]/20 text-xs font-medium text-[#E5E7EB] transition duration-200 border border-[#38BDF8]/30"
|
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-bg-primary)] hover:bg-[var(--color-primary-light)] text-xs font-medium text-[var(--color-text-primary)] transition duration-200 border border-[var(--color-border-subtle)]"
|
||||||
title="View details">
|
title="View details">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -350,12 +352,12 @@
|
|||||||
<template x-if="!backend.processing && backend.installed">
|
<template x-if="!backend.processing && backend.installed">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button @click="reinstallBackend(backend.id)"
|
<button @click="reinstallBackend(backend.id)"
|
||||||
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[#38BDF8] hover:bg-[#38BDF8]/80 text-xs font-medium text-white transition duration-200"
|
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-xs font-medium text-white transition duration-200"
|
||||||
title="Reinstall">
|
title="Reinstall">
|
||||||
<i class="fa-solid fa-arrow-rotate-right"></i>
|
<i class="fa-solid fa-arrow-rotate-right"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="deleteBackend(backend.id)"
|
<button @click="deleteBackend(backend.id)"
|
||||||
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-red-600 hover:bg-red-700 text-xs font-medium text-white transition duration-200"
|
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-error)] hover:bg-[var(--color-error)]/80 text-xs font-medium text-white transition duration-200"
|
||||||
title="Delete">
|
title="Delete">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -365,7 +367,7 @@
|
|||||||
<!-- Not Installed State Actions -->
|
<!-- Not Installed State Actions -->
|
||||||
<template x-if="!backend.processing && !backend.installed">
|
<template x-if="!backend.processing && !backend.installed">
|
||||||
<button @click="installBackend(backend.id)"
|
<button @click="installBackend(backend.id)"
|
||||||
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[#38BDF8] hover:bg-[#38BDF8]/80 text-xs font-medium text-white transition duration-200"
|
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-xs font-medium text-white transition duration-200"
|
||||||
title="Install">
|
title="Install">
|
||||||
<i class="fa-solid fa-download"></i>
|
<i class="fa-solid fa-download"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -383,15 +385,15 @@
|
|||||||
<div x-show="selectedBackend"
|
<div x-show="selectedBackend"
|
||||||
x-transition
|
x-transition
|
||||||
@click.away="closeModal()"
|
@click.away="closeModal()"
|
||||||
class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full md:inset-0 h-full max-h-full bg-gray-900/50"
|
class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full md:inset-0 h-full max-h-full bg-black/50"
|
||||||
style="display: none;">
|
style="display: none;">
|
||||||
<div class="relative p-4 w-full max-w-2xl h-[90vh] mx-auto mt-[5vh]">
|
<div class="relative p-4 w-full max-w-2xl h-[90vh] mx-auto mt-[5vh]">
|
||||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 h-full flex flex-col">
|
<div class="relative bg-[var(--color-bg-secondary)] rounded-lg shadow h-full flex flex-col border border-[var(--color-border-subtle)]">
|
||||||
<!-- Modal Header -->
|
<!-- Modal Header -->
|
||||||
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
<div class="flex items-center justify-between p-4 md:p-5 border-b border-[var(--color-border-subtle)] rounded-t">
|
||||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white" x-text="selectedBackend?.name"></h3>
|
<h3 class="text-xl font-semibold text-[var(--color-text-primary)]" x-text="selectedBackend?.name"></h3>
|
||||||
<button @click="closeModal()"
|
<button @click="closeModal()"
|
||||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
class="text-[var(--color-text-secondary)] bg-transparent hover:bg-[var(--color-bg-primary)] hover:text-[var(--color-text-primary)] rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center transition-colors">
|
||||||
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -401,21 +403,21 @@
|
|||||||
<!-- Modal Body -->
|
<!-- Modal Body -->
|
||||||
<div class="p-4 md:p-5 space-y-4 overflow-y-auto flex-1 min-h-0">
|
<div class="p-4 md:p-5 space-y-4 overflow-y-auto flex-1 min-h-0">
|
||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
<div class="w-48 h-48 rounded-lg border border-gray-300 dark:border-gray-600 flex items-center justify-center bg-gray-100 dark:bg-gray-800 mt-3">
|
<div class="w-48 h-48 rounded-lg border border-[var(--color-border-subtle)] flex items-center justify-center bg-[var(--color-bg-primary)] mt-3">
|
||||||
<img x-show="selectedBackend?.icon"
|
<img x-show="selectedBackend?.icon"
|
||||||
:src="selectedBackend?.icon"
|
:src="selectedBackend?.icon"
|
||||||
class="rounded-lg max-h-48 max-w-96 object-cover"
|
class="rounded-lg max-h-48 max-w-96 object-cover"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
<i x-show="!selectedBackend?.icon" class="fas fa-cog text-6xl text-gray-400 dark:text-gray-500"></i>
|
<i x-show="!selectedBackend?.icon" class="fas fa-cog text-6xl text-[var(--color-text-muted)]"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-base leading-relaxed text-gray-500 dark:text-gray-400 break-words max-w-full markdown-content" x-html="renderMarkdown(selectedBackend?.description)"></div>
|
<div class="text-base leading-relaxed text-[var(--color-text-secondary)] break-words max-w-full markdown-content" x-html="renderMarkdown(selectedBackend?.description)"></div>
|
||||||
<template x-if="selectedBackend?.tags && selectedBackend.tags.length > 0">
|
<template x-if="selectedBackend?.tags && selectedBackend.tags.length > 0">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm mb-3 font-semibold text-gray-900 dark:text-white">Tags</p>
|
<p class="text-sm mb-3 font-semibold text-[var(--color-text-primary)]">Tags</p>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<template x-for="tag in selectedBackend.tags" :key="tag">
|
<template x-for="tag in selectedBackend.tags" :key="tag">
|
||||||
<span class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-gray-700/60 text-gray-300 border border-gray-600/50">
|
<span class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] border border-[var(--color-border-subtle)]">
|
||||||
<i class="fas fa-tag pr-2"></i>
|
<i class="fas fa-tag pr-2"></i>
|
||||||
<span x-text="tag"></span>
|
<span x-text="tag"></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -425,11 +427,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<template x-if="selectedBackend?.urls && selectedBackend.urls.length > 0">
|
<template x-if="selectedBackend?.urls && selectedBackend.urls.length > 0">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Links</p>
|
<p class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Links</p>
|
||||||
<ul>
|
<ul>
|
||||||
<template x-for="url in selectedBackend.urls" :key="url">
|
<template x-for="url in selectedBackend.urls" :key="url">
|
||||||
<li>
|
<li>
|
||||||
<a :href="url" target="_blank" class="text-blue-500 hover:underline">
|
<a :href="url" target="_blank" class="text-[var(--color-primary)] hover:underline">
|
||||||
<i class="fas fa-link pr-2"></i>
|
<i class="fas fa-link pr-2"></i>
|
||||||
<span x-text="url"></span>
|
<span x-text="url"></span>
|
||||||
</a>
|
</a>
|
||||||
@@ -440,9 +442,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<!-- Modal Footer -->
|
<!-- Modal Footer -->
|
||||||
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div class="flex items-center p-4 md:p-5 border-t border-[var(--color-border-subtle)] rounded-b">
|
||||||
<button @click="closeModal()"
|
<button @click="closeModal()"
|
||||||
class="text-white bg-emerald-700 hover:bg-emerald-800 focus:ring-4 focus:outline-none focus:ring-emerald-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-emerald-600 dark:hover:bg-emerald-700 dark:focus:ring-emerald-800">
|
class="text-white bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] focus:ring-2 focus:outline-none focus:ring-[var(--color-primary)]/50 font-medium rounded-lg text-sm px-5 py-2.5 text-center transition-colors">
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -453,30 +455,29 @@
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div x-show="totalPages > 1" class="flex justify-center mt-12">
|
<div x-show="totalPages > 1" class="flex justify-center mt-12">
|
||||||
<div class="flex items-center gap-4 bg-gray-800/60 rounded-2xl p-4 backdrop-blur-sm border border-gray-700/50">
|
<div class="flex items-center gap-4 bg-[var(--color-bg-secondary)] rounded-2xl p-4 backdrop-blur-sm border border-[var(--color-border-subtle)]">
|
||||||
<button @click="goToPage(currentPage - 1)"
|
<button @click="goToPage(currentPage - 1)"
|
||||||
:disabled="currentPage <= 1"
|
:disabled="currentPage <= 1"
|
||||||
:class="currentPage <= 1 ? 'opacity-50 cursor-not-allowed' : ''"
|
:class="currentPage <= 1 ? 'opacity-50 cursor-not-allowed' : ''"
|
||||||
class="flex items-center justify-center h-12 w-12 bg-[#1E293B] hover:bg-emerald-600 text-[#94A3B8] hover:text-white rounded-lg transition-colors">
|
class="flex items-center justify-center h-12 w-12 bg-[var(--color-bg-primary)] hover:bg-[var(--color-success)] text-[var(--color-text-secondary)] hover:text-white rounded-lg transition-colors">
|
||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-gray-300 text-sm font-medium px-4">
|
<div class="text-[var(--color-text-primary)] text-sm font-medium px-4">
|
||||||
<span class="text-gray-400">Page</span>
|
<span class="text-[var(--color-text-secondary)]">Page</span>
|
||||||
<span class="text-white font-bold text-lg mx-2" x-text="currentPage"></span>
|
<span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="currentPage"></span>
|
||||||
<span class="text-gray-400">of</span>
|
<span class="text-[var(--color-text-secondary)]">of</span>
|
||||||
<span class="text-white font-bold text-lg mx-2" x-text="totalPages"></span>
|
<span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="totalPages"></span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="goToPage(currentPage + 1)"
|
<button @click="goToPage(currentPage + 1)"
|
||||||
:disabled="currentPage >= totalPages"
|
:disabled="currentPage >= totalPages"
|
||||||
:class="currentPage >= totalPages ? 'opacity-50 cursor-not-allowed' : ''"
|
:class="currentPage >= totalPages ? 'opacity-50 cursor-not-allowed' : ''"
|
||||||
class="group flex items-center justify-center h-12 w-12 bg-gray-700/80 hover:bg-emerald-600 text-gray-300 hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110">
|
class="group flex items-center justify-center h-12 w-12 bg-[var(--color-bg-primary)] hover:bg-[var(--color-success)] text-[var(--color-text-secondary)] hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110">
|
||||||
<i class="fas fa-chevron-right group-hover:animate-pulse"></i>
|
<i class="fas fa-chevron-right group-hover:animate-pulse"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{template "views/partials/footer" .}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -516,16 +517,16 @@
|
|||||||
|
|
||||||
/* Table progress bar styling */
|
/* Table progress bar styling */
|
||||||
.progress-table {
|
.progress-table {
|
||||||
background: linear-gradient(135deg, rgba(56, 189, 248, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
|
background: var(--color-primary-light);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
border: 1px solid rgba(56, 189, 248, 0.3);
|
border: 1px solid var(--color-primary-border);
|
||||||
height: 6px;
|
height: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-table-backend {
|
.progress-bar-table-backend {
|
||||||
background: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 100%);
|
background: var(--gradient-primary);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
@@ -534,6 +535,7 @@
|
|||||||
table {
|
table {
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:last-child td:first-child {
|
tbody tr:last-child td:first-child {
|
||||||
@@ -905,5 +907,10 @@ function backendsGallery() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -587,19 +587,25 @@ SOFTWARE.
|
|||||||
<script defer src="static/chat.js"></script>
|
<script defer src="static/chat.js"></script>
|
||||||
{{ $allGalleryConfigs:=.GalleryConfig }}
|
{{ $allGalleryConfigs:=.GalleryConfig }}
|
||||||
{{ $model:=.Model}}
|
{{ $model:=.Model}}
|
||||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen" x-data="{ sidebarOpen: true, showClearAlert: false }">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]" x-data="{ settingsPanelOpen: true, showClearAlert: false, isMobile: false }" x-init="isMobile = window.innerWidth < 1024; if (isMobile) settingsPanelOpen = false; window.addEventListener('resize', () => { isMobile = window.innerWidth < 1024 })">
|
||||||
{{template "views/partials/navbar" .}}
|
<div class="app-layout chat-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
|
|
||||||
|
<main class="main-content chat-layout">
|
||||||
|
<div class="main-content-inner chat-layout h-full flex flex-col">
|
||||||
|
|
||||||
<!-- Main container with sidebar toggle -->
|
<!-- Main container with settings panel -->
|
||||||
<div class="flex flex-1 overflow-hidden relative">
|
<div class="flex flex-1 min-h-0 relative">
|
||||||
<!-- Sidebar -->
|
<!-- Backdrop for mobile when settings panel is open (click to close) -->
|
||||||
|
<div x-show="settingsPanelOpen && isMobile" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-150" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="settingsPanelOpen = false" class="fixed inset-0 bg-black/50 z-20" aria-hidden="true"></div>
|
||||||
|
<!-- Chat Settings Panel (right side): overlay on mobile (w-full), sidebar on desktop (md:w-56) -->
|
||||||
<div
|
<div
|
||||||
class="sidebar bg-[var(--color-bg-secondary)] fixed top-14 bottom-0 left-0 w-56 transform transition-transform duration-300 ease-in-out z-30 border-r border-[var(--color-bg-primary)] overflow-y-auto"
|
class="chat-settings-panel bg-[var(--color-bg-secondary)] fixed top-0 right-0 bottom-0 w-full md:w-56 transform transition-transform duration-300 ease-in-out z-30 border-l border-[var(--color-border-subtle)] overflow-y-auto"
|
||||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
|
:class="settingsPanelOpen ? 'translate-x-0' : 'translate-x-full'">
|
||||||
|
|
||||||
<div class="p-3 flex justify-between items-center border-b border-[var(--color-bg-primary)]">
|
<div class="p-3 flex justify-between items-center border-b border-[var(--color-border-subtle)]">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Settings</h2>
|
<h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Chat Settings</h2>
|
||||||
<a
|
<a
|
||||||
href="https://localai.io/features/text-generation/"
|
href="https://localai.io/features/text-generation/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -609,10 +615,10 @@ SOFTWARE.
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="sidebarOpen = false"
|
@click="settingsPanelOpen = false"
|
||||||
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none text-xs"
|
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none text-xs"
|
||||||
title="Hide sidebar">
|
title="Hide settings">
|
||||||
<i class="fa-solid fa-chevron-left"></i>
|
<i class="fa-solid fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1099,61 +1105,57 @@ SOFTWARE.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main chat container (shifts with sidebar) -->
|
<!-- Main chat container (shifts with settings panel on desktop only; on mobile panel overlays) -->
|
||||||
<div
|
<div
|
||||||
class="flex-1 flex flex-col transition-all duration-300 ease-in-out"
|
class="flex-1 flex flex-col min-h-0 transition-all duration-300 ease-in-out"
|
||||||
:class="sidebarOpen ? 'ml-56' : 'ml-0'">
|
:class="settingsPanelOpen ? 'md:mr-56' : 'mr-0'">
|
||||||
|
|
||||||
<!-- Chat header with toggle button -->
|
<!-- Chat header with toggle button -->
|
||||||
<div class="border-b border-[var(--color-bg-secondary)] p-4 flex items-center justify-between">
|
<div class="flex-shrink-0 border-b border-[var(--color-bg-secondary)] p-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center min-w-0 flex-1">
|
||||||
<!-- Sidebar toggle button moved to be the first element in the header and with clear styling -->
|
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)] flex-shrink-0"></i>
|
||||||
<button
|
<!-- Model icon - reactive to active chat -->
|
||||||
@click="sidebarOpen = !sidebarOpen"
|
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model && window.__galleryConfigs && window.__galleryConfigs[$store.chat.activeChat().model] && window.__galleryConfigs[$store.chat.activeChat().model].Icon">
|
||||||
class="mr-4 text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-secondary)]/80 p-2 rounded transition-colors"
|
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2 flex-shrink-0">
|
||||||
style="min-width: 36px;"
|
</template>
|
||||||
title="Toggle settings">
|
<!-- Fallback icon for initial model from server (when no active chat yet) -->
|
||||||
<i class="fa-solid" :class="sidebarOpen ? 'fa-chevron-left' : 'fa-bars'"></i>
|
<template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon">
|
||||||
</button>
|
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2 flex-shrink-0">
|
||||||
|
</template>
|
||||||
<div class="flex items-center">
|
<h1 class="text-lg font-semibold text-[var(--color-text-primary)] truncate min-w-0">
|
||||||
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)]"></i>
|
Chat
|
||||||
<!-- Model icon - reactive to active chat -->
|
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model">
|
||||||
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model && window.__galleryConfigs && window.__galleryConfigs[$store.chat.activeChat().model] && window.__galleryConfigs[$store.chat.activeChat().model].Icon">
|
<span x-text="' with ' + $store.chat.activeChat().model"></span>
|
||||||
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2">
|
|
||||||
</template>
|
</template>
|
||||||
<!-- Fallback icon for initial model from server (when no active chat yet) -->
|
<template x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model">
|
||||||
<template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon">
|
{{ if .Model }}<span> with {{.Model}}</span>{{ end }}
|
||||||
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2">
|
|
||||||
</template>
|
</template>
|
||||||
<h1 class="text-lg font-semibold text-[var(--color-text-primary)]">
|
</h1>
|
||||||
Chat
|
<!-- Loading indicator next to model name -->
|
||||||
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model">
|
<div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)] flex-shrink-0" style="display: none;">
|
||||||
<span x-text="' with ' + $store.chat.activeChat().model"></span>
|
<i class="fas fa-spinner fa-spin text-sm"></i>
|
||||||
</template>
|
|
||||||
<template x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model">
|
|
||||||
{{ if .Model }}<span> with {{.Model}}</span>{{ end }}
|
|
||||||
</template>
|
|
||||||
</h1>
|
|
||||||
<!-- Loading indicator next to model name -->
|
|
||||||
<div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)]" style="display: none;">
|
|
||||||
<i class="fas fa-spinner fa-spin text-sm"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
@click="if (confirm('Clear all messages from this conversation? This action cannot be undone.')) { $store.chat.clear(); showClearAlert = true; setTimeout(() => showClearAlert = false, 3000); }"
|
@click="if (confirm('Clear all messages from this conversation? This action cannot be undone.')) { $store.chat.clear(); showClearAlert = true; setTimeout(() => showClearAlert = false, 3000); }"
|
||||||
id="clear"
|
id="clear"
|
||||||
title="Clear current chat history"
|
title="Clear current chat history"
|
||||||
class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors p-2 rounded hover:bg-[var(--color-bg-secondary)]"
|
class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center rounded hover:bg-[var(--color-bg-secondary)]"
|
||||||
x-show="$store.chat.activeChat() && ($store.chat.activeChat()?.history?.length || 0) > 0">
|
x-show="$store.chat.activeChat() && ($store.chat.activeChat()?.history?.length || 0) > 0">
|
||||||
<i class="fa-solid fa-broom"></i>
|
<i class="fa-solid fa-broom"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Settings panel toggle button -->
|
||||||
|
<button
|
||||||
|
@click="settingsPanelOpen = !settingsPanelOpen"
|
||||||
|
class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] focus:outline-none bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-secondary)]/80 p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center rounded transition-colors"
|
||||||
|
title="Toggle chat settings">
|
||||||
|
<i class="fa-solid" :class="settingsPanelOpen ? 'fa-chevron-right' : 'fa-cog'"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Clear Chat Alert -->
|
<!-- Clear Chat Alert (bottom on mobile to avoid covering header) -->
|
||||||
<div x-show="showClearAlert"
|
<div x-show="showClearAlert"
|
||||||
x-transition:enter="transition ease-out duration-300"
|
x-transition:enter="transition ease-out duration-300"
|
||||||
x-transition:enter-start="opacity-0 translate-y-2"
|
x-transition:enter-start="opacity-0 translate-y-2"
|
||||||
@@ -1161,7 +1163,7 @@ SOFTWARE.
|
|||||||
x-transition:leave="transition ease-in duration-200"
|
x-transition:leave="transition ease-in duration-200"
|
||||||
x-transition:leave-start="opacity-100"
|
x-transition:leave-start="opacity-100"
|
||||||
x-transition:leave-end="opacity-0"
|
x-transition:leave-end="opacity-0"
|
||||||
class="fixed top-20 right-4 z-50 max-w-sm pointer-events-none">
|
class="fixed top-20 right-4 max-md:top-auto max-md:bottom-4 max-md:left-4 max-md:right-4 z-50 max-w-sm pointer-events-none">
|
||||||
<div class="bg-[var(--color-primary)]/20 border border-[var(--color-primary-border)]/40 rounded-lg p-3 shadow-lg backdrop-blur-sm">
|
<div class="bg-[var(--color-primary)]/20 border border-[var(--color-primary-border)]/40 rounded-lg p-3 shadow-lg backdrop-blur-sm">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<i class="fa-solid fa-check-circle text-[var(--color-primary)]"></i>
|
<i class="fa-solid fa-check-circle text-[var(--color-primary)]"></i>
|
||||||
@@ -1172,7 +1174,8 @@ SOFTWARE.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chat messages area -->
|
<!-- Chat messages area -->
|
||||||
<div class="flex-1 p-4 overflow-auto" id="chat">
|
<div class="flex-1 min-h-0 overflow-y-auto" id="chat">
|
||||||
|
<div class="p-4">
|
||||||
<p id="usage" x-show="!$store.chat.activeChat() || ($store.chat.activeChat()?.history?.length || 0) === 0" class="text-[var(--color-text-secondary)]">
|
<p id="usage" x-show="!$store.chat.activeChat() || ($store.chat.activeChat()?.history?.length || 0) === 0" class="text-[var(--color-text-secondary)]">
|
||||||
Start chatting with the AI by typing a prompt in the input field below and pressing Enter.<br>
|
Start chatting with the AI by typing a prompt in the input field below and pressing Enter.<br>
|
||||||
<ul class="list-disc list-inside mt-2 space-y-1">
|
<ul class="list-disc list-inside mt-2 space-y-1">
|
||||||
@@ -1367,11 +1370,11 @@ SOFTWARE.
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Chat Input -->
|
<!-- Chat Input -->
|
||||||
<div class="p-4 border-t border-[var(--color-bg-secondary)]" x-data="{ inputValue: '', shiftPressed: false, attachedFiles: [] }">
|
<div class="flex-shrink-0 p-4 pb-safe border-t border-[var(--color-bg-secondary)] bg-[var(--color-bg-primary)]" x-data="{ inputValue: '', shiftPressed: false, attachedFiles: [] }">
|
||||||
<form id="prompt" action="chat/{{.Model}}" method="get" @submit.prevent="submitPrompt" class="max-w-3xl mx-auto">
|
<form id="prompt" action="chat/{{.Model}}" method="get" @submit.prevent="submitPrompt" class="max-w-3xl mx-auto">
|
||||||
<!-- Attachment Tags - Show above input when files are attached -->
|
<!-- Attachment Tags - Show above input when files are attached -->
|
||||||
<div x-show="attachedFiles.length > 0" class="mb-3 flex flex-wrap gap-2 items-center">
|
<div x-show="attachedFiles.length > 0" class="mb-3 flex flex-wrap gap-2 items-center">
|
||||||
@@ -1391,38 +1394,38 @@ SOFTWARE.
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Token Usage and Context Window - Compact above input -->
|
<!-- Token Usage and Context Window - responsive: two rows on mobile -->
|
||||||
<div class="mb-3 flex items-center justify-between gap-4 text-xs">
|
<div class="mb-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3 text-xs">
|
||||||
<!-- Token Usage -->
|
<!-- Token Usage (wraps on mobile) -->
|
||||||
<div class="flex items-center gap-3 text-[var(--color-text-secondary)]">
|
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-[var(--color-text-secondary)]">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1 max-md:hidden">
|
||||||
<i class="fas fa-chart-line text-[var(--color-primary)]"></i>
|
<i class="fas fa-chart-line text-[var(--color-primary)]"></i>
|
||||||
<span>Prompt:</span>
|
<span>Prompt:</span>
|
||||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.promptTokens || 0)"></span>
|
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.promptTokens || 0)"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1 max-md:hidden">
|
||||||
<span>Completion:</span>
|
<span>Completion:</span>
|
||||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.completionTokens || 0)"></span>
|
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.completionTokens || 0)"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-3">
|
<div class="flex items-center gap-1 md:border-l border-[var(--color-bg-secondary)] pl-0 md:pl-3">
|
||||||
<span class="text-[var(--color-primary)] font-semibold">Total:</span>
|
<span class="text-[var(--color-primary)] font-semibold">Total:</span>
|
||||||
<span class="text-[var(--color-text-primary)] font-bold" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
|
<span class="text-[var(--color-text-primary)] font-bold" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Tokens per second display -->
|
<!-- Tokens per second display -->
|
||||||
<div id="tokens-per-second-container" class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-3">
|
<div id="tokens-per-second-container" class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] pl-2 md:pl-3">
|
||||||
<i class="fas fa-tachometer-alt text-[var(--color-primary)]"></i>
|
<i class="fas fa-tachometer-alt text-[var(--color-primary)]"></i>
|
||||||
<span id="tokens-per-second" class="text-[var(--color-text-primary)] font-medium">-</span>
|
<span id="tokens-per-second" class="text-[var(--color-text-primary)] font-medium">-</span>
|
||||||
<span id="max-tokens-per-second-badge" class="ml-2 px-1.5 py-0.5 text-[10px] bg-[var(--color-primary)]/20 text-[var(--color-primary)] rounded border border-[var(--color-primary-border)]/30 hidden"></span>
|
<span id="max-tokens-per-second-badge" class="ml-2 px-1.5 py-0.5 text-[10px] bg-[var(--color-primary)]/20 text-[var(--color-primary)] rounded border border-[var(--color-primary-border)]/30 hidden"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Context Window -->
|
<!-- Context Window (second row on mobile) -->
|
||||||
<template x-if="$store.chat.activeChat()?.contextSize && $store.chat.activeChat().contextSize > 0">
|
<template x-if="$store.chat.activeChat()?.contextSize && $store.chat.activeChat().contextSize > 0">
|
||||||
<div class="flex items-center gap-2 text-[var(--color-text-secondary)]">
|
<div class="flex items-center gap-2 text-[var(--color-text-secondary)] flex-shrink-0">
|
||||||
<i class="fas fa-database text-[var(--color-primary)]"></i>
|
<i class="fas fa-database text-[var(--color-primary)]"></i>
|
||||||
<span>
|
<span>
|
||||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
|
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.tokenUsage?.totalTokens || 0)"></span>
|
||||||
/
|
/
|
||||||
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.contextSize || 0)"></span>
|
<span class="text-[var(--color-text-primary)] font-medium" x-text="new Intl.NumberFormat().format($store.chat.activeChat()?.contextSize || 0)"></span>
|
||||||
</span>
|
</span>
|
||||||
<div class="w-16 bg-[var(--color-bg-primary)] rounded-full h-1.5 overflow-hidden border border-[var(--color-bg-secondary)]">
|
<div class="w-16 bg-[var(--color-bg-primary)] rounded-full h-1.5 overflow-hidden border border-[var(--color-bg-secondary)]">
|
||||||
@@ -1443,38 +1446,36 @@ SOFTWARE.
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Attachment buttons row (mobile only) - avoids overlap with input on narrow screens -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-2 md:hidden">
|
||||||
|
<button type="button" onclick="document.getElementById('input_image').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Attach images" aria-label="Attach images">
|
||||||
|
<i class="fa-solid fa-image text-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="document.getElementById('input_audio').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Attach an audio file" aria-label="Attach audio">
|
||||||
|
<i class="fa-solid fa-microphone text-lg"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" onclick="document.getElementById('input_file').click()" class="min-w-[44px] min-h-[44px] flex items-center justify-center rounded-lg bg-[var(--color-bg-secondary)] text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors border border-[var(--color-border-subtle)]" title="Upload text, markdown or PDF file" aria-label="Attach file">
|
||||||
|
<i class="fa-solid fa-file text-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<textarea
|
<textarea
|
||||||
id="input"
|
id="input"
|
||||||
name="input"
|
name="input"
|
||||||
x-model="inputValue"
|
x-model="inputValue"
|
||||||
class="input w-full p-3 pr-16 resize-none border-0"
|
class="input w-full p-3 pr-12 md:pr-28 resize-none border-0 bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)] placeholder-[var(--color-text-secondary)] focus:outline-none rounded-xl transition-colors duration-200"
|
||||||
placeholder="Send a message..."
|
placeholder="Send a message..."
|
||||||
class="p-3 pr-16 w-full bg-[var(--color-bg-secondary)] text-[var(--color-text-primary)] placeholder-[var(--color-text-secondary)] focus:outline-none resize-none border-0 rounded-xl transition-colors duration-200"
|
|
||||||
required
|
required
|
||||||
@keydown.shift="shiftPressed = true"
|
@keydown.shift="shiftPressed = true"
|
||||||
@keyup.shift="shiftPressed = false"
|
@keyup.shift="shiftPressed = false"
|
||||||
@keydown.enter.prevent="if (!shiftPressed) { submitPrompt($event); }"
|
@keydown.enter.prevent="if (!shiftPressed) { submitPrompt($event); }"
|
||||||
rows="2"
|
rows="2"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button
|
<!-- Attachment buttons (desktop only - inside input) -->
|
||||||
type="button"
|
<button type="button" onclick="document.getElementById('input_image').click()" class="hidden md:flex fa-solid fa-image text-[var(--color-text-secondary)] absolute right-12 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Attach images" aria-label="Attach images"></button>
|
||||||
onclick="document.getElementById('input_image').click()"
|
<button type="button" onclick="document.getElementById('input_audio').click()" class="hidden md:flex fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Attach an audio file" aria-label="Attach audio"></button>
|
||||||
class="fa-solid fa-image text-[var(--color-text-secondary)] absolute right-12 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
|
<button type="button" onclick="document.getElementById('input_file').click()" class="hidden md:flex fa-solid fa-file text-[var(--color-text-secondary)] absolute right-28 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Upload text, markdown or PDF file" aria-label="Attach file"></button>
|
||||||
title="Attach images"
|
|
||||||
></button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick="document.getElementById('input_audio').click()"
|
|
||||||
class="fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
|
|
||||||
title="Attach an audio file"
|
|
||||||
></button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick="document.getElementById('input_file').click()"
|
|
||||||
class="fa-solid fa-file text-[var(--color-text-secondary)] absolute right-28 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
|
|
||||||
title="Upload text, markdown or PDF file"
|
|
||||||
></button>
|
|
||||||
|
|
||||||
<!-- Send button and stop button in the same position -->
|
<!-- Send button and stop button in the same position -->
|
||||||
<div class="absolute right-3 top-3 flex items-center">
|
<div class="absolute right-3 top-3 flex items-center">
|
||||||
@@ -1483,7 +1484,7 @@ SOFTWARE.
|
|||||||
id="stop-button"
|
id="stop-button"
|
||||||
type="button"
|
type="button"
|
||||||
onclick="stopRequest()"
|
onclick="stopRequest()"
|
||||||
class="text-lg p-2 text-[var(--color-error)] hover:text-[var(--color-error)] transition-colors duration-200"
|
class="text-lg p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center text-[var(--color-error)] hover:text-[var(--color-error)] transition-colors duration-200"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
title="Stop request"
|
title="Stop request"
|
||||||
>
|
>
|
||||||
@@ -1494,7 +1495,7 @@ SOFTWARE.
|
|||||||
<button
|
<button
|
||||||
id="send-button"
|
id="send-button"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="text-lg p-2 text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors duration-200"
|
class="text-lg p-2 min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-center text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors duration-200"
|
||||||
title="Send message (Enter)"
|
title="Send message (Enter)"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-paper-plane"></i>
|
<i class="fa-solid fa-paper-plane"></i>
|
||||||
@@ -1536,11 +1537,11 @@ SOFTWARE.
|
|||||||
<!-- Modal moved outside of sidebar to appear in center of page - Always available, content updated dynamically -->
|
<!-- Modal moved outside of sidebar to appear in center of page - Always available, content updated dynamically -->
|
||||||
<div id="model-info-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full md:inset-0 max-h-full" style="padding: 1rem;">
|
<div id="model-info-modal" tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full md:inset-0 max-h-full" style="padding: 1rem;">
|
||||||
<div class="relative p-4 w-full max-w-2xl max-h-full">
|
<div class="relative p-4 w-full max-w-2xl max-h-full">
|
||||||
<div class="relative p-4 w-full max-w-2xl max-h-full bg-white rounded-lg shadow dark:bg-gray-700">
|
<div class="relative p-4 w-full max-w-2xl max-h-full bg-[var(--color-bg-secondary)] rounded-lg shadow border border-[var(--color-border-subtle)]">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
<div class="flex items-center justify-between p-4 md:p-5 border-b border-[var(--color-border-subtle)] rounded-t">
|
||||||
<h3 id="model-info-modal-title" class="text-xl font-semibold text-gray-900 dark:text-white">{{ if $model }}{{ $model }}{{ end }}</h3>
|
<h3 id="model-info-modal-title" class="text-xl font-semibold text-[var(--color-text-primary)]">{{ if $model }}{{ $model }}{{ end }}</h3>
|
||||||
<button class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" data-modal-hide="model-info-modal" @click="if (window.closeModelInfoModal) { window.closeModelInfoModal(); }">
|
<button class="text-[var(--color-text-secondary)] bg-transparent hover:bg-[var(--color-bg-primary)] hover:text-[var(--color-text-primary)] rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center transition-colors" data-modal-hide="model-info-modal" @click="if (window.closeModelInfoModal) { window.closeModelInfoModal(); }">
|
||||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -1553,16 +1554,16 @@ SOFTWARE.
|
|||||||
<div class="flex justify-center items-center">
|
<div class="flex justify-center items-center">
|
||||||
<img id="model-info-modal-icon" class="lazy rounded-t-lg max-h-48 max-w-96 object-cover mt-3 entered loaded" style="display: none;" loading="lazy"/>
|
<img id="model-info-modal-icon" class="lazy rounded-t-lg max-h-48 max-w-96 object-cover mt-3 entered loaded" style="display: none;" loading="lazy"/>
|
||||||
</div>
|
</div>
|
||||||
<div id="model-info-description" class="text-base leading-relaxed text-gray-500 dark:text-gray-400 break-words max-w-full"></div>
|
<div id="model-info-description" class="text-base leading-relaxed text-[var(--color-text-secondary)] break-words max-w-full"></div>
|
||||||
<hr>
|
<hr class="border-[var(--color-border-subtle)]">
|
||||||
<p class="text-sm font-semibold text-gray-900 dark:text-white">Links</p>
|
<p class="text-sm font-semibold text-[var(--color-text-primary)]">Links</p>
|
||||||
<ul id="model-info-links">
|
<ul id="model-info-links">
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
|
<div class="flex items-center p-4 md:p-5 border-t border-[var(--color-border-subtle)] rounded-b">
|
||||||
<button data-modal-hide="model-info-modal" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700" @click="if (window.closeModelInfoModal) { window.closeModelInfoModal(); }">
|
<button data-modal-hide="model-info-modal" class="py-2.5 px-5 ms-3 text-sm font-medium text-white focus:outline-none bg-[var(--color-primary)] rounded-lg border-none hover:bg-[var(--color-primary-hover)] focus:z-10 focus:ring-2 focus:ring-[var(--color-primary)]/50 transition-colors" @click="if (window.closeModelInfoModal) { window.closeModelInfoModal(); }">
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1874,7 +1875,7 @@ SOFTWARE.
|
|||||||
let backdrop = document.querySelector('.modal-backdrop');
|
let backdrop = document.querySelector('.modal-backdrop');
|
||||||
if (!backdrop) {
|
if (!backdrop) {
|
||||||
backdrop = document.createElement('div');
|
backdrop = document.createElement('div');
|
||||||
backdrop.className = 'modal-backdrop fixed inset-0 bg-gray-900 bg-opacity-50 dark:bg-opacity-80 z-40';
|
backdrop.className = 'modal-backdrop fixed inset-0 bg-black/50 z-40';
|
||||||
document.body.appendChild(backdrop);
|
document.body.appendChild(backdrop);
|
||||||
backdrop.addEventListener('click', () => {
|
backdrop.addEventListener('click', () => {
|
||||||
closeModelInfoModal();
|
closeModelInfoModal();
|
||||||
@@ -1962,7 +1963,7 @@ SOFTWARE.
|
|||||||
let backdrop = document.querySelector('.modal-backdrop');
|
let backdrop = document.querySelector('.modal-backdrop');
|
||||||
if (!backdrop) {
|
if (!backdrop) {
|
||||||
backdrop = document.createElement('div');
|
backdrop = document.createElement('div');
|
||||||
backdrop.className = 'modal-backdrop fixed inset-0 bg-gray-900 bg-opacity-50 dark:bg-opacity-80 z-40';
|
backdrop.className = 'modal-backdrop fixed inset-0 bg-black/50 z-40';
|
||||||
document.body.appendChild(backdrop);
|
document.body.appendChild(backdrop);
|
||||||
backdrop.addEventListener('click', () => {
|
backdrop.addEventListener('click', () => {
|
||||||
window.closeModelInfoModal();
|
window.closeModelInfoModal();
|
||||||
@@ -2065,8 +2066,8 @@ SOFTWARE.
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
background: #101827 !important;
|
background: var(--color-bg-primary) !important;
|
||||||
border: 1px solid #1E293B;
|
border: 1px solid var(--color-border-subtle);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -2079,7 +2080,7 @@ SOFTWARE.
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: #E5E7EB;
|
color: var(--color-text-primary);
|
||||||
font-family: 'ui-monospace', 'Monaco', 'Consolas', monospace;
|
font-family: 'ui-monospace', 'Monaco', 'Consolas', monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -2182,13 +2183,13 @@ SOFTWARE.
|
|||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar::-webkit-scrollbar-track,
|
.chat-settings-panel::-webkit-scrollbar-track,
|
||||||
#chat::-webkit-scrollbar-track,
|
#chat::-webkit-scrollbar-track,
|
||||||
#messages::-webkit-scrollbar-track {
|
#messages::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar::-webkit-scrollbar-thumb,
|
.chat-settings-panel::-webkit-scrollbar-thumb,
|
||||||
#chat::-webkit-scrollbar-thumb,
|
#chat::-webkit-scrollbar-thumb,
|
||||||
#messages::-webkit-scrollbar-thumb {
|
#messages::-webkit-scrollbar-thumb {
|
||||||
background: rgba(148, 163, 184, 0.2);
|
background: rgba(148, 163, 184, 0.2);
|
||||||
@@ -2196,14 +2197,14 @@ SOFTWARE.
|
|||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar::-webkit-scrollbar-thumb:hover,
|
.chat-settings-panel::-webkit-scrollbar-thumb:hover,
|
||||||
#chat::-webkit-scrollbar-thumb:hover,
|
#chat::-webkit-scrollbar-thumb:hover,
|
||||||
#messages::-webkit-scrollbar-thumb:hover {
|
#messages::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(148, 163, 184, 0.4);
|
background: rgba(148, 163, 184, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Firefox - Minimal */
|
/* Firefox - Minimal */
|
||||||
.sidebar,
|
.chat-settings-panel,
|
||||||
#chat,
|
#chat,
|
||||||
#messages {
|
#messages {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
@@ -2234,5 +2235,8 @@ SOFTWARE.
|
|||||||
scrollbar-color: rgba(148, 163, 184, 0.15) transparent;
|
scrollbar-color: rgba(148, 163, 184, 0.15) transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,51 +2,51 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[#101827] text-[#E5E7EB]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner">
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8 flex-grow">
|
<div class="container mx-auto px-4 py-8 flex-grow">
|
||||||
<!-- Error Section -->
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-error)]/20 rounded-xl p-8 mb-10">
|
||||||
<div class="bg-[#1E293B] border border-red-500/20 rounded-xl p-8 mb-10">
|
|
||||||
<div class="max-w-4xl mx-auto text-center">
|
<div class="max-w-4xl mx-auto text-center">
|
||||||
<div class="mb-6 text-6xl text-red-400">
|
<div class="mb-6 text-6xl text-[var(--color-error)]">
|
||||||
<i class="fas fa-exclamation-circle"></i>
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="hero-title mb-4" style="color: var(--color-error);">
|
<h1 class="hero-title mb-4" style="color: var(--color-error);">
|
||||||
{{if .ErrorCode}}{{.ErrorCode}}{{else}}Error{{end}}
|
{{if .ErrorCode}}{{.ErrorCode}}{{else}}Error{{end}}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-xl text-[#94A3B8] mb-6">{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}An unexpected error occurred{{end}}</p>
|
<p class="text-xl text-[var(--color-text-secondary)] mb-6">{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}An unexpected error occurred{{end}}</p>
|
||||||
<div class="flex flex-wrap justify-center gap-4">
|
<div class="flex flex-wrap justify-center gap-2">
|
||||||
<a href="./"
|
<a href="./" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
class="inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] font-semibold py-3 px-6 rounded-lg transition-colors">
|
<i class="fas fa-home"></i>
|
||||||
<i class="fas fa-home mr-2"></i>
|
|
||||||
<span>Return Home</span>
|
<span>Return Home</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="browse/"
|
<a href="browse/" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
class="inline-flex items-center bg-[#8B5CF6] hover:bg-[#8B5CF6]/90 text-white font-semibold py-3 px-6 rounded-lg transition-colors">
|
<i class="fas fa-images"></i>
|
||||||
<i class="fas fa-images mr-2"></i>
|
|
||||||
<span>Browse Gallery</span>
|
<span>Browse Gallery</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Additional Information -->
|
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8">
|
||||||
<div class="bg-[#1E293B] border border-[#1E293B] rounded-xl p-8">
|
|
||||||
<div class="text-center max-w-3xl mx-auto">
|
<div class="text-center max-w-3xl mx-auto">
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-500/10 border border-yellow-500/20 mb-4">
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-[var(--color-warning-light)] border border-[var(--color-warning)]/20 mb-4">
|
||||||
<i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i>
|
<i class="text-[var(--color-warning)] text-2xl fa-solid fa-triangle-exclamation"></i>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-2xl md:text-3xl font-semibold text-[#E5E7EB] mb-4">Need help?</h2>
|
<h2 class="text-2xl md:text-3xl font-semibold text-[var(--color-text-primary)] mb-4">Need help?</h2>
|
||||||
<p class="text-lg text-[#94A3B8] mb-6">Visit our <a class="text-[#38BDF8] hover:text-[#8B5CF6] underline underline-offset-2 transition-colors" href="browse">🖼️ Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[#38BDF8] hover:text-[#8B5CF6] underline underline-offset-2 transition-colors"> <i class="fa-solid fa-book"></i> Getting started documentation</a></p>
|
<p class="text-lg text-[var(--color-text-secondary)] mb-6">Visit our <a class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors" href="browse">Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors">Getting started documentation</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "views/partials/footer" .}}
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: #101827;
|
background-color: var(--color-bg-primary);
|
||||||
color: #E5E7EB;
|
color: var(--color-text-primary);
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
}
|
}
|
||||||
.token {
|
.token {
|
||||||
@@ -19,51 +19,53 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.network-card {
|
.network-card {
|
||||||
background-color: #2d3748;
|
background-color: var(--color-bg-secondary);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
.network-card:hover {
|
.network-card:hover {
|
||||||
background-color: #374151;
|
background-color: var(--color-bg-tertiary);
|
||||||
}
|
}
|
||||||
.network-title {
|
.network-title {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: #63b3ed;
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
.network-token {
|
.network-token {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: #cbd5e0;
|
color: var(--color-text-secondary);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
word-break: break-word; /* Breaks words to prevent overflow */
|
word-break: break-word;
|
||||||
overflow-wrap: break-word; /* Ensures long strings break */
|
overflow-wrap: break-word;
|
||||||
white-space: pre-wrap; /* Preserves whitespace for breaking */
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
.cluster {
|
.cluster {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
background-color: #4a5568;
|
background-color: var(--color-bg-tertiary);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
.cluster:hover {
|
.cluster:hover {
|
||||||
background-color: #5a6b78;
|
background-color: var(--color-bg-secondary);
|
||||||
}
|
}
|
||||||
.cluster-title {
|
.cluster-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #e2e8f0;
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
.form-container {
|
.form-container {
|
||||||
background-color: #2d3748;
|
background-color: var(--color-bg-secondary);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
border: 1px solid var(--color-border-subtle);
|
||||||
}
|
}
|
||||||
.form-control {
|
.form-control {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
@@ -72,47 +74,50 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #4a5568;
|
border: 1px solid var(--color-border-subtle);
|
||||||
background-color: #3a4250;
|
background-color: var(--color-bg-primary);
|
||||||
color: #e2e8f0;
|
color: var(--color-text-primary);
|
||||||
transition: border-color 0.3s ease, background-color 0.3s ease;
|
transition: border-color 0.3s ease, background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
border-color: #63b3ed;
|
border-color: var(--color-primary);
|
||||||
background-color: #4a5568;
|
background-color: var(--color-bg-tertiary);
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background-color: #3182ce;
|
background-color: var(--color-primary);
|
||||||
color: #e2e8f0;
|
color: white;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--color-primary-hover);
|
||||||
|
}
|
||||||
.error {
|
.error {
|
||||||
color: #e53e3e;
|
color: var(--color-error);
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
.success {
|
.success {
|
||||||
color: #38a169;
|
color: var(--color-success);
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
/* Spinner Styles */
|
|
||||||
.spinner {
|
.spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border: 5px solid rgba(255, 255, 255, 0.2);
|
border: 5px solid var(--color-border-subtle);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border-top-color: #3182ce;
|
border-top-color: var(--color-primary);
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
@@ -121,43 +126,46 @@
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Center the loading text and spinner */
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
}
|
}
|
||||||
.warning-box {
|
.warning-box {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
.warning-box i {
|
.warning-box i {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
.token-box {
|
.token-box {
|
||||||
background-color: #4a5568;
|
background-color: var(--color-bg-tertiary);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
border: 1px solid var(--color-border-subtle);
|
||||||
.token-box:hover {
|
}
|
||||||
background-color: #5a6b7e;
|
.token-box:hover {
|
||||||
}
|
background-color: var(--color-bg-secondary);
|
||||||
.token-text {
|
}
|
||||||
overflow-wrap: break-word;
|
.token-text {
|
||||||
font-family: monospace;
|
overflow-wrap: break-word;
|
||||||
}
|
font-family: monospace;
|
||||||
.copy-icon {
|
}
|
||||||
position: absolute;
|
.copy-icon {
|
||||||
top: 10px;
|
position: absolute;
|
||||||
right: 10px;
|
top: 10px;
|
||||||
color: #e2e8f0;
|
right: 10px;
|
||||||
}
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<body class="bg-gray-900 text-gray-200">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen" x-data="networkClusters()" x-init="init()">
|
<div class="app-layout">
|
||||||
{{template "views/partials/navbar_explorer" .}}
|
{{template "views/partials/navbar_explorer" .}}
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner" x-data="networkClusters()" x-init="init()">
|
||||||
<div class="animation-container">
|
<div class="animation-container">
|
||||||
<canvas id="networkCanvas"></canvas>
|
<canvas id="networkCanvas"></canvas>
|
||||||
<div class="text-overlay">
|
<div class="text-overlay">
|
||||||
@@ -178,8 +186,8 @@
|
|||||||
|
|
||||||
<div class="container mx-auto px-4 flex-grow">
|
<div class="container mx-auto px-4 flex-grow">
|
||||||
<!-- Warning Box -->
|
<!-- Warning Box -->
|
||||||
<div class="warning-box bg-yellow-100 text-gray-800 mb-20 pt-5 pb-5 pr-5 pl-5 text-lg">
|
<div class="warning-box bg-[var(--color-warning-light)] border border-[var(--color-warning)]/30 text-[var(--color-text-primary)] mb-20 pt-5 pb-5 pr-5 pl-5 text-lg rounded-lg">
|
||||||
<i class="fa-solid fa-triangle-exclamation"></i><i class="fa-solid fa-flask"></i>
|
<i class="fa-solid fa-triangle-exclamation text-[var(--color-warning)]"></i><i class="fa-solid fa-flask text-[var(--color-warning)]"></i>
|
||||||
The explorer is a global, community-driven tool to share network tokens and view available clusters in the globe.
|
The explorer is a global, community-driven tool to share network tokens and view available clusters in the globe.
|
||||||
Anyone can use the tokens to offload computation and use the clusters available or share resources.
|
Anyone can use the tokens to offload computation and use the clusters available or share resources.
|
||||||
This is provided without any warranty. Use it at your own risk. We are not responsible for any potential harm or misuse. Sharing tokens globally allows anyone from the internet to use your instances.
|
This is provided without any warranty. Use it at your own risk. We are not responsible for any potential harm or misuse. Sharing tokens globally allows anyone from the internet to use your instances.
|
||||||
@@ -187,9 +195,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flow-root">
|
<div class="flow-root">
|
||||||
<!-- Toggle button for showing/hiding the form -->
|
<!-- Toggle button for showing/hiding the form -->
|
||||||
<button class="btn-primary float-right mb-2" @click="toggleForm()">
|
<button type="button" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors float-right mb-2" @click="toggleForm()">
|
||||||
<!-- Conditional icon display -->
|
<i :class="showForm ? 'fa-solid fa-times' : 'fa-solid fa-plus'"></i>
|
||||||
<i :class="showForm ? 'fa-solid fa-times' : 'fa-solid fa-plus'" class="mr-2"></i>
|
|
||||||
<span x-text="showForm ? 'Close' : 'Add New Network'"></span>
|
<span x-text="showForm ? 'Close' : 'Add New Network'"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,7 +215,7 @@
|
|||||||
<label for="token">Token</label>
|
<label for="token">Token</label>
|
||||||
<textarea id="token" x-model="newNetwork.token" placeholder="Enter token" class="input"></textarea>
|
<textarea id="token" x-model="newNetwork.token" placeholder="Enter token" class="input"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button @click="addNetwork" class="btn-primary"><i class="fa-solid fa-plus"></i> Add Network</button>
|
<button type="button" @click="addNetwork" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors"><i class="fa-solid fa-plus"></i> <span>Add Network</span></button>
|
||||||
<template x-if="errorMessage">
|
<template x-if="errorMessage">
|
||||||
<p class="error" x-text="errorMessage"></p>
|
<p class="error" x-text="errorMessage"></p>
|
||||||
</template>
|
</template>
|
||||||
@@ -259,19 +266,19 @@
|
|||||||
<span class="inline-block bg-blue-500 text-white py-1 px-3 rounded-full text-xs" x-text="'Number of Workers: ' + cluster.Workers.length">
|
<span class="inline-block bg-blue-500 text-white py-1 px-3 rounded-full text-xs" x-text="'Number of Workers: ' + cluster.Workers.length">
|
||||||
</span>
|
</span>
|
||||||
<!-- Give commands and instructions to join the network -->
|
<!-- Give commands and instructions to join the network -->
|
||||||
<span class="inline-block token-box text-white py-1 px-3 text-xs" x-show="cluster.Type == 'federated'" >
|
<span class="inline-block token-box text-white py-1 px-3 text-xs" x-show="cluster.Type == 'federated'" >
|
||||||
<p class="text-lg font-bold mb-4 mt-1">
|
<p class="text-lg font-bold mb-4 mt-1">
|
||||||
<i class="fa-solid fa-copy copy-icon float-right"></i>
|
<i class="fa-solid fa-copy copy-icon float-right"></i>
|
||||||
Command to connect (click to copy):
|
Command to connect (click to copy):
|
||||||
</p>
|
</p>
|
||||||
<code class="block bg-gray-700 text-yellow-300 p-4 rounded-lg break-words" @click="copyToken($el.textContent)" >
|
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words border border-[var(--color-border-subtle)]" @click="copyToken($el.textContent)" >
|
||||||
docker run -d --restart=always -e ADDRESS=":80" -e LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> -e LOCALAI_P2P_LOGLEVEL=debug --name local-ai -e TOKEN="<span class="token" x-text="network.token"></span>" --net host -ti localai/localai:master federated --debug
|
docker run -d --restart=always -e ADDRESS=":80" -e LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> -e LOCALAI_P2P_LOGLEVEL=debug --name local-ai -e TOKEN="<span class="token" x-text="network.token"></span>" --net host -ti localai/localai:master federated --debug
|
||||||
</code>
|
</code>
|
||||||
or via CLI:
|
or via CLI:
|
||||||
<code class="block bg-gray-700 text-yellow-300 p-4 rounded-lg break-words" @click="copyToken($el.textContent)" >
|
<code class="block bg-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words border border-[var(--color-border-subtle)]" @click="copyToken($el.textContent)" >
|
||||||
ADDRESS=":80" LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> LOCALAI_P2P_LOGLEVEL=debug TOKEN="<span class="token" x-text="network.token"></span>" local-ai federated --debug
|
ADDRESS=":80" LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> LOCALAI_P2P_LOGLEVEL=debug TOKEN="<span class="token" x-text="network.token"></span>" local-ai federated --debug
|
||||||
</code>
|
</code>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -371,6 +378,8 @@
|
|||||||
|
|
||||||
{{template "views/partials/footer" .}}
|
{{template "views/partials/footer" .}}
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
<script defer src="static/image.js"></script>
|
<script defer src="static/image.js"></script>
|
||||||
|
|
||||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col flex-1 overflow-hidden">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner h-screen flex flex-col">
|
||||||
<div class="flex flex-1 overflow-hidden">
|
<div class="flex flex-1 overflow-hidden">
|
||||||
<!-- Two Column Layout: Settings on Left, Preview on Right -->
|
<!-- Two Column Layout: Settings on Left, Preview on Right -->
|
||||||
<div class="flex flex-col lg:flex-row flex-1 gap-4 p-4 overflow-hidden">
|
<div class="flex flex-col lg:flex-row flex-1 gap-4 p-4 overflow-hidden">
|
||||||
@@ -237,6 +239,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner">
|
||||||
|
|
||||||
<!-- Main Content - ChatGPT-style minimal interface -->
|
<!-- Main Content - ChatGPT-style minimal interface -->
|
||||||
<div class="flex-1 flex flex-col items-center justify-center px-4 py-12">
|
<div class="flex-1 flex flex-col items-center justify-center px-4 py-12">
|
||||||
@@ -85,19 +87,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center gap-4 mb-8">
|
<div class="flex flex-wrap justify-center gap-2 mb-8">
|
||||||
<a href="/browse/" class="btn-primary">
|
<a href="/browse/" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-images mr-2"></i>
|
<i class="fas fa-images"></i>
|
||||||
Browse Model Gallery
|
<span>Browse Model Gallery</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/import-model" class="btn-primary">
|
<a href="/import-model" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-upload mr-2"></i>
|
<i class="fas fa-upload"></i>
|
||||||
Import Model
|
<span>Import Model</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://localai.io/basics/getting_started/" target="_blank" class="btn-secondary">
|
<a href="https://localai.io/basics/getting_started/" target="_blank" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-graduation-cap mr-2"></i>
|
<i class="fas fa-graduation-cap"></i>
|
||||||
Getting Started
|
<span>Getting Started</span>
|
||||||
<i class="fas fa-external-link-alt ml-2 text-sm"></i>
|
<i class="fas fa-external-link-alt text-[10px]"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
@@ -524,6 +526,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "views/partials/footer" .}}
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[#101827] text-[#E5E7EB]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen">
|
<div class="app-layout no-sidebar">
|
||||||
|
<main class="main-content">
|
||||||
{{template "views/partials/navbar" .}}
|
<div class="main-content-inner">
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8 flex-grow flex items-center justify-center">
|
<div class="container mx-auto px-4 py-8 flex-grow flex items-center justify-center">
|
||||||
<!-- Auth Card -->
|
<!-- Auth Card -->
|
||||||
<div class="max-w-md w-full bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl overflow-hidden">
|
<div class="max-w-md w-full bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl overflow-hidden">
|
||||||
<div class="animation-container">
|
<div class="animation-container">
|
||||||
<div class="text-overlay">
|
<div class="text-overlay">
|
||||||
<img src="static/logo.png" alt="LocalAI Logo" class="h-32 drop-shadow-[0_0_15px_rgba(56,189,248,0.3)]">
|
<img src="static/logo.png" alt="LocalAI Logo" class="h-32 drop-shadow-[0_0_15px_rgba(56,189,248,0.3)]">
|
||||||
@@ -21,22 +21,22 @@
|
|||||||
<h2 class="h2">
|
<h2 class="h2">
|
||||||
Authorization Required
|
Authorization Required
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-[#94A3B8] mt-2">Please enter your access token to continue</p>
|
<p class="text-[var(--color-text-secondary)] mt-2">Please enter your access token to continue</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="login-form" class="space-y-6" onsubmit="login(); return false;">
|
<form id="login-form" class="space-y-6" onsubmit="login(); return false;">
|
||||||
<div>
|
<div>
|
||||||
<label for="token" class="block text-sm font-medium text-[#94A3B8] mb-2">Access Token</label>
|
<label for="token" class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Access Token</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none z-10">
|
<div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none z-10">
|
||||||
<i class="fas fa-key text-[#38BDF8]"></i>
|
<i class="fas fa-key text-[var(--color-primary)]"></i>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="token"
|
id="token"
|
||||||
name="token"
|
name="token"
|
||||||
placeholder="Enter your token"
|
placeholder="Enter your token"
|
||||||
class="bg-[#101827] border border-[#1E293B] text-[#E5E7EB] placeholder-[#94A3B8] text-sm rounded-lg focus:ring-[#38BDF8] focus:border-[#38BDF8] focus:ring-2 block w-full p-2.5 transition-all"
|
class="input"
|
||||||
style="padding-left: 3.5rem !important;"
|
style="padding-left: 3.5rem !important;"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -44,19 +44,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button type="submit"
|
||||||
type="submit"
|
class="inline-flex items-center justify-center gap-1.5 w-full text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
class="w-full flex items-center justify-center bg-[#38BDF8] hover:bg-[#38BDF8]/90 text-[#101827] font-semibold py-3 px-6 rounded-lg transition-colors"
|
<i class="fas fa-sign-in-alt"></i>
|
||||||
>
|
|
||||||
<i class="fas fa-sign-in-alt mr-2"></i>
|
|
||||||
<span>Login</span>
|
<span>Login</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="mt-8 pt-6 border-t border-[#1E293B] text-center text-sm text-[#94A3B8]">
|
<div class="mt-8 pt-6 border-t border-[var(--color-border-subtle)] text-center text-sm text-[var(--color-text-secondary)]">
|
||||||
<div class="flex items-center justify-center mb-2">
|
<div class="flex items-center justify-center mb-2">
|
||||||
<i class="fas fa-shield-alt mr-2 text-[#38BDF8]"></i>
|
<i class="fas fa-shield-alt mr-2 text-[var(--color-primary)]"></i>
|
||||||
<span>Instance is token protected</span>
|
<span>Instance is token protected</span>
|
||||||
</div>
|
</div>
|
||||||
<p>Current time (UTC): <span id="current-time">{{.CurrentDate}}</span></p>
|
<p>Current time (UTC): <span id="current-time">{{.CurrentDate}}</span></p>
|
||||||
@@ -66,6 +64,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "views/partials/footer" .}}
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen" x-data="indexDashboard()">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner" x-data="indexDashboard()">
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
|
<div class="fixed top-20 right-4 z-50 space-y-2" style="max-width: 400px;">
|
||||||
@@ -40,36 +42,6 @@
|
|||||||
Model & Backend Management
|
Model & Backend Management
|
||||||
</h1>
|
</h1>
|
||||||
<p class="hero-subtitle">Manage your installed models and backends</p>
|
<p class="hero-subtitle">Manage your installed models and backends</p>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
|
||||||
<div class="flex flex-wrap justify-center gap-3">
|
|
||||||
<a href="browse/" class="btn-primary text-sm py-1.5 px-3">
|
|
||||||
<i class="fas fa-images mr-1.5 text-[10px]"></i>
|
|
||||||
<span>Model Gallery</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="/import-model" class="btn-primary text-sm py-1.5 px-3">
|
|
||||||
<i class="fas fa-plus mr-1.5 text-[10px]"></i>
|
|
||||||
<span>Import Model</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<button id="reload-models-btn" class="btn-primary text-sm py-1.5 px-3">
|
|
||||||
<i class="fas fa-sync-alt mr-1.5 text-[10px]"></i>
|
|
||||||
<span>Update Models</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="/browse/backends" class="btn-secondary text-sm py-1.5 px-3">
|
|
||||||
<i class="fas fa-cogs mr-1.5 text-[10px]"></i>
|
|
||||||
<span>Backend Gallery</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{ if not .DisableRuntimeSettings }}
|
|
||||||
<a href="/settings" class="btn-secondary text-sm py-1.5 px-3">
|
|
||||||
<i class="fas fa-cog mr-1.5 text-[10px]"></i>
|
|
||||||
<span>Settings</span>
|
|
||||||
</a>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -169,6 +141,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- Models storage (disk usage) -->
|
||||||
|
<template x-if="resourceData.storage_size != null">
|
||||||
|
<div class="mt-3 pt-3 border-t border-[var(--color-primary-border)]/20">
|
||||||
|
<div class="flex justify-between text-xs">
|
||||||
|
<span class="text-[var(--color-text-secondary)]">Models storage</span>
|
||||||
|
<span class="font-mono text-[var(--color-text-primary)]" x-text="formatBytes(resourceData.storage_size)"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,17 +169,17 @@
|
|||||||
<p class="text-sm text-[var(--color-text-secondary)] mb-6">Get started by installing a model from the gallery or importing it</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mb-6">Get started by installing a model from the gallery or importing it</p>
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center gap-2 mb-6">
|
<div class="flex flex-wrap justify-center gap-2 mb-6">
|
||||||
<a href="browse" class="btn-primary text-sm py-1.5 px-3">
|
<a href="browse" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-images mr-1.5 text-[10px]"></i>
|
<i class="fas fa-images text-[10px]"></i>
|
||||||
Browse Model Gallery
|
<span>Browse Model Gallery</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="/import-model" class="btn-primary text-sm py-1.5 px-3">
|
<a href="/import-model" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-upload mr-1.5 text-[10px]"></i>
|
<i class="fas fa-upload text-[10px]"></i>
|
||||||
Import Model
|
<span>Import Model</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://localai.io/basics/getting_started/" target="_blank" class="btn-secondary text-sm py-1.5 px-3">
|
<a href="https://localai.io/basics/getting_started/" target="_blank" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-book mr-1.5 text-[10px]"></i>
|
<i class="fas fa-book text-[10px]"></i>
|
||||||
Documentation
|
<span>Documentation</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -226,13 +207,22 @@
|
|||||||
{{ $modelsN := len .ModelsConfig}}
|
{{ $modelsN := len .ModelsConfig}}
|
||||||
{{ $modelsN = add $modelsN (len .Models)}}
|
{{ $modelsN = add $modelsN (len .Models)}}
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="h3 mb-1 flex items-center">
|
<div class="flex items-center justify-between gap-3 mb-1">
|
||||||
<i class="fas fa-brain mr-2 text-[var(--color-primary)] text-sm"></i>
|
<div>
|
||||||
Installed Models
|
<h2 class="h3 flex items-center">
|
||||||
</h2>
|
<i class="fas fa-brain mr-2 text-[var(--color-primary)] text-sm"></i>
|
||||||
<p class="text-sm text-[var(--color-text-secondary)] mb-4">
|
Installed Models
|
||||||
<span class="text-[var(--color-primary)] font-medium">{{$modelsN}}</span> model{{if gt $modelsN 1}}s{{end}} ready to use
|
</h2>
|
||||||
</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mt-0.5">
|
||||||
|
<span class="text-[var(--color-primary)] font-medium">{{$modelsN}}</span> model{{if gt $modelsN 1}}s{{end}} ready to use
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button id="reload-models-btn" type="button" title="Update models list from disk"
|
||||||
|
class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
|
<i class="fas fa-sync-alt text-[10px]"></i>
|
||||||
|
<span>Update</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto mb-8">
|
<div class="overflow-x-auto mb-8">
|
||||||
@@ -337,13 +327,13 @@
|
|||||||
<td class="p-2">
|
<td class="p-2">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
{{ if index $loadedModels .Name }}
|
{{ if index $loadedModels .Name }}
|
||||||
<button class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
<button type="button" class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
||||||
onclick="handleStopModel('{{.Name}}')"
|
onclick="handleStopModel('{{.Name}}')"
|
||||||
title="Stop {{.Name}}">
|
title="Stop {{.Name}}">
|
||||||
<i class="fas fa-stop text-xs"></i>
|
<i class="fas fa-stop text-xs"></i>
|
||||||
</button>
|
</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<button class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
<button type="button" class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
||||||
onclick="handleDeleteModel('{{.Name}}')"
|
onclick="handleDeleteModel('{{.Name}}')"
|
||||||
title="Delete {{.Name}}">
|
title="Delete {{.Name}}">
|
||||||
<i class="fas fa-trash-alt text-xs"></i>
|
<i class="fas fa-trash-alt text-xs"></i>
|
||||||
@@ -395,12 +385,12 @@
|
|||||||
Installed Backends
|
Installed Backends
|
||||||
</h2>
|
</h2>
|
||||||
{{ if gt (len .InstalledBackends) 0 }}
|
{{ if gt (len .InstalledBackends) 0 }}
|
||||||
<button
|
<button type="button"
|
||||||
@click="reinstallAllBackends()"
|
@click="reinstallAllBackends()"
|
||||||
:disabled="reinstallingAll"
|
:disabled="reinstallingAll"
|
||||||
class="btn-primary text-sm py-1.5 px-3"
|
class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border-subtle)]"
|
||||||
title="Reinstall all backends">
|
title="Reinstall all backends">
|
||||||
<i class="fas fa-arrow-rotate-right mr-1.5 text-[10px]" :class="reinstallingAll ? 'fa-spin' : ''"></i>
|
<i class="fas fa-arrow-rotate-right text-[10px]" :class="reinstallingAll ? 'fa-spin' : ''"></i>
|
||||||
<span x-text="reinstallingAll ? 'Reinstalling...' : 'Reinstall All'"></span>
|
<span x-text="reinstallingAll ? 'Reinstalling...' : 'Reinstall All'"></span>
|
||||||
</button>
|
</button>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -420,14 +410,14 @@
|
|||||||
<h2 class="h2 mb-2">No backends installed yet</h2>
|
<h2 class="h2 mb-2">No backends installed yet</h2>
|
||||||
<p class="text-sm text-[var(--color-text-secondary)] mb-6">Backends power your AI models. Install them from the backend gallery to get started</p>
|
<p class="text-sm text-[var(--color-text-secondary)] mb-6">Backends power your AI models. Install them from the backend gallery to get started</p>
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center gap-3">
|
<div class="flex flex-wrap justify-center gap-2">
|
||||||
<a href="/browse/backends" class="btn-primary">
|
<a href="/browse/backends" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-cogs mr-2 text-xs"></i>
|
<i class="fas fa-cogs text-[10px]"></i>
|
||||||
Browse Backend Gallery
|
<span>Browse Backend Gallery</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://localai.io/backends/" target="_blank" class="btn-secondary">
|
<a href="https://localai.io/backends/" target="_blank" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-book mr-2 text-xs"></i>
|
<i class="fas fa-book text-[10px]"></i>
|
||||||
Documentation
|
<span>Documentation</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -500,14 +490,14 @@
|
|||||||
<td class="p-2">
|
<td class="p-2">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<div class="flex items-center justify-end gap-1">
|
||||||
{{ if not .IsSystem }}
|
{{ if not .IsSystem }}
|
||||||
<button
|
<button type="button"
|
||||||
@click="reinstallBackend('{{.Name}}')"
|
@click="reinstallBackend('{{.Name}}')"
|
||||||
:disabled="reinstallingBackends['{{.Name}}']"
|
:disabled="reinstallingBackends['{{.Name}}']"
|
||||||
class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 disabled:opacity-50 disabled:cursor-not-allowed rounded p-1 transition-colors"
|
class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 disabled:opacity-50 disabled:cursor-not-allowed rounded p-1 transition-colors"
|
||||||
title="Reinstall {{.Name}}">
|
title="Reinstall {{.Name}}">
|
||||||
<i class="fas fa-arrow-rotate-right text-xs" :class="reinstallingBackends['{{.Name}}'] ? 'fa-spin' : ''"></i>
|
<i class="fas fa-arrow-rotate-right text-xs" :class="reinstallingBackends['{{.Name}}'] ? 'fa-spin' : ''"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
@click="deleteBackend('{{.Name}}')"
|
@click="deleteBackend('{{.Name}}')"
|
||||||
class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors"
|
||||||
title="Delete {{.Name}}">
|
title="Delete {{.Name}}">
|
||||||
@@ -527,7 +517,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "views/partials/footer" .}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -869,6 +858,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
{{template "views/partials/head" .}}
|
{{template "views/partials/head" .}}
|
||||||
|
|
||||||
<body class="bg-[#101827] text-[#E5E7EB]">
|
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
|
||||||
<div class="flex flex-col min-h-screen" x-data="importModel()" x-init="init()">
|
<div class="app-layout">
|
||||||
|
{{template "views/partials/navbar" .}}
|
||||||
{{template "views/partials/navbar" .}}
|
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="main-content-inner" x-data="importModel()" x-init="init()">
|
||||||
{{template "views/partials/inprogress" .}}
|
{{template "views/partials/inprogress" .}}
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-8 flex-grow">
|
<div class="container mx-auto px-4 py-8 flex-grow">
|
||||||
@@ -22,30 +24,30 @@
|
|||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<!-- Mode Toggle (only show when not in edit mode) -->
|
<!-- Mode Toggle (only show when not in edit mode) -->
|
||||||
<template x-if="!isEditMode">
|
<template x-if="!isEditMode">
|
||||||
<button @click="toggleMode()" class="btn-secondary">
|
<button type="button" @click="toggleMode()" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas" :class="isAdvancedMode ? 'fa-magic mr-2' : 'fa-code mr-2'"></i>
|
<i class="fas" :class="isAdvancedMode ? 'fa-magic' : 'fa-code'"></i>
|
||||||
<span x-text="isAdvancedMode ? 'Simple Mode' : 'Advanced Mode'"></span>
|
<span x-text="isAdvancedMode ? 'Simple Mode' : 'Advanced Mode'"></span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<!-- Advanced Mode Buttons -->
|
<!-- Advanced Mode Buttons -->
|
||||||
<template x-if="isAdvancedMode">
|
<template x-if="isAdvancedMode">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-2">
|
||||||
<button id="validateBtn" class="btn-primary">
|
<button type="button" id="validateBtn" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-check mr-2"></i>
|
<i class="fas fa-check"></i>
|
||||||
<span>Validate</span>
|
<span>Validate</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="saveBtn" class="btn-primary">
|
<button type="button" id="saveBtn" class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors">
|
||||||
<i class="fas fa-save mr-2"></i>
|
<i class="fas fa-save"></i>
|
||||||
<span>{{if .ModelName}}Update{{else}}Create{{end}}</span>
|
<span>{{if .ModelName}}Update{{else}}Create{{end}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- Simple Mode Button -->
|
<!-- Simple Mode Button -->
|
||||||
<template x-if="!isAdvancedMode && !isEditMode">
|
<template x-if="!isAdvancedMode && !isEditMode">
|
||||||
<button @click="submitImport()"
|
<button type="button" @click="submitImport()"
|
||||||
:disabled="isSubmitting || !importUri.trim()"
|
:disabled="isSubmitting || !importUri.trim()"
|
||||||
class="btn-primary">
|
class="inline-flex items-center gap-1.5 text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] bg-transparent hover:bg-[var(--color-primary)]/10 border border-[var(--color-border-subtle)] hover:border-[var(--color-primary)]/30 rounded-md py-1.5 px-2.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-[var(--color-border-subtle)]">
|
||||||
<i class="fas" :class="isSubmitting ? 'fa-spinner fa-spin mr-2' : 'fa-upload mr-2'"></i>
|
<i class="fas text-[10px]" :class="isSubmitting ? 'fa-spinner fa-spin' : 'fa-upload'"></i>
|
||||||
<span x-text="isSubmitting ? 'Importing...' : 'Import Model'"></span>
|
<span x-text="isSubmitting ? 'Importing...' : 'Import Model'"></span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -57,6 +59,26 @@
|
|||||||
<!-- Alert Messages -->
|
<!-- Alert Messages -->
|
||||||
<div id="alertContainer" class="mb-6"></div>
|
<div id="alertContainer" class="mb-6"></div>
|
||||||
|
|
||||||
|
<!-- Persistent estimate (stays visible so user can see size/VRAM even if alert is replaced) -->
|
||||||
|
<div x-show="!isAdvancedMode && !isEditMode && lastEstimate && ((lastEstimate.sizeDisplay && lastEstimate.sizeDisplay !== '0 B') || (lastEstimate.vramDisplay && lastEstimate.vramDisplay !== '0 B'))"
|
||||||
|
x-transition
|
||||||
|
class="mb-6 p-4 rounded-xl border border-[var(--color-primary)]/30 bg-[var(--color-primary-light)]/30">
|
||||||
|
<h3 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
|
||||||
|
<i class="fas fa-memory text-[var(--color-primary)]"></i>
|
||||||
|
Estimated requirements
|
||||||
|
</h3>
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm text-[var(--color-text-secondary)]">
|
||||||
|
<span x-show="lastEstimate && lastEstimate.sizeDisplay && lastEstimate.sizeDisplay !== '0 B'">
|
||||||
|
<i class="fas fa-download mr-1.5 text-[var(--color-primary)]"></i>
|
||||||
|
Download size: <span class="font-medium text-[var(--color-text-primary)]" x-text="lastEstimate?.sizeDisplay"></span>
|
||||||
|
</span>
|
||||||
|
<span x-show="lastEstimate && lastEstimate.vramDisplay && lastEstimate.vramDisplay !== '0 B'">
|
||||||
|
<i class="fas fa-microchip mr-1.5 text-[var(--color-primary)]"></i>
|
||||||
|
VRAM: <span class="font-medium text-[var(--color-text-primary)]" x-text="lastEstimate?.vramDisplay"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Simple Import Mode -->
|
<!-- Simple Import Mode -->
|
||||||
<div x-show="!isAdvancedMode && !isEditMode"
|
<div x-show="!isAdvancedMode && !isEditMode"
|
||||||
x-transition:enter="transition ease-out duration-200"
|
x-transition:enter="transition ease-out duration-200"
|
||||||
@@ -64,9 +86,9 @@
|
|||||||
x-transition:enter-end="opacity-100"
|
x-transition:enter-end="opacity-100"
|
||||||
class="card p-8">
|
class="card p-8">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<h2 class="text-2xl font-semibold text-[#E5E7EB] flex items-center gap-3 mb-6">
|
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] flex items-center gap-3 mb-6">
|
||||||
<div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center">
|
<div class="w-10 h-10 rounded-lg bg-[var(--color-success-light)] flex items-center justify-center">
|
||||||
<i class="fas fa-link text-green-400"></i>
|
<i class="fas fa-link text-[var(--color-success)]"></i>
|
||||||
</div>
|
</div>
|
||||||
Import from URI
|
Import from URI
|
||||||
</h2>
|
</h2>
|
||||||
@@ -74,20 +96,20 @@
|
|||||||
<!-- URI Input -->
|
<!-- URI Input -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<label class="block text-sm font-medium text-[#94A3B8]">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)]">
|
||||||
<i class="fas fa-link mr-2"></i>Model URI
|
<i class="fas fa-link mr-2"></i>Model URI
|
||||||
</label>
|
</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a href="https://huggingface.co/models?search=gguf&sort=trending"
|
<a href="https://huggingface.co/models?search=gguf&sort=trending"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-xs px-3 py-1.5 rounded-lg bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 border border-purple-500/30 transition-all flex items-center gap-1.5">
|
class="text-xs px-3 py-1.5 rounded-lg bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-accent)] border border-[var(--color-accent)]/30 transition-all flex items-center gap-1.5">
|
||||||
<i class="fab fa-huggingface"></i>
|
<i class="fab fa-huggingface"></i>
|
||||||
<span>Search GGUF Models on Hugging Face</span>
|
<span>Search GGUF Models on Hugging Face</span>
|
||||||
<i class="fas fa-external-link-alt text-xs"></i>
|
<i class="fas fa-external-link-alt text-xs"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://huggingface.co/models?sort=trending"
|
<a href="https://huggingface.co/models?sort=trending"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-xs px-3 py-1.5 rounded-lg bg-purple-600/20 hover:bg-purple-600/30 text-purple-300 border border-purple-500/30 transition-all flex items-center gap-1.5">
|
class="text-xs px-3 py-1.5 rounded-lg bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-accent)] border border-[var(--color-accent)]/30 transition-all flex items-center gap-1.5">
|
||||||
<i class="fab fa-huggingface"></i>
|
<i class="fab fa-huggingface"></i>
|
||||||
<span>Browse All Models on Hugging Face</span>
|
<span>Browse All Models on Hugging Face</span>
|
||||||
<i class="fas fa-external-link-alt text-xs"></i>
|
<i class="fas fa-external-link-alt text-xs"></i>
|
||||||
@@ -100,14 +122,14 @@
|
|||||||
placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf"
|
placeholder="huggingface://TheBloke/Llama-2-7B-Chat-GGUF or https://example.com/model.gguf"
|
||||||
class="input w-full"
|
class="input w-full"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<p class="mt-2 text-xs text-[#94A3B8]">
|
<p class="mt-2 text-xs text-[var(--color-text-secondary)]">
|
||||||
Enter the URI or path to the model file you want to import
|
Enter the URI or path to the model file you want to import
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- URI Format Guide -->
|
<!-- URI Format Guide -->
|
||||||
<div class="mt-4" x-data="{ showGuide: false }">
|
<div class="mt-4" x-data="{ showGuide: false }">
|
||||||
<button @click="showGuide = !showGuide"
|
<button @click="showGuide = !showGuide"
|
||||||
class="flex items-center gap-2 text-sm text-[#94A3B8] hover:text-[#E5E7EB] transition-colors">
|
class="flex items-center gap-2 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
|
||||||
<i class="fas" :class="showGuide ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
|
<i class="fas" :class="showGuide ? 'fa-chevron-down' : 'fa-chevron-right'"></i>
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
<span>Supported URI Formats</span>
|
<span>Supported URI Formats</span>
|
||||||
@@ -117,34 +139,34 @@
|
|||||||
x-transition:enter="transition ease-out duration-200"
|
x-transition:enter="transition ease-out duration-200"
|
||||||
x-transition:enter-start="opacity-0 transform -translate-y-2"
|
x-transition:enter-start="opacity-0 transform -translate-y-2"
|
||||||
x-transition:enter-end="opacity-100 transform translate-y-0"
|
x-transition:enter-end="opacity-100 transform translate-y-0"
|
||||||
class="mt-3 p-4 bg-[#101827] border border-[#1E293B] rounded-lg space-y-4">
|
class="mt-3 p-4 bg-[var(--color-bg-primary)] border border-[var(--color-border-subtle)] rounded-lg space-y-4">
|
||||||
|
|
||||||
<!-- HuggingFace -->
|
<!-- HuggingFace -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
|
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
|
||||||
<i class="fab fa-huggingface text-purple-400"></i>
|
<i class="fab fa-huggingface text-[var(--color-accent)]"></i>
|
||||||
HuggingFace
|
HuggingFace
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
|
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#10B981]">huggingface://</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
|
<code class="text-[var(--color-success)]">huggingface://</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Standard HuggingFace format</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Standard HuggingFace format</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#10B981]">hf://</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
|
<code class="text-[var(--color-success)]">hf://</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Short HuggingFace format</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Short HuggingFace format</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#10B981]">https://huggingface.co/</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
|
<code class="text-[var(--color-success)]">https://huggingface.co/</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Full HuggingFace URL</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Full HuggingFace URL</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,16 +174,16 @@
|
|||||||
|
|
||||||
<!-- HTTP/HTTPS -->
|
<!-- HTTP/HTTPS -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
|
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
|
||||||
<i class="fas fa-globe text-blue-400"></i>
|
<i class="fas fa-globe text-[var(--color-primary)]"></i>
|
||||||
HTTP/HTTPS URLs
|
HTTP/HTTPS URLs
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
|
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#10B981]">https://</code><span class="text-[#94A3B8]">example.com/model.gguf</span>
|
<code class="text-[var(--color-success)]">https://</code><span class="text-[var(--color-text-secondary)]">example.com/model.gguf</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Direct download from any HTTPS URL</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Direct download from any HTTPS URL</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,23 +191,23 @@
|
|||||||
|
|
||||||
<!-- Local Files -->
|
<!-- Local Files -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
|
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
|
||||||
<i class="fas fa-file text-yellow-400"></i>
|
<i class="fas fa-file text-[var(--color-warning)]"></i>
|
||||||
Local Files
|
Local Files
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
|
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#10B981]">file://</code><span class="text-[#94A3B8]">/path/to/model.gguf</span>
|
<code class="text-[var(--color-success)]">file://</code><span class="text-[var(--color-text-secondary)]">/path/to/model.gguf</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Local file path (absolute)</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Local file path (absolute)</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#94A3B8]">/path/to/model.yaml</code>
|
<span class="text-[var(--color-text-secondary)]">/path/to/model.yaml</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Direct local YAML config file</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Direct local YAML config file</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,23 +215,23 @@
|
|||||||
|
|
||||||
<!-- OCI -->
|
<!-- OCI -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
|
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
|
||||||
<i class="fas fa-box text-cyan-400"></i>
|
<i class="fas fa-box text-cyan-400"></i>
|
||||||
OCI Registry
|
OCI Registry
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
|
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#10B981]">oci://</code><span class="text-[#94A3B8]">registry.example.com/model:tag</span>
|
<code class="text-[var(--color-success)]">oci://</code><span class="text-[var(--color-text-secondary)]">registry.example.com/model:tag</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">OCI container registry</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">OCI container registry</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#10B981]">ocifile://</code><span class="text-[#94A3B8]">/path/to/image.tar</span>
|
<code class="text-[var(--color-success)]">ocifile://</code><span class="text-[var(--color-text-secondary)]">/path/to/image.tar</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Local OCI tarball file</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Local OCI tarball file</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,16 +239,16 @@
|
|||||||
|
|
||||||
<!-- Ollama -->
|
<!-- Ollama -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
|
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
|
||||||
<i class="fas fa-cube text-indigo-400"></i>
|
<i class="fas fa-cube text-indigo-400"></i>
|
||||||
Ollama
|
Ollama
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
|
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#10B981]">ollama://</code><span class="text-[#94A3B8]">llama2:7b</span>
|
<code class="text-[var(--color-success)]">ollama://</code><span class="text-[var(--color-text-secondary)]">llama2:7b</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Ollama model format</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Ollama model format</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,31 +256,31 @@
|
|||||||
|
|
||||||
<!-- YAML Config Files -->
|
<!-- YAML Config Files -->
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
|
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2">
|
||||||
<i class="fas fa-code text-pink-400"></i>
|
<i class="fas fa-code text-pink-400"></i>
|
||||||
YAML Configuration Files
|
YAML Configuration Files
|
||||||
</h4>
|
</h4>
|
||||||
<div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
|
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#94A3B8]">https://example.com/model.yaml</code>
|
<span class="text-[var(--color-text-secondary)]">https://example.com/model.yaml</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Remote YAML config file</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Remote YAML config file</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-green-400">•</span>
|
<span class="text-[var(--color-success)]">•</span>
|
||||||
<div>
|
<div>
|
||||||
<code class="text-[#94A3B8]">file:///path/to/config.yaml</code>
|
<span class="text-[var(--color-text-secondary)]">file:///path/to/config.yaml</span>
|
||||||
<p class="text-[#6B7280] mt-0.5">Local YAML config file</p>
|
<p class="text-[var(--color-text-muted)] mt-0.5">Local YAML config file</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-2 mt-3 border-t border-[#1E293B]">
|
<div class="pt-2 mt-3 border-t border-[var(--color-border-subtle)]">
|
||||||
<p class="text-xs text-[#6B7280] italic">
|
<p class="text-xs text-[var(--color-text-muted)] italic">
|
||||||
<i class="fas fa-lightbulb mr-1.5 text-yellow-400"></i>
|
<i class="fas fa-lightbulb mr-1.5 text-[var(--color-warning)]"></i>
|
||||||
Tip: For HuggingFace models, you can use any of the three formats. The system will automatically detect and download the appropriate model files.
|
Tip: For HuggingFace models, you can use any of the three formats. The system will automatically detect and download the appropriate model files.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,25 +291,25 @@
|
|||||||
<!-- Preferences Section -->
|
<!-- Preferences Section -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-300">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)]">
|
||||||
<i class="fas fa-cog mr-2"></i>Preferences (Optional)
|
<i class="fas fa-cog mr-2"></i>Preferences (Optional)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Common Preferences -->
|
<!-- Common Preferences -->
|
||||||
<div class="space-y-4 mb-6 p-4 bg-gray-900/50 rounded-xl border border-gray-700/50">
|
<div class="space-y-4 mb-6 p-4 bg-[var(--color-bg-primary)]/50 rounded-xl border border-[var(--color-border-subtle)]/50">
|
||||||
<h3 class="text-sm font-semibold text-gray-300 mb-3 flex items-center">
|
<h3 class="text-sm font-semibold text-[var(--color-text-secondary)] mb-3 flex items-center">
|
||||||
<i class="fas fa-star mr-2 text-yellow-400"></i>Common Preferences
|
<i class="fas fa-star mr-2 text-[var(--color-warning)]"></i>Common Preferences
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Backend Selection -->
|
<!-- Backend Selection -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-server mr-2"></i>Backend
|
<i class="fas fa-server mr-2"></i>Backend
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
x-model="commonPreferences.backend"
|
x-model="commonPreferences.backend"
|
||||||
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="input w-full px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<option value="">Auto-detect (based on URI)</option>
|
<option value="">Auto-detect (based on URI)</option>
|
||||||
<option value="llama-cpp">llama-cpp</option>
|
<option value="llama-cpp">llama-cpp</option>
|
||||||
@@ -297,30 +319,30 @@
|
|||||||
<option value="vllm">vllm</option>
|
<option value="vllm">vllm</option>
|
||||||
<option value="diffusers">diffusers</option>
|
<option value="diffusers">diffusers</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Force a specific backend. Leave empty to auto-detect from URI.
|
Force a specific backend. Leave empty to auto-detect from URI.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Name -->
|
<!-- Model Name -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-tag mr-2"></i>Model Name
|
<i class="fas fa-tag mr-2"></i>Model Name
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
x-model="commonPreferences.name"
|
x-model="commonPreferences.name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Leave empty to use filename"
|
placeholder="Leave empty to use filename"
|
||||||
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="input w-full px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Custom name for the model. If empty, the filename will be used.
|
Custom name for the model. If empty, the filename will be used.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-align-left mr-2"></i>Description
|
<i class="fas fa-align-left mr-2"></i>Description
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -329,39 +351,39 @@
|
|||||||
placeholder="Leave empty to use default description"
|
placeholder="Leave empty to use default description"
|
||||||
class="input w-full resize-none"
|
class="input w-full resize-none"
|
||||||
:disabled="isSubmitting"></textarea>
|
:disabled="isSubmitting"></textarea>
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Custom description for the model. If empty, a default description will be generated.
|
Custom description for the model. If empty, a default description will be generated.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quantizations -->
|
<!-- Quantizations -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-layer-group mr-2"></i>Quantizations
|
<i class="fas fa-layer-group mr-2"></i>Quantizations
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
x-model="commonPreferences.quantizations"
|
x-model="commonPreferences.quantizations"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="q4_k_m,q4_k_s,q3_k_m (comma-separated)"
|
placeholder="q4_k_m,q4_k_s,q3_k_m (comma-separated)"
|
||||||
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="input w-full px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Preferred quantizations (comma-separated). Examples: q4_k_m, q4_k_s, q3_k_m, q2_k. Leave empty to use default (q4_k_m).
|
Preferred quantizations (comma-separated). Examples: q4_k_m, q4_k_s, q3_k_m, q2_k. Leave empty to use default (q4_k_m).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MMProj Quantizations -->
|
<!-- MMProj Quantizations -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-image mr-2"></i>MMProj Quantizations
|
<i class="fas fa-image mr-2"></i>MMProj Quantizations
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
x-model="commonPreferences.mmproj_quantizations"
|
x-model="commonPreferences.mmproj_quantizations"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="fp16,fp32 (comma-separated)"
|
placeholder="fp16,fp32 (comma-separated)"
|
||||||
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="input w-full px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Preferred MMProj quantizations (comma-separated). Examples: fp16, fp32. Leave empty to use default (fp16).
|
Preferred MMProj quantizations (comma-separated). Examples: fp16, fp32. Leave empty to use default (fp16).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -372,77 +394,77 @@
|
|||||||
<input
|
<input
|
||||||
x-model="commonPreferences.embeddings"
|
x-model="commonPreferences.embeddings"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="w-5 h-5 rounded bg-gray-900/90 border-gray-700/70 text-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all cursor-pointer"
|
class="w-5 h-5 rounded bg-[var(--color-bg-primary)] border-[var(--color-border-subtle)] text-[var(--color-success)] focus:ring-2 focus:ring-[var(--color-success)]/50 focus:outline-none transition-all cursor-pointer"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<span class="ml-3 text-sm font-medium text-gray-300">
|
<span class="ml-3 text-sm font-medium text-[var(--color-text-secondary)]">
|
||||||
<i class="fas fa-vector-square mr-2"></i>Embeddings
|
<i class="fas fa-vector-square mr-2"></i>Embeddings
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="mt-1 ml-8 text-xs text-gray-400">
|
<p class="mt-1 ml-8 text-xs text-[var(--color-text-muted)]">
|
||||||
Enable embeddings support for this model.
|
Enable embeddings support for this model.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Type -->
|
<!-- Model Type -->
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-tag mr-2"></i>Model Type
|
<i class="fas fa-tag mr-2"></i>Model Type
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
x-model="commonPreferences.type"
|
x-model="commonPreferences.type"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="AutoModelForCausalLM (for transformers backend)"
|
placeholder="AutoModelForCausalLM (for transformers backend)"
|
||||||
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="input w-full px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba, MusicgenForConditionalGeneration. Leave empty to use default (AutoModelForCausalLM).
|
Model type for transformers backend. Examples: AutoModelForCausalLM, SentenceTransformer, Mamba, MusicgenForConditionalGeneration. Leave empty to use default (AutoModelForCausalLM).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pipeline Type (Diffusers) -->
|
<!-- Pipeline Type (Diffusers) -->
|
||||||
<div x-show="commonPreferences.backend === 'diffusers'">
|
<div x-show="commonPreferences.backend === 'diffusers'">
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-stream mr-2"></i>Pipeline Type
|
<i class="fas fa-stream mr-2"></i>Pipeline Type
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
x-model="commonPreferences.pipeline_type"
|
x-model="commonPreferences.pipeline_type"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="StableDiffusionPipeline (for diffusers backend)"
|
placeholder="StableDiffusionPipeline (for diffusers backend)"
|
||||||
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="input w-full px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Pipeline type for diffusers backend. Examples: StableDiffusionPipeline, StableDiffusion3Pipeline, FluxPipeline. Leave empty to use default (StableDiffusionPipeline).
|
Pipeline type for diffusers backend. Examples: StableDiffusionPipeline, StableDiffusion3Pipeline, FluxPipeline. Leave empty to use default (StableDiffusionPipeline).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scheduler Type (Diffusers) -->
|
<!-- Scheduler Type (Diffusers) -->
|
||||||
<div x-show="commonPreferences.backend === 'diffusers'">
|
<div x-show="commonPreferences.backend === 'diffusers'">
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-clock mr-2"></i>Scheduler Type
|
<i class="fas fa-clock mr-2"></i>Scheduler Type
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
x-model="commonPreferences.scheduler_type"
|
x-model="commonPreferences.scheduler_type"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="k_dpmpp_2m (optional)"
|
placeholder="k_dpmpp_2m (optional)"
|
||||||
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="input w-full px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim. Leave empty to use model default.
|
Scheduler type for diffusers backend. Examples: k_dpmpp_2m, euler_a, ddim. Leave empty to use model default.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Enable Parameters (Diffusers) -->
|
<!-- Enable Parameters (Diffusers) -->
|
||||||
<div x-show="commonPreferences.backend === 'diffusers'">
|
<div x-show="commonPreferences.backend === 'diffusers'">
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">
|
||||||
<i class="fas fa-cogs mr-2"></i>Enable Parameters
|
<i class="fas fa-cogs mr-2"></i>Enable Parameters
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
x-model="commonPreferences.enable_parameters"
|
x-model="commonPreferences.enable_parameters"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="negative_prompt,num_inference_steps (comma-separated)"
|
placeholder="negative_prompt,num_inference_steps (comma-separated)"
|
||||||
class="w-full px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="input w-full px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
<p class="mt-1 text-xs text-[var(--color-text-muted)]">
|
||||||
Enabled parameters for diffusers backend (comma-separated). Leave empty to use default (negative_prompt,num_inference_steps).
|
Enabled parameters for diffusers backend (comma-separated). Leave empty to use default (negative_prompt,num_inference_steps).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -453,13 +475,13 @@
|
|||||||
<input
|
<input
|
||||||
x-model="commonPreferences.cuda"
|
x-model="commonPreferences.cuda"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="w-5 h-5 rounded bg-gray-900/90 border-gray-700/70 text-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all cursor-pointer"
|
class="w-5 h-5 rounded bg-[var(--color-bg-primary)] border-[var(--color-border-subtle)] text-[var(--color-success)] focus:ring-2 focus:ring-[var(--color-success)]/50 focus:outline-none transition-all cursor-pointer"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<span class="ml-3 text-sm font-medium text-gray-300">
|
<span class="ml-3 text-sm font-medium text-[var(--color-text-secondary)]">
|
||||||
<i class="fas fa-microchip mr-2"></i>CUDA
|
<i class="fas fa-microchip mr-2"></i>CUDA
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="mt-1 ml-8 text-xs text-gray-400">
|
<p class="mt-1 ml-8 text-xs text-[var(--color-text-muted)]">
|
||||||
Enable CUDA support for GPU acceleration with diffusers backend.
|
Enable CUDA support for GPU acceleration with diffusers backend.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -468,12 +490,12 @@
|
|||||||
<!-- Custom Preferences -->
|
<!-- Custom Preferences -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<label class="block text-sm font-medium text-gray-300">
|
<label class="block text-sm font-medium text-[var(--color-text-secondary)]">
|
||||||
<i class="fas fa-sliders-h mr-2"></i>Custom Preferences
|
<i class="fas fa-sliders-h mr-2"></i>Custom Preferences
|
||||||
</label>
|
</label>
|
||||||
<button @click="addPreference()"
|
<button @click="addPreference()"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
class="text-sm px-3 py-1.5 rounded-lg bg-green-600/20 hover:bg-green-600/30 text-green-300 border border-green-500/30 transition-all">
|
class="text-sm px-3 py-1.5 rounded-lg bg-[var(--color-success-light)] hover:bg-[var(--color-success)]/30 text-[var(--color-success)] border border-[var(--color-success)]/30 transition-all">
|
||||||
<i class="fas fa-plus mr-1"></i>Add Custom
|
<i class="fas fa-plus mr-1"></i>Add Custom
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -485,24 +507,24 @@
|
|||||||
x-model="pref.key"
|
x-model="pref.key"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
class="flex-1 px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="flex-1 input px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<span class="text-gray-400">:</span>
|
<span class="text-[var(--color-text-secondary)]">:</span>
|
||||||
<input
|
<input
|
||||||
x-model="pref.value"
|
x-model="pref.value"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
class="flex-1 px-4 py-2 bg-gray-900/90 border border-gray-700/70 rounded-lg text-gray-200 focus:border-green-500 focus:ring-2 focus:ring-green-500/50 focus:outline-none transition-all"
|
class="flex-1 input px-4 py-2"
|
||||||
:disabled="isSubmitting">
|
:disabled="isSubmitting">
|
||||||
<button @click="removePreference(index)"
|
<button @click="removePreference(index)"
|
||||||
:disabled="isSubmitting"
|
:disabled="isSubmitting"
|
||||||
class="px-3 py-2 rounded-lg bg-red-600/20 hover:bg-red-600/30 text-red-300 border border-red-500/30 transition-all">
|
class="px-3 py-2 rounded-lg bg-[var(--color-error-light)] hover:bg-[var(--color-error)]/30 text-[var(--color-error)] border border-[var(--color-error)]/30 transition-all">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-xs text-gray-400">
|
<p class="mt-2 text-xs text-[var(--color-text-muted)]">
|
||||||
Add custom key-value pairs for advanced configuration
|
Add custom key-value pairs for advanced configuration
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -515,19 +537,19 @@
|
|||||||
x-transition:enter="transition ease-out duration-200"
|
x-transition:enter="transition ease-out duration-200"
|
||||||
x-transition:enter-start="opacity-0"
|
x-transition:enter-start="opacity-0"
|
||||||
x-transition:enter-end="opacity-100"
|
x-transition:enter-end="opacity-100"
|
||||||
class="bg-[#1E293B] border border-[#8B5CF6]/20 rounded-xl overflow-hidden h-[calc(100vh-250px)]">
|
class="bg-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 rounded-xl overflow-hidden h-[calc(100vh-250px)]">
|
||||||
<div class="sticky top-0 bg-[#1E293B] border-b border-[#101827] p-6 flex items-center justify-between z-10">
|
<div class="sticky top-0 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border-subtle)] p-6 flex items-center justify-between z-10">
|
||||||
<h2 class="text-xl font-semibold text-[#E5E7EB] flex items-center gap-3">
|
<h2 class="text-xl font-semibold text-[var(--color-text-primary)] flex items-center gap-3">
|
||||||
<div class="w-8 h-8 rounded-lg bg-fuchsia-500/10 flex items-center justify-center">
|
<div class="w-8 h-8 rounded-lg bg-fuchsia-500/10 flex items-center justify-center">
|
||||||
<i class="fas fa-code text-fuchsia-400"></i>
|
<i class="fas fa-code text-fuchsia-400"></i>
|
||||||
</div>
|
</div>
|
||||||
YAML Configuration Editor
|
YAML Configuration Editor
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button id="formatYamlBtn" class="text-[#94A3B8] hover:text-[#E5E7EB] text-sm px-3 py-1.5 rounded-lg hover:bg-[#101827] transition-colors">
|
<button id="formatYamlBtn" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] text-sm px-3 py-1.5 rounded-lg hover:bg-[var(--color-bg-primary)] transition-colors">
|
||||||
<i class="fas fa-indent mr-1.5"></i> Format
|
<i class="fas fa-indent mr-1.5"></i> Format
|
||||||
</button>
|
</button>
|
||||||
<button id="copyYamlBtn" class="text-[#94A3B8] hover:text-[#E5E7EB] text-sm px-3 py-1.5 rounded-lg hover:bg-[#101827] transition-colors">
|
<button id="copyYamlBtn" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] text-sm px-3 py-1.5 rounded-lg hover:bg-[var(--color-bg-primary)] transition-colors">
|
||||||
<i class="fas fa-copy mr-1.5"></i> Copy
|
<i class="fas fa-copy mr-1.5"></i> Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -537,8 +559,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "views/partials/footer" .}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Include JS-YAML library -->
|
<!-- Include JS-YAML library -->
|
||||||
@@ -553,8 +573,8 @@
|
|||||||
<style>
|
<style>
|
||||||
/* Enhanced CodeMirror styling */
|
/* Enhanced CodeMirror styling */
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
background: linear-gradient(135deg, #111827 0%, #1f2937 100%) !important;
|
background: var(--color-bg-primary) !important;
|
||||||
color: #e5e7eb !important;
|
color: var(--color-text-primary) !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important;
|
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace !important;
|
||||||
@@ -564,7 +584,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-cursor {
|
.CodeMirror-cursor {
|
||||||
border-left: 2px solid #a78bfa !important;
|
border-left: 2px solid var(--color-accent) !important;
|
||||||
animation: blink 1s infinite;
|
animation: blink 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,20 +594,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-gutters {
|
.CodeMirror-gutters {
|
||||||
background: linear-gradient(135deg, #1f2937 0%, #374151 100%) !important;
|
background: var(--color-bg-secondary) !important;
|
||||||
border-right: 1px solid rgba(75, 85, 99, 0.5) !important;
|
border-right: 1px solid var(--color-border-subtle) !important;
|
||||||
color: #9ca3af !important;
|
color: var(--color-text-secondary) !important;
|
||||||
padding-right: 8px !important;
|
padding-right: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-linenumber {
|
.CodeMirror-linenumber {
|
||||||
color: #6b7280 !important;
|
color: var(--color-text-muted) !important;
|
||||||
padding: 0 8px 0 4px !important;
|
padding: 0 8px 0 4px !important;
|
||||||
font-size: 12px !important;
|
font-size: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-activeline-background {
|
.CodeMirror-activeline-background {
|
||||||
background: rgba(139, 92, 246, 0.1) !important;
|
background: var(--color-accent-light) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-selected {
|
.CodeMirror-selected {
|
||||||
@@ -614,27 +634,27 @@
|
|||||||
.cm-keyword { color: #8b5cf6 !important; font-weight: 600 !important; }
|
.cm-keyword { color: #8b5cf6 !important; font-weight: 600 !important; }
|
||||||
.cm-string { color: #10b981 !important; }
|
.cm-string { color: #10b981 !important; }
|
||||||
.cm-number { color: #f59e0b !important; }
|
.cm-number { color: #f59e0b !important; }
|
||||||
.cm-comment { color: #6b7280 !important; font-style: italic !important; }
|
.cm-comment { color: var(--color-text-muted) !important; font-style: italic !important; }
|
||||||
.cm-property { color: #ec4899 !important; }
|
.cm-property { color: #ec4899 !important; }
|
||||||
.cm-operator { color: #ef4444 !important; }
|
.cm-operator { color: #ef4444 !important; }
|
||||||
.cm-variable { color: #06b6d4 !important; }
|
.cm-variable { color: #06b6d4 !important; }
|
||||||
.cm-tag { color: #8b5cf6 !important; font-weight: 600 !important; }
|
.cm-tag { color: #8b5cf6 !important; font-weight: 600 !important; }
|
||||||
.cm-attribute { color: #f59e0b !important; }
|
.cm-attribute { color: #f59e0b !important; }
|
||||||
.cm-def { color: #ec4899 !important; font-weight: 600 !important; }
|
.cm-def { color: #ec4899 !important; font-weight: 600 !important; }
|
||||||
.cm-bracket { color: #d1d5db !important; }
|
.cm-bracket { color: var(--color-text-secondary) !important; }
|
||||||
.cm-punctuation { color: #d1d5db !important; }
|
.cm-punctuation { color: var(--color-text-secondary) !important; }
|
||||||
.cm-quote { color: #10b981 !important; }
|
.cm-quote { color: #10b981 !important; }
|
||||||
.cm-meta { color: #6b7280 !important; }
|
.cm-meta { color: var(--color-text-muted) !important; }
|
||||||
.cm-builtin { color: #f472b6 !important; }
|
.cm-builtin { color: #f472b6 !important; }
|
||||||
.cm-atom { color: #f59e0b !important; }
|
.cm-atom { color: #f59e0b !important; }
|
||||||
|
|
||||||
/* Enhanced scrollbar styling */
|
/* Enhanced scrollbar styling */
|
||||||
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||||
background: #1f2937 !important;
|
background: var(--color-bg-secondary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
|
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
|
||||||
background: #1f2937 !important;
|
background: var(--color-bg-secondary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar {
|
.CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar {
|
||||||
@@ -643,17 +663,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track {
|
.CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track {
|
||||||
background: #1f2937;
|
background: var(--color-bg-secondary);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
|
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb {
|
||||||
background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%);
|
background: var(--color-text-muted);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover {
|
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb:hover, .CodeMirror-hscrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
background: linear-gradient(135deg, #9ca3af 0%, #d1d5db 100%);
|
background: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus ring styling */
|
/* Focus ring styling */
|
||||||
@@ -682,27 +702,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-success {
|
.alert-success {
|
||||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
|
background: var(--color-success-light);
|
||||||
border-color: rgba(16, 185, 129, 0.3);
|
border-color: var(--color-success);
|
||||||
color: #10b981;
|
color: var(--color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-error {
|
.alert-error {
|
||||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%);
|
background: var(--color-error-light);
|
||||||
border-color: rgba(239, 68, 68, 0.3);
|
border-color: var(--color-error);
|
||||||
color: #ef4444;
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-warning {
|
.alert-warning {
|
||||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%);
|
background: var(--color-warning-light);
|
||||||
border-color: rgba(245, 158, 11, 0.3);
|
border-color: var(--color-warning);
|
||||||
color: #f59e0b;
|
color: var(--color-warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-info {
|
.alert-info {
|
||||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%);
|
background: var(--color-info-light);
|
||||||
border-color: rgba(59, 130, 246, 0.3);
|
border-color: var(--color-info);
|
||||||
color: #3b82f6;
|
color: var(--color-info);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -731,6 +751,7 @@ function importModel() {
|
|||||||
jobPollInterval: null,
|
jobPollInterval: null,
|
||||||
yamlEditor: null,
|
yamlEditor: null,
|
||||||
modelEditor: null,
|
modelEditor: null,
|
||||||
|
lastEstimate: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// If in edit mode, always show advanced mode
|
// If in edit mode, always show advanced mode
|
||||||
@@ -854,15 +875,36 @@ function importModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
|
const hasSize = result.estimated_size_display && result.estimated_size_display !== '0 B';
|
||||||
|
const hasVram = result.estimated_vram_display && result.estimated_vram_display !== '0 B';
|
||||||
|
if (hasSize || hasVram) {
|
||||||
|
this.lastEstimate = {
|
||||||
|
sizeDisplay: result.estimated_size_display || '',
|
||||||
|
vramDisplay: result.estimated_vram_display || '',
|
||||||
|
sizeBytes: result.estimated_size_bytes || 0,
|
||||||
|
vramBytes: result.estimated_vram_bytes || 0
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.lastEstimate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let successMsg = 'Import started! Tracking progress...';
|
||||||
|
if (hasSize || hasVram) {
|
||||||
|
const parts = [];
|
||||||
|
if (hasSize) parts.push('Size: ' + result.estimated_size_display);
|
||||||
|
if (hasVram) parts.push('VRAM: ' + result.estimated_vram_display);
|
||||||
|
successMsg += ' (' + parts.join(' · ') + ')';
|
||||||
|
}
|
||||||
|
|
||||||
if (result.uuid) {
|
if (result.uuid) {
|
||||||
this.currentJobId = result.uuid;
|
this.currentJobId = result.uuid;
|
||||||
this.showAlert('success', 'Import started! Tracking progress...');
|
this.showAlert('success', successMsg);
|
||||||
this.startJobPolling();
|
this.startJobPolling();
|
||||||
} else if (result.ID) {
|
} else if (result.ID) {
|
||||||
// Fallback for different response format
|
// Fallback for different response format
|
||||||
this.currentJobId = result.ID;
|
this.currentJobId = result.ID;
|
||||||
this.showAlert('success', 'Import started! Tracking progress...');
|
this.showAlert('success', successMsg);
|
||||||
this.startJobPolling();
|
this.startJobPolling();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No job ID returned from server');
|
throw new Error('No job ID returned from server');
|
||||||
@@ -1180,5 +1222,10 @@ parameters:
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{{template "views/partials/footer" .}}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user