Compare commits

..

1 Commits

Author SHA1 Message Date
Ettore Di Giacinto
1f0110368d step-flash fixes
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-12 23:36:16 +01:00
82 changed files with 1770 additions and 3861 deletions

View File

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

View File

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

View File

@@ -237,7 +237,7 @@ Roadmap items: [List of issues](https://github.com/mudler/LocalAI/issues?q=is%3A
- 🧩 [Backend Gallery](https://localai.io/backends/): Install/remove backends on the fly, powered by OCI images — fully customizable and API-driven. - 🧩 [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 to Text](https://localai.io/features/audio-to-text/) (Audio transcription with `whisper.cpp`)
- 🎨 [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)

View File

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

View File

@@ -294,6 +294,76 @@ json parse_options(bool streaming, const backend::PredictOptions* predict, const
return data; return data;
} }
static bool template_uses_arguments_items_filter(const std::string & template_src) {
return template_src.find("arguments|items") != std::string::npos ||
template_src.find("arguments | items") != std::string::npos ||
template_src.find("arguments| items") != std::string::npos ||
template_src.find("arguments |items") != std::string::npos;
}
static void normalize_tool_call_arguments_for_template(
json & messages,
const std::string & template_src,
const char * request_name)
{
if (!messages.is_array() || !template_uses_arguments_items_filter(template_src)) {
return;
}
size_t converted = 0;
size_t failed = 0;
for (auto & message : messages) {
if (!message.is_object() || !message.contains("tool_calls") || !message["tool_calls"].is_array()) {
continue;
}
for (auto & tool_call : message["tool_calls"]) {
if (!tool_call.is_object() || !tool_call.contains("function") || !tool_call["function"].is_object()) {
continue;
}
auto & function = tool_call["function"];
if (!function.contains("arguments")) {
continue;
}
auto & arguments = function["arguments"];
if (!arguments.is_string()) {
continue;
}
const std::string args_str = arguments.get<std::string>();
if (args_str.empty()) {
arguments = json::object();
converted++;
continue;
}
try {
json parsed_args = json::parse(args_str);
if (parsed_args.is_object()) {
arguments = parsed_args;
converted++;
}
} catch (const json::parse_error &) {
failed++;
}
}
}
if (converted > 0) {
SRV_INF("[TOOLS DEBUG] %s: Converted %zu tool call argument strings to JSON objects for arguments|items template compatibility\n",
request_name,
converted);
}
if (failed > 0) {
SRV_WRN("[TOOLS DEBUG] %s: Failed to parse %zu tool call argument strings as JSON for arguments|items template compatibility\n",
request_name,
failed);
}
}
const std::vector<ggml_type> kv_cache_types = { const std::vector<ggml_type> kv_cache_types = {
GGML_TYPE_F32, GGML_TYPE_F32,
@@ -417,12 +487,6 @@ 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);
@@ -1261,40 +1325,9 @@ 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 if (body_json.contains("messages") && ctx_server.impl->chat_params.tmpls) {
// and doesn't overwrite them with defaults in the returned parsed_data const auto template_src = common_chat_templates_source(ctx_server.impl->chat_params.tmpls.get());
if (data.contains("n_predict")) { normalize_tool_call_arguments_for_template(body_json["messages"], template_src, "PredictStream");
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.)
@@ -2028,40 +2061,9 @@ 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 if (body_json.contains("messages") && ctx_server.impl->chat_params.tmpls) {
// and doesn't overwrite them with defaults in the returned parsed_data const auto template_src = common_chat_templates_source(ctx_server.impl->chat_params.tmpls.get());
if (data.contains("n_predict")) { normalize_tool_call_arguments_for_template(body_json["messages"], template_src, "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.)

View File

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

View File

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

View File

@@ -40,21 +40,7 @@ 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
# Try to import sd_embed - it might not always be available 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:
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
@@ -63,9 +49,6 @@ 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"
@@ -196,7 +179,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, device_map=None): def _load_pipeline(self, request, modelFile, fromSingleFile, torchType, variant):
""" """
Load a diffusers pipeline dynamically using the dynamic loader. Load a diffusers pipeline dynamically using the dynamic loader.
@@ -210,7 +193,6 @@ 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
@@ -232,14 +214,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, device_map=device_map) transformer = FluxTransformer2DModel.from_single_file(modelFile, torch_dtype=dtype)
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, device_map=device_map) text_encoder_2 = T5EncoderModel.from_pretrained(bfl_repo, subfolder="text_encoder_2", torch_dtype=dtype)
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, device_map=device_map) pipe = FluxPipeline.from_pretrained(bfl_repo, transformer=None, text_encoder_2=None, torch_dtype=dtype)
pipe.transformer = transformer pipe.transformer = transformer
pipe.text_encoder_2 = text_encoder_2 pipe.text_encoder_2 = text_encoder_2
@@ -252,15 +234,13 @@ 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
@@ -270,15 +250,13 @@ 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
@@ -289,8 +267,7 @@ 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)
@@ -302,8 +279,7 @@ 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
@@ -314,8 +290,7 @@ 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()
@@ -339,7 +314,6 @@ 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,
) )
@@ -349,7 +323,6 @@ 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
@@ -358,7 +331,6 @@ 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
@@ -366,8 +338,7 @@ 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:
@@ -392,7 +363,6 @@ 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,
) )
@@ -402,7 +372,6 @@ 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
@@ -411,7 +380,6 @@ 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
@@ -419,8 +387,7 @@ 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:
@@ -443,10 +410,6 @@ 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"
@@ -549,13 +512,6 @@ 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(
@@ -563,8 +519,7 @@ 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)
@@ -589,7 +544,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, device_map=device_map request.ControlNet, torch_dtype=torchType, variant=variant
) )
self.pipe.controlnet = self.controlnet self.pipe.controlnet = self.controlnet
else: else:
@@ -628,9 +583,7 @@ 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)
# Only move pipeline to device if NOT using device_map if device != "cpu":
# 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)
@@ -790,7 +743,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 and SD_EMBED_AVAILABLE: elif SD_EMBED:
if self.PipelineType == "StableDiffusionPipeline": if self.PipelineType == "StableDiffusionPipeline":
( (
kwargs["prompt_embeds"], kwargs["prompt_embeds"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
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"
) )
@@ -35,15 +37,11 @@ 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),
) )
// Create new stop channel BEFORE setting up any goroutines
// This prevents race conditions where the old shutdown handler might
// receive the closed channel and try to shut down the new watchdog
a.watchdogStop = make(chan bool, 1)
// Set the watchdog on the model loader
a.modelLoader.SetWatchDog(wd) a.modelLoader.SetWatchDog(wd)
// Create new stop channel
a.watchdogStop = make(chan bool, 1)
// 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
@@ -51,19 +49,15 @@ func (a *Application) startWatchdog() error {
go wd.Run() go wd.Run()
} }
// Setup shutdown handler - this goroutine will wait on a.watchdogStop // Setup shutdown handler
// 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")
wdForShutdown.Shutdown() wd.Shutdown()
case <-appConfig.Context.Done(): case <-appConfig.Context.Done():
xlog.Debug("Context canceled, shutting down watchdog") xlog.Debug("Context canceled, shutting down watchdog")
wdForShutdown.Shutdown() wd.Shutdown()
} }
}() }()
@@ -88,41 +82,20 @@ func (a *Application) RestartWatchdog() error {
a.watchdogMutex.Lock() a.watchdogMutex.Lock()
defer a.watchdogMutex.Unlock() defer a.watchdogMutex.Unlock()
// Get the old watchdog before we shut it down // Shutdown existing watchdog if running
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 - this triggers the stop signal // Shutdown existing watchdog if running
if oldWD != nil { currentWD := a.modelLoader.GetWatchDog()
oldWD.Shutdown() if currentWD != nil {
// Wait for the old watchdog's Run() goroutine to fully shut down currentWD.Shutdown()
oldWD.WaitDone() // Wait a bit for shutdown to complete
time.Sleep(100 * time.Millisecond)
} }
// Start watchdog with new settings // Start watchdog with new settings
if err := a.startWatchdog(); err != nil { return a.startWatchdog()
return err
}
// Restore the model state from the old watchdog to the new one
// This ensures the new watchdog knows about already-loaded models
newWD := a.modelLoader.GetWatchDog()
if newWD != nil && len(oldState.AddressModelMap) > 0 {
newWD.RestoreState(oldState)
}
return nil
} }

View File

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

View File

@@ -1,10 +1,7 @@
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"
@@ -39,46 +36,6 @@ func ImageGeneration(height, width, step, seed int, positive_prompt, negative_pr
return err return err
} }
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
traceData := map[string]any{
"positive_prompt": positive_prompt,
"negative_prompt": negative_prompt,
"height": height,
"width": width,
"step": step,
"seed": seed,
"source_image": src,
"destination": dst,
}
startTime := time.Now()
originalFn := fn
fn = func() error {
err := originalFn()
duration := time.Since(startTime)
errStr := ""
if err != nil {
errStr = err.Error()
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: duration,
Type: trace.BackendTraceImageGeneration,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(positive_prompt, 200),
Error: errStr,
Data: traceData,
})
return err
}
}
return fn, nil return fn, nil
} }

View File

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

View File

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

View File

@@ -5,10 +5,8 @@ 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"
@@ -94,51 +92,7 @@ func SoundGeneration(
req.Instrumental = instrumental req.Instrumental = instrumental
} }
var startTime time.Time
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
startTime = time.Now()
}
res, err := soundGenModel.SoundGeneration(context.Background(), req) res, err := soundGenModel.SoundGeneration(context.Background(), req)
if appConfig.EnableTracing {
errStr := ""
if err != nil {
errStr = err.Error()
} else if res != nil && !res.Success {
errStr = fmt.Sprintf("sound generation error: %s", res.Message)
}
summary := trace.TruncateString(text, 200)
if summary == "" && caption != "" {
summary = trace.TruncateString(caption, 200)
}
traceData := map[string]any{
"text": text,
"caption": caption,
"lyrics": lyrics,
}
if duration != nil {
traceData["duration"] = *duration
}
if temperature != nil {
traceData["temperature"] = *temperature
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceSoundGeneration,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: summary,
Error: errStr,
Data: traceData,
})
}
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }

View File

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

View File

@@ -6,7 +6,6 @@ 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"
@@ -29,12 +28,6 @@ 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,
@@ -44,24 +37,6 @@ 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{
@@ -82,26 +57,5 @@ func ModelTranscription(audio, language string, translate, diarize bool, prompt
Speaker: s.Speaker, Speaker: s.Speaker,
}) })
} }
if appConfig.EnableTracing {
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceTranscription,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(audio+" -> "+tr.Text, 200),
Data: map[string]any{
"audio_file": audio,
"language": language,
"translate": translate,
"diarize": diarize,
"prompt": prompt,
"result_text": tr.Text,
"segments_count": len(tr.Segments),
},
})
}
return tr, err return tr, err
} }

View File

@@ -8,10 +8,8 @@ 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"
@@ -62,12 +60,6 @@ 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,
@@ -75,31 +67,6 @@ 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
} }
@@ -148,12 +115,6 @@ 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
@@ -210,34 +171,6 @@ func ModelTTSStream(
} }
}) })
resultErr := err
if callbackErr != nil {
resultErr = callbackErr
}
if appConfig.EnableTracing {
errStr := ""
if resultErr != nil {
errStr = resultErr.Error()
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: time.Since(startTime),
Type: trace.BackendTraceTTS,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(text, 200),
Error: errStr,
Data: map[string]any{
"text": text,
"voice": voice,
"language": language,
"streaming": true,
},
})
}
if callbackErr != nil { if callbackErr != nil {
return callbackErr return callbackErr
} }

View File

@@ -1,10 +1,7 @@
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"
@@ -40,46 +37,5 @@ func VideoGeneration(height, width int32, prompt, negativePrompt, startImage, en
return err return err
} }
if appConfig.EnableTracing {
trace.InitBackendTracingIfEnabled(appConfig.TracingMaxItems)
traceData := map[string]any{
"prompt": prompt,
"negative_prompt": negativePrompt,
"height": height,
"width": width,
"num_frames": numFrames,
"fps": fps,
"seed": seed,
"cfg_scale": cfgScale,
"step": step,
}
startTime := time.Now()
originalFn := fn
fn = func() error {
err := originalFn()
duration := time.Since(startTime)
errStr := ""
if err != nil {
errStr = err.Error()
}
trace.RecordBackendTrace(trace.BackendTrace{
Timestamp: startTime,
Duration: duration,
Type: trace.BackendTraceVideoGeneration,
ModelName: modelConfig.Name,
Backend: modelConfig.Backend,
Summary: trace.TruncateString(prompt, 200),
Error: errStr,
Data: traceData,
})
return err
}
}
return fn, nil return fn, nil
} }

View File

@@ -83,7 +83,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
} }

View File

@@ -99,10 +99,6 @@ 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) {
@@ -708,7 +704,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.WithForceReasoning()) cogitoOpts = append(cogitoOpts, cogito.EnableToolReasoner)
} }
if c.Agent.EnablePlanning { if c.Agent.EnablePlanning {
@@ -731,21 +727,5 @@ func (c *ModelConfig) BuildCogitoOptions() []cogito.Option {
cogitoOpts = append(cogitoOpts, cogito.WithMaxAttempts(c.Agent.MaxAttempts)) cogitoOpts = append(cogitoOpts, cogito.WithMaxAttempts(c.Agent.MaxAttempts))
} }
if c.Agent.DisableSinkState {
cogitoOpts = append(cogitoOpts, cogito.DisableSinkState)
}
if c.Agent.LoopDetection != 0 {
cogitoOpts = append(cogitoOpts, cogito.WithLoopDetection(c.Agent.LoopDetection))
}
if c.Agent.MaxAdjustmentAttempts != 0 {
cogitoOpts = append(cogitoOpts, cogito.WithMaxAdjustmentAttempts(c.Agent.MaxAdjustmentAttempts))
}
if c.Agent.ForceReasoningTool {
cogitoOpts = append(cogitoOpts, cogito.WithForceReasoningTool())
}
return cogitoOpts return cogitoOpts
} }

View File

@@ -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.v3" "gopkg.in/yaml.v2"
) )
const ( const (

View File

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

View File

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

View File

@@ -55,22 +55,20 @@ 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)

View File

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

View File

@@ -27,19 +27,14 @@ 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
@@ -78,7 +73,6 @@ 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) {
@@ -100,13 +94,12 @@ 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,
@@ -174,9 +167,6 @@ 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")
@@ -380,17 +370,8 @@ func registerRealtime(application *application.Application, model string) func(c
continue continue
} }
// Check buffer size limits before appending
session.AudioBufferLock.Lock()
newSize := len(session.InputAudioBuffer) + len(decodedAudio)
if newSize > maxAudioBufferSize {
session.AudioBufferLock.Unlock()
xlog.Error("audio buffer size limit exceeded", "current_size", len(session.InputAudioBuffer), "incoming_size", len(decodedAudio), "limit", maxAudioBufferSize)
sendError(c, "buffer_size_exceeded", fmt.Sprintf("Audio buffer size limit exceeded (max %d bytes)", maxAudioBufferSize), "", "")
continue
}
// Append to InputAudioBuffer // Append to InputAudioBuffer
session.AudioBufferLock.Lock()
session.InputAudioBuffer = append(session.InputAudioBuffer, decodedAudio...) session.InputAudioBuffer = append(session.InputAudioBuffer, decodedAudio...)
session.AudioBufferLock.Unlock() session.AudioBufferLock.Unlock()
@@ -696,10 +677,6 @@ 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
} }
@@ -755,18 +732,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")
// NOTE: OpenAI doesn't send this message unless the client requests it sendEvent(c, types.InputAudioBufferClearedEvent{
// xlog.Debug("Detected silence for a while, clearing audio buffer") ServerEventBase: types.ServerEventBase{
// sendEvent(c, types.InputAudioBufferClearedEvent{ EventID: "event_TODO",
// ServerEventBase: types.ServerEventBase{ },
// EventID: "event_TODO", })
// },
// })
continue continue
} else if len(segments) == 0 { } else if len(segments) == 0 {
@@ -936,7 +913,6 @@ 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 {
@@ -948,29 +924,8 @@ 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),
@@ -994,12 +949,7 @@ 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:
img, err := utils.GetContentURIAsBase64(content.ImageURL) msg.StringImages = append(msg.StringImages, content.ImageURL)
if err != nil {
xlog.Warn("Failed to process image", "error", err)
continue
}
msg.StringImages = append(msg.StringImages, img)
imgIndex++ imgIndex++
nrOfImgsInMessage++ nrOfImgsInMessage++
} }
@@ -1078,34 +1028,13 @@ 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
} }
// Detect thinking start token from template for reasoning extraction reasoningText, responseWithoutReasoning := reasoning.ExtractReasoningWithConfig(rawResponse, "", config.ReasoningConfig)
var template string
if config.TemplateConfig.UseTokenizerTemplate {
template = config.GetModelTemplate()
} else {
template = config.TemplateConfig.Chat
}
thinkingStartToken := reasoning.DetectThinkingStartToken(template, &config.ReasoningConfig)
reasoningText, responseWithoutReasoning := reasoning.ExtractReasoningWithConfig(rawResponse, thinkingStartToken, config.ReasoningConfig)
xlog.Debug("LLM Response", "reasoning", reasoningText, "response_without_reasoning", responseWithoutReasoning) xlog.Debug("LLM Response", "reasoning", reasoningText, "response_without_reasoning", responseWithoutReasoning)
textContent := functions.ParseTextContent(responseWithoutReasoning, config.FunctionsConfig) textContent := functions.ParseTextContent(responseWithoutReasoning, config.FunctionsConfig)

View File

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

View File

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

View File

@@ -279,18 +279,6 @@ 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)
@@ -335,59 +323,12 @@ func convertORInputToMessages(input interface{}, cfg *config.ModelConfig) ([]sch
messages = append(messages, msg) messages = append(messages, msg)
} }
} }
return mergeContiguousAssistantMessages(messages), nil return 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) {
@@ -425,52 +366,19 @@ 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)
} }
} }
func extractReasoningContentFromORItem(item *schema.ORItemField) string { // convertOROutputItemsToMessages converts Open Responses output items to internal Messages
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 {
@@ -484,23 +392,9 @@ 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":
msg := schema.Message{ // Function calls are handled separately - they become tool calls in the next turn
Role: "assistant", // For now, we skip them as they're part of the model's output, not input
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
@@ -520,74 +414,7 @@ func convertOROutputItemsToMessages(outputItems []schema.ORItemField) ([]schema.
} }
} }
return mergeContiguousAssistantMessages(messages), nil return 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
@@ -1100,7 +927,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(cogito.MessageRole(message.Role), message.StringContent) fragment = fragment.AddMessage(message.Role, message.StringContent)
} }
fragmentPtr := &fragment fragmentPtr := &fragment
@@ -1177,6 +1004,12 @@ 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)
@@ -1353,6 +1186,21 @@ 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())
@@ -2657,7 +2505,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(cogito.MessageRole(message.Role), message.StringContent) fragment = fragment.AddMessage(message.Role, message.StringContent)
} }
fragmentPtr := &fragment fragmentPtr := &fragment
@@ -2732,6 +2580,12 @@ 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)
@@ -2876,6 +2730,17 @@ func handleMCPStream(c echo.Context, responseID string, createdAt int64, input *
return return
} }
// Get final response
f, err = defaultLLM.Ask(ctx, f)
if err != nil {
events <- map[string]interface{}{
"type": "error",
"message": fmt.Sprintf("Failed to get response: %v", err),
}
ended <- err
return
}
// Stream final assistant message // Stream final assistant message
content := f.LastMessage().Content content := f.LastMessage().Content
messageID := fmt.Sprintf("msg_%s", uuid.New().String()) messageID := fmt.Sprintf("msg_%s", uuid.New().String())

View File

@@ -31,10 +31,9 @@ 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,
}) })
}) })

View File

@@ -7,7 +7,6 @@ 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"
) )
@@ -431,13 +430,4 @@ func RegisterUIRoutes(app *echo.Echo,
return c.NoContent(204) return c.NoContent(204)
}) })
app.GET("/api/backend-traces", func(c echo.Context) error {
return c.JSON(200, trace.GetBackendTraces())
})
app.POST("/api/backend-traces/clear", func(c echo.Context) error {
trace.ClearBackendTraces()
return c.NoContent(204)
})
} }

View File

@@ -418,337 +418,6 @@ 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
============================================ */ ============================================ */
@@ -770,36 +439,3 @@ td {
} }
} }
/* Tablet and mobile - sidebar becomes overlay */
@media (max-width: 1023px) {
.mobile-menu-btn {
display: flex;
align-items: center;
justify-content: center;
}
.sidebar {
transform: translateX(-100%);
z-index: 45;
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-close-btn {
display: flex;
align-items: center;
justify-content: center;
}
.sidebar-header {
padding-right: 3rem;
}
.sidebar-overlay.open + .sidebar,
.sidebar.open {
transform: translateX(0);
}
}

View File

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

View File

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

View File

@@ -2,50 +2,50 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[#101827] text-[#E5E7EB]">
<div class="app-layout"> <div class="flex flex-col min-h-screen">
{{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">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8 mb-10"> <!-- Error Section -->
<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-[var(--color-primary)]"> <div class="mb-6 text-6xl text-[#38BDF8]">
<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-[var(--color-text-secondary)] mb-6">The page you're looking for doesn't exist or has been moved</p> <p class="text-xl text-[#94A3B8] mb-6">The page you're looking for doesn't exist or has been moved</p>
<div class="flex flex-wrap justify-center gap-2"> <div class="flex flex-wrap justify-center gap-4">
<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"> <a href="./"
<i class="fas fa-home"></i> 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 mr-2"></i>
<span>Return Home</span> <span>Return Home</span>
</a> </a>
<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"> <a href="browse/"
<i class="fas fa-images"></i> 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 mr-2"></i>
<span>Browse Gallery</span> <span>Browse Gallery</span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8"> <!-- Additional Information -->
<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-[var(--color-warning-light)] border border-[var(--color-warning)]/20 mb-4"> <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-500/10 border border-yellow-500/20 mb-4">
<i class="text-[var(--color-warning)] text-2xl fa-solid fa-triangle-exclamation"></i> <i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i>
</div> </div>
<h2 class="text-2xl md:text-3xl font-semibold text-[var(--color-text-primary)] mb-4">Looking for resources?</h2> <h2 class="text-2xl md:text-3xl font-semibold text-[#E5E7EB] mb-4">Looking for resources?</h2>
<p class="text-lg text-[var(--color-text-secondary)] mb-6">Visit our <a class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors" href="browse">Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors">Getting started documentation</a></p> <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>
</div> </div>
</div> </div>
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>

View File

@@ -2,12 +2,10 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[#101827] text-[#E5E7EB]">
<div class="app-layout"> <div class="flex flex-col min-h-screen" x-data="jobDetails()" x-init="init()">
{{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 -->
@@ -19,7 +17,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-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"> <a href="/agent-jobs" class="text-[#94A3B8] hover:text-[#E5E7EB]">
<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>
@@ -28,7 +26,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-[var(--color-text-primary)]">Job Status</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB]">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',
@@ -39,75 +37,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 type="button" x-show="job.status === 'pending' || job.status === 'running'" <button x-show="job.status === 'pending' || job.status === 'running'"
@click="cancelJob()" @click="cancelJob()"
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"> class="btn-primary"
<i class="fas fa-stop"></i> style="background: var(--color-error);">
<span>Cancel</span> <i class="fas fa-stop mr-2"></i>Cancel
</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-[var(--color-text-secondary)] text-sm">Job ID</label> <label class="text-[#94A3B8] text-sm">Job ID</label>
<div class="font-mono text-[var(--color-text-primary)] mt-1" x-text="job.id || '-'"></div> <div class="font-mono text-[#E5E7EB] mt-1" x-text="job.id || '-'"></div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Task</label> <label class="text-[#94A3B8] text-sm">Task</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="task ? task.name : (job.task_id || '-')"></div> <div class="text-[#E5E7EB] mt-1" x-text="task ? task.name : (job.task_id || '-')"></div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Created</label> <label class="text-[#94A3B8] text-sm">Created</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.created_at)"></div> <div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.created_at)"></div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Started</label> <label class="text-[#94A3B8] text-sm">Started</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.started_at)"></div> <div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.started_at)"></div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Completed</label> <label class="text-[#94A3B8] text-sm">Completed</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="formatDate(job.completed_at)"></div> <div class="text-[#E5E7EB] mt-1" x-text="formatDate(job.completed_at)"></div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Triggered By</label> <label class="text-[#94A3B8] text-sm">Triggered By</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="job.triggered_by || '-'"></div> <div class="text-[#E5E7EB] 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-[var(--color-text-primary)] mb-6">Agent Prompt Template</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Agent Prompt Template</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">The original prompt template from the task definition.</p> <p class="text-sm text-[#94A3B8] mb-4">The original prompt template from the task definition.</p>
<div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap font-mono text-sm" x-text="task.prompt"></div> <div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap font-mono text-sm" x-text="task.prompt"></div>
</div> </div>
<!-- Cron Parameters --> <!-- Cron Parameters -->
<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"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.triggered_by === 'cron' && task && task.cron_parameters && Object.keys(task.cron_parameters).length > 0">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Cron Parameters</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Parameters</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">Parameters configured for cron-triggered executions of this task.</p> <p class="text-sm text-[#94A3B8] mb-4">Parameters configured for cron-triggered executions of this task.</p>
<pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm overflow-x-auto" x-text="JSON.stringify(task.cron_parameters, null, 2)"></pre> <pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] text-sm overflow-x-auto" x-text="JSON.stringify(task.cron_parameters, null, 2)"></pre>
</div> </div>
<!-- Parameters --> <!-- Parameters -->
<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"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.parameters && Object.keys(job.parameters).length > 0">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Job Parameters</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Job Parameters</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">Parameters used for this specific job execution.</p> <p class="text-sm text-[#94A3B8] mb-4">Parameters used for this specific job execution.</p>
<pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm overflow-x-auto" x-text="JSON.stringify(job.parameters, null, 2)"></pre> <pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] 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-[var(--color-text-primary)] mb-6">Rendered Job Prompt</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Rendered Job Prompt</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4">The prompt with parameters substituted, as it was sent to the agent.</p> <p class="text-sm text-[#94A3B8] mb-4">The prompt with parameters substituted, as it was sent to the agent.</p>
<div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap" x-text="getRenderedPrompt()"></div> <div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap" x-text="getRenderedPrompt()"></div>
</div> </div>
<!-- Result --> <!-- Result -->
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="job.result"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.result">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Result</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Result</h2>
<div class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] whitespace-pre-wrap" x-text="job.result"></div> <div class="bg-[#101827] p-4 rounded text-[#E5E7EB] whitespace-pre-wrap" x-text="job.result"></div>
</div> </div>
<!-- Error --> <!-- Error -->
@@ -117,18 +115,18 @@
</div> </div>
<!-- Reasoning Traces & Actions --> <!-- Reasoning Traces & Actions -->
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Execution Traces</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Execution Traces</h2>
<div x-show="!traces || traces.length === 0" class="text-[var(--color-text-secondary)] text-center py-8"> <div x-show="!traces || traces.length === 0" class="text-[#94A3B8] 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-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4"> <div class="bg-[#101827] border border-[#38BDF8]/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-[var(--color-text-secondary)] font-mono" x-text="'Step ' + (index + 1)"></span> <span class="text-xs text-[#94A3B8] 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',
@@ -138,14 +136,14 @@
}" }"
x-text="trace.type"></span> x-text="trace.type"></span>
</div> </div>
<span class="text-xs text-[var(--color-text-secondary)]" x-text="formatTime(trace.timestamp)"></span> <span class="text-xs text-[#94A3B8]" x-text="formatTime(trace.timestamp)"></span>
</div> </div>
<div class="text-[var(--color-text-primary)] text-sm" x-text="trace.content"></div> <div class="text-[#E5E7EB] text-sm" x-text="trace.content"></div>
<div x-show="trace.tool_name" class="mt-2 text-xs text-[var(--color-text-secondary)]"> <div x-show="trace.tool_name" class="mt-2 text-xs text-[#94A3B8]">
<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-[var(--color-text-secondary)] bg-[var(--color-bg-tertiary)] p-2 rounded overflow-x-auto" x-text="JSON.stringify(trace.arguments, null, 2)"></pre> <pre class="text-xs text-[#94A3B8] bg-[#0A0E1A] p-2 rounded overflow-x-auto" x-text="JSON.stringify(trace.arguments, null, 2)"></pre>
</div> </div>
</div> </div>
</template> </template>
@@ -153,16 +151,16 @@
</div> </div>
<!-- Webhook Status --> <!-- Webhook Status -->
<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"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="job.webhook_sent !== undefined || job.webhook_error">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhook Status</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] 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-[var(--color-success)]' : (job.webhook_error ? 'text-[var(--color-warning)]' : 'text-[var(--color-text-muted)]')"> <span :class="job.webhook_sent && !job.webhook_error ? 'text-green-400' : (job.webhook_error ? 'text-yellow-400' : 'text-gray-400')">
<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-[var(--color-text-primary)]" <span class="text-[#E5E7EB]"
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-[var(--color-text-secondary)] text-sm" x-text="'at ' + formatDate(job.webhook_sent_at)"></span> <span x-show="job.webhook_sent_at" class="text-[#94A3B8] 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">
@@ -322,9 +320,7 @@
} }
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -3,11 +3,9 @@
{{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="app-layout"> <div class="flex flex-col min-h-screen" x-data="agentJobs()" x-init="init()">
{{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 -->
@@ -142,13 +140,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-[var(--color-bg-secondary)] rounded-lg border border-[var(--color-border-subtle)]"> <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 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-[var(--color-warning)] hover:bg-[var(--color-warning)]/80 text-white px-4 py-2 rounded-lg transition-colors text-sm"> class="inline-flex items-center bg-yellow-600 hover:bg-yellow-700 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>
@@ -761,9 +759,7 @@
} }
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -2,22 +2,20 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[#101827] text-[#E5E7EB]">
<div class="app-layout"> <div class="flex flex-col min-h-screen" x-data="taskDetails()" x-init="init()">
{{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-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/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-[var(--color-text-secondary)]" x-text="isNewTask ? 'Create a new agent task' : (task ? task.name : 'Loading...')"></p> <p class="text-lg text-[#94A3B8]" 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">
@@ -39,7 +37,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-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-primary)] text-white px-4 py-2 rounded-lg transition-colors"> class="bg-[#1E293B] hover:bg-[#2D3A4F] text-white px-4 py-2 rounded-lg transition-colors">
Cancel Cancel
</button> </button>
<button @click="saveTask()" <button @click="saveTask()"
@@ -48,7 +46,7 @@
</button> </button>
</div> </div>
</template> </template>
<a href="/agent-jobs" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] px-4 py-2"> <a href="/agent-jobs" class="text-[#94A3B8] hover:text-[#E5E7EB] 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>
@@ -59,62 +57,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-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Basic Information</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Basic Information</h2>
<div class="space-y-6"> <div class="space-y-6">
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">Name *</label> <label class="block text-[#E5E7EB] mb-2">Name *</label>
<input type="text" x-model="taskForm.name" required <input type="text" x-model="taskForm.name" required
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"> 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">
</div> </div>
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">Description</label> <label class="block text-[#E5E7EB] mb-2">Description</label>
<textarea x-model="taskForm.description" rows="3" <textarea x-model="taskForm.description" rows="3"
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> 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>
</div> </div>
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">Model *</label> <label class="block text-[#E5E7EB] mb-2">Model *</label>
<select x-model="taskForm.model" required <select x-model="taskForm.model" required
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"> 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">
<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-[var(--color-bg-secondary)] text-[var(--color-text-primary)]">{{$cfg.Name}}</option> <option value="{{$cfg.Name}}" class="bg-[#1E293B] text-[#E5E7EB]">{{$cfg.Name}}</option>
{{ end }} {{ end }}
{{ end }} {{ end }}
</select> </select>
<p class="text-sm text-[var(--color-text-secondary)] mt-1">Only models with MCP configuration are shown</p> <p class="text-sm text-[#94A3B8] 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-[var(--color-text-primary)]">Enabled</span> <span class="text-[#E5E7EB]">Enabled</span>
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<!-- Prompt Template --> <!-- Prompt Template -->
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Prompt Template</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Prompt Template</h2>
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">Prompt *</label> <label class="block text-[#E5E7EB] mb-2">Prompt *</label>
<p class="text-sm text-[var(--color-text-secondary)] mb-4"> <p class="text-sm text-[#94A3B8] mb-4">
Use Go template syntax with <code class="bg-[var(--color-bg-primary)] px-1.5 py-0.5 rounded text-[var(--color-primary)]">{{"{{"}}.param{{"}}"}}</code> for dynamic parameters. Use Go template syntax with <code class="bg-[#101827] px-1.5 py-0.5 rounded text-[#38BDF8]">{{"{{"}}.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-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4 mb-4"> <div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4 mb-4">
<p class="text-xs text-[var(--color-text-secondary)] mb-2 font-semibold">Example Prompt:</p> <p class="text-xs text-[#94A3B8] mb-2 font-semibold">Example Prompt:</p>
<pre class="text-xs text-[var(--color-text-primary)] font-mono whitespace-pre-wrap">You are a helpful assistant. The user's name is {{"{{"}}.user_name{{"}}"}} and they work as a {{"{{"}}.job_title{{"}}"}}. <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{{"}}"}}.
Please help them with the following task: {{"{{"}}.task_description{{"}}"}} Please help them with the following task: {{"{{"}}.task_description{{"}}"}}
@@ -123,8 +121,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-[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> class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
<p class="text-xs text-[var(--color-text-secondary)] mt-2"> <p class="text-xs text-[#94A3B8] 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>
@@ -132,25 +130,25 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
<!-- Cron Schedule --> <!-- Cron Schedule -->
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Cron Schedule (Optional)</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Cron Schedule (Optional)</h2>
<div class="space-y-6"> <div class="space-y-6">
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">Cron Expression</label> <label class="block text-[#E5E7EB] 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-[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'"> :class="cronError ? 'w-full bg-[#101827] border border-red-500 rounded px-4 py-2 text-[#E5E7EB] focus:border-red-500 focus:ring-2 focus:ring-red-500/50' : 'w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50'">
<p class="text-sm text-[var(--color-text-secondary)] mt-1">Standard 5-field cron format (minute hour day month weekday)</p> <p class="text-sm text-[#94A3B8] 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-[var(--color-text-primary)] mb-2">Cron Parameters (Optional)</label> <label class="block text-[#E5E7EB] mb-2">Cron Parameters (Optional)</label>
<p class="text-sm text-[var(--color-text-secondary)] mb-3"> <p class="text-sm text-[#94A3B8] 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>
@@ -158,27 +156,27 @@ Provide a detailed response that addresses their specific needs.</pre>
@input="updateCronParameters()" @input="updateCronParameters()"
rows="6" rows="6"
placeholder="user_name=Alice&#10;job_title=Software Engineer&#10;task_description=Daily status report" placeholder="user_name=Alice&#10;job_title=Software Engineer&#10;task_description=Daily status report"
class="w-full bg-[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> class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
<p class="text-xs text-[var(--color-text-secondary)] mt-1"> <p class="text-xs text-[#94A3B8] mt-1">
<i class="fas fa-info-circle mr-1"></i> <i class="fas fa-info-circle mr-1"></i>
Example: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">user_name=Alice</code> Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">user_name=Alice</code>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<!-- Multimedia Sources Configuration --> <!-- Multimedia Sources Configuration -->
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Multimedia Sources (Optional)</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Multimedia Sources (Optional)</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4"> <p class="text-sm text-[#94A3B8] 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-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10"> <div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/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-[var(--color-text-primary)]">Multimedia Source <span x-text="index + 1"></span></h3> <h3 class="text-lg font-semibold text-[#E5E7EB]">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>
@@ -186,9 +184,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-[var(--color-text-primary)] mb-2">Type *</label> <label class="block text-[#E5E7EB] mb-2">Type *</label>
<select x-model="source.type" required <select x-model="source.type" required
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"> 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">
<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>
@@ -197,40 +195,40 @@ Provide a detailed response that addresses their specific needs.</pre>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">URL *</label> <label class="block text-[#E5E7EB] 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-[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"> class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50">
<p class="text-xs text-[var(--color-text-secondary)] mt-1">URL where multimedia content will be fetched from</p> <p class="text-xs text-[#94A3B8] mt-1">URL where multimedia content will be fetched from</p>
</div> </div>
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">Headers (JSON)</label> <label class="block text-[#E5E7EB] 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-[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> class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
<p class="text-xs text-[var(--color-text-secondary)] mt-1">Custom headers for the HTTP request (e.g., Authorization)</p> <p class="text-xs text-[#94A3B8] 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-[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"> 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">
<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-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhooks (Optional)</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">Webhooks (Optional)</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4"> <p class="text-sm text-[#94A3B8] 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-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10"> <div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/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-[var(--color-text-primary)]">Webhook <span x-text="index + 1"></span></h3> <h3 class="text-lg font-semibold text-[#E5E7EB]">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>
@@ -238,35 +236,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-[var(--color-text-primary)] mb-2">URL *</label> <label class="block text-[#E5E7EB] 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-[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"> class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50">
<p class="text-xs text-[var(--color-text-secondary)] mt-1">URL where webhook notifications will be sent</p> <p class="text-xs text-[#94A3B8] mt-1">URL where webhook notifications will be sent</p>
</div> </div>
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">HTTP Method</label> <label class="block text-[#E5E7EB] mb-2">HTTP Method</label>
<select x-model="webhook.method" <select x-model="webhook.method"
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"> 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">
<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-[var(--color-text-primary)] mb-2">Headers (JSON)</label> <label class="block text-[#E5E7EB] 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-[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> class="w-full bg-[#0A0E1A] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
<p class="text-xs text-[var(--color-text-secondary)] mt-1">Custom headers for the webhook request (e.g., Authorization)</p> <p class="text-xs text-[#94A3B8] mt-1">Custom headers for the webhook request (e.g., Authorization)</p>
</div> </div>
<div> <div>
<label class="block text-[var(--color-text-primary)] mb-2">Custom Payload Template (Optional)</label> <label class="block text-[#E5E7EB] mb-2">Custom Payload Template (Optional)</label>
<p class="text-xs text-[var(--color-text-secondary)] mb-2">Customize the webhook payload using Go template syntax. Available variables: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Job</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Task</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Result</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Error</code>, <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">.Status</code></p> <p class="text-xs text-[#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">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> <p class="text-xs text-[#94A3B8] mb-2">Note: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">.Error</code> will be empty string if job succeeded, or contain the error message if it failed. Use this to handle both success and failure cases in a single webhook.</p>
<div class="bg-[var(--color-bg-tertiary)] border border-[var(--color-primary)]/10 rounded-lg p-3 mb-2"> <div class="bg-[#0A0E1A] border border-[#38BDF8]/10 rounded-lg p-3 mb-2">
<p class="text-xs text-[var(--color-text-secondary)] mb-1 font-semibold">Example (Slack with error handling):</p> <p class="text-xs text-[#94A3B8] mb-1 font-semibold">Example (Slack with error handling):</p>
<pre class="text-xs text-[var(--color-text-primary)] font-mono whitespace-pre-wrap">{ <pre class="text-xs text-[#E5E7EB] 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": [
{ {
@@ -281,13 +279,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-[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> 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>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<button type="button" @click="addWebhook()" <button type="button" @click="addWebhook()"
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"> class="w-full bg-[#101827] border border-[#38BDF8]/20 hover:border-[#38BDF8]/40 rounded px-4 py-3 text-[#38BDF8] 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>
@@ -299,15 +297,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-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Task Information</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] 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-[var(--color-text-secondary)] text-sm">Name</label> <label class="text-[#94A3B8] text-sm">Name</label>
<div class="text-[var(--color-text-primary)] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></div> <div class="text-[#E5E7EB] mt-1 font-semibold" x-text="task ? task.name : 'Loading...'"></div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Status</label> <label class="text-[#94A3B8] 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"
@@ -315,10 +313,10 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Model</label> <label class="text-[#94A3B8] 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-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline" class="text-[#38BDF8] hover:text-[#38BDF8]/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"
@@ -328,47 +326,47 @@ Provide a detailed response that addresses their specific needs.</pre>
</div> </div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Cron Schedule</label> <label class="text-[#94A3B8] text-sm">Cron Schedule</label>
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm" x-text="task && task.cron ? task.cron : '-'"></div> <div class="text-[#E5E7EB] 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-[var(--color-text-secondary)] text-sm">Cron Parameters</label> <label class="text-[#94A3B8] 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-[var(--color-text-primary)] text-sm mb-1"> <div class="text-[#E5E7EB] text-sm mb-1">
<span class="font-semibold text-[var(--color-primary)]" x-text="key + ':'"></span> <span class="font-semibold text-[#38BDF8]" 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-[var(--color-text-secondary)] text-sm">Description</label> <label class="text-[#94A3B8] text-sm">Description</label>
<div class="text-[var(--color-text-primary)] mt-1" x-text="task && task.description ? task.description : 'No description'"></div> <div class="text-[#E5E7EB] 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-[var(--color-text-secondary)] text-sm">Prompt Template</label> <label class="text-[#94A3B8] text-sm">Prompt Template</label>
<pre class="bg-[var(--color-bg-primary)] p-4 rounded text-[var(--color-text-primary)] text-sm mt-1 whitespace-pre-wrap" x-text="task ? task.prompt : '-'"></pre> <pre class="bg-[#101827] p-4 rounded text-[#E5E7EB] 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-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8 mb-8" x-show="task && task.id"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="task && task.id">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">API Usage Examples</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-6">API Usage Examples</h2>
<p class="text-sm text-[var(--color-text-secondary)] mb-4"> <p class="text-sm text-[#94A3B8] 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-[var(--color-text-primary)] mb-3 flex items-center"> <h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
<i class="fas fa-play text-[var(--color-primary)] mr-2"></i> <i class="fas fa-play text-[#38BDF8] mr-2"></i>
Execute Task by ID Execute Task by ID
</h3> </h3>
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4"> <div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
<pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \ <pre class="text-xs text-[#E5E7EB] 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 '{
@@ -384,12 +382,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-[var(--color-text-primary)] mb-3 flex items-center"> <h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
<i class="fas fa-code text-[var(--color-primary)] mr-2"></i> <i class="fas fa-code text-[#38BDF8] mr-2"></i>
Execute Task by Name Execute Task by Name
</h3> </h3>
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4"> <div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
<pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/tasks/<span x-text="task ? task.name : 'task-name'"></span>/execute \ <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 \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \ -H "Authorization: Bearer YOUR_API_KEY" \
-d '{ -d '{
@@ -398,7 +396,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-[var(--color-text-secondary)] mt-2"> <p class="text-xs text-[#94A3B8] 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.
@@ -407,12 +405,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-[var(--color-text-primary)] mb-3 flex items-center"> <h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
<i class="fas fa-images text-[var(--color-primary)] mr-2"></i> <i class="fas fa-images text-[#38BDF8] mr-2"></i>
Execute Task with Multimedia (Images) Execute Task with Multimedia (Images)
</h3> </h3>
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4"> <div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
<pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X POST {{ .BaseURL }}api/agent/jobs/execute \ <pre class="text-xs text-[#E5E7EB] 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 '{
@@ -427,53 +425,53 @@ Provide a detailed response that addresses their specific needs.</pre>
] ]
}'</code></pre> }'</code></pre>
</div> </div>
<p class="text-xs text-[var(--color-text-secondary)] mt-2"> <p class="text-xs text-[#94A3B8] mt-2">
You can provide multimedia content as URLs or base64-encoded data URIs. Supported types: <code class="bg-[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>. You can provide multimedia content as URLs or base64-encoded data URIs. Supported types: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">images</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">videos</code>, <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">audios</code>, and <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">files</code>.
</p> </p>
</div> </div>
<!-- Check Job Status --> <!-- Check Job Status -->
<div> <div>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-3 flex items-center"> <h3 class="text-lg font-semibold text-[#E5E7EB] mb-3 flex items-center">
<i class="fas fa-info-circle text-[var(--color-primary)] mr-2"></i> <i class="fas fa-info-circle text-[#38BDF8] mr-2"></i>
Check Job Status Check Job Status
</h3> </h3>
<div class="bg-[var(--color-bg-primary)] border border-[var(--color-primary)]/10 rounded-lg p-4"> <div class="bg-[#101827] border border-[#38BDF8]/10 rounded-lg p-4">
<pre class="text-xs text-[var(--color-text-primary)] font-mono overflow-x-auto"><code>curl -X GET {{ .BaseURL }}api/agent/jobs/JOB_ID \ <pre class="text-xs text-[#E5E7EB] 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-[var(--color-text-secondary)] mt-2"> <p class="text-xs text-[#94A3B8] mt-2">
After executing a task, you will receive a <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">job_id</code> in the response. Use it to query the job's status and results. After executing a task, you will receive a <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">job_id</code> in the response. Use it to query the job's status and results.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<!-- Webhook Configuration (View Mode) --> <!-- Webhook Configuration (View Mode) -->
<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"> <div class="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl p-8 mb-8" x-show="task && task.id && task.webhooks && task.webhooks.length > 0">
<h2 class="text-2xl font-semibold text-[var(--color-text-primary)] mb-6">Webhook Configuration</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB] 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-[var(--color-bg-primary)] p-4 rounded border border-[var(--color-primary)]/10"> <div class="bg-[#101827] p-4 rounded border border-[#38BDF8]/10">
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Webhook <span x-text="index + 1"></span></h3> <h3 class="text-lg font-semibold text-[#E5E7EB]">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-[var(--color-text-secondary)] text-sm">URL</label> <label class="text-[#94A3B8] text-sm">URL</label>
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div> <div class="text-[#E5E7EB] mt-1 font-mono text-sm break-all" x-text="webhook.url"></div>
</div> </div>
<div> <div>
<label class="text-[var(--color-text-secondary)] text-sm">Method</label> <label class="text-[#94A3B8] text-sm">Method</label>
<div class="text-[var(--color-text-primary)] mt-1 font-mono text-sm" x-text="webhook.method || 'POST'"></div> <div class="text-[#E5E7EB] 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-[var(--color-text-secondary)] text-sm">Headers</label> <label class="text-[#94A3B8] text-sm">Headers</label>
<pre class="bg-[var(--color-bg-tertiary)] p-3 rounded text-[var(--color-text-primary)] text-xs mt-1 overflow-x-auto" x-text="JSON.stringify(webhook.headers, null, 2)"></pre> <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>
</div> </div>
<div x-show="webhook.payload_template"> <div x-show="webhook.payload_template">
<label class="text-[var(--color-text-secondary)] text-sm">Payload Template</label> <label class="text-[#94A3B8] text-sm">Payload Template</label>
<pre class="bg-[var(--color-bg-tertiary)] p-3 rounded text-[var(--color-text-primary)] text-xs mt-1 whitespace-pre-wrap overflow-x-auto" x-text="webhook.payload_template"></pre> <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>
</div> </div>
</div> </div>
</div> </div>
@@ -484,12 +482,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-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-xl p-8"> <div class="bg-[#1E293B] border border-[#38BDF8]/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-[var(--color-text-primary)]">Job History</h2> <h2 class="text-2xl font-semibold text-[#E5E7EB]">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-[var(--color-bg-primary)] border border-[var(--color-primary)]/20 rounded px-4 py-2 text-[var(--color-text-primary)]"> class="bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB]">
<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>
@@ -507,20 +505,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-[var(--color-primary)]/20"> <tr class="border-b border-[#38BDF8]/20">
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Job ID</th> <th class="text-left py-3 px-4 text-[#94A3B8]">Job ID</th>
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Status</th> <th class="text-left py-3 px-4 text-[#94A3B8]">Status</th>
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Created</th> <th class="text-left py-3 px-4 text-[#94A3B8]">Created</th>
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Triggered By</th> <th class="text-left py-3 px-4 text-[#94A3B8]">Triggered By</th>
<th class="text-left py-3 px-4 text-[var(--color-text-secondary)]">Actions</th> <th class="text-left py-3 px-4 text-[#94A3B8]">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-[var(--color-primary)]/10 hover:bg-[var(--color-bg-primary)]"> <tr class="border-b border-[#38BDF8]/10 hover:bg-[#101827]">
<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-[var(--color-primary)] hover:text-[var(--color-primary)]/80 hover:underline" class="font-mono text-sm text-[#38BDF8] hover:text-[#38BDF8]/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>
@@ -535,8 +533,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-[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="formatDate(job.created_at)"></td>
<td class="py-3 px-4 text-[var(--color-text-secondary)] text-sm" x-text="job.triggered_by || '-'"></td> <td class="py-3 px-4 text-[#94A3B8] 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)"
@@ -548,7 +546,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-[var(--color-text-secondary)]">No jobs found for this task</td> <td colspan="5" class="py-8 text-center text-[#94A3B8]">No jobs found for this task</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -562,11 +560,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-[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="bg-[#1E293B] border border-[#38BDF8]/20 rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] flex flex-col" @click.stop>
<div class="flex justify-between items-center p-8 pb-6 border-b border-[var(--color-primary)]/20"> <div class="flex justify-between items-center p-8 pb-6 border-b border-[#38BDF8]/20">
<h3 class="text-2xl font-semibold text-[var(--color-text-primary)]">Execute Task</h3> <h3 class="text-2xl font-semibold text-[#E5E7EB]">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-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"> class="text-[#94A3B8] hover:text-[#E5E7EB]">
<i class="fas fa-times text-xl"></i> <i class="fas fa-times text-xl"></i>
</button> </button>
</div> </div>
@@ -574,20 +572,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-[var(--color-text-primary)] mb-2">Task</label> <label class="block text-sm font-medium text-[#E5E7EB] mb-2">Task</label>
<div class="text-[var(--color-text-secondary)]" x-text="task.name"></div> <div class="text-[#94A3B8]" x-text="task.name"></div>
</div> </div>
<!-- Tabs for Parameters and Multimedia --> <!-- Tabs for Parameters and Multimedia -->
<div class="border-b border-[var(--color-primary)]/20"> <div class="border-b border-[#38BDF8]/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-[var(--color-primary)] text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'" :class="executeModalTab === 'parameters' ? 'border-b-2 border-[#38BDF8] text-[#38BDF8]' : 'text-[#94A3B8] hover:text-[#E5E7EB]'"
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-[var(--color-primary)] text-[var(--color-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'" :class="executeModalTab === 'multimedia' ? 'border-b-2 border-[#38BDF8] text-[#38BDF8]' : 'text-[#94A3B8] hover:text-[#E5E7EB]'"
class="px-4 py-2 font-medium transition-colors"> class="px-4 py-2 font-medium transition-colors">
Multimedia Multimedia
</button> </button>
@@ -596,75 +594,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-[var(--color-text-primary)] mb-2">Parameters</label> <label class="block text-sm font-medium text-[#E5E7EB] mb-2">Parameters</label>
<p class="text-xs text-[var(--color-text-secondary)] mb-3"> <p class="text-xs text-[#94A3B8] mb-3">
Enter parameters as key-value pairs (one per line, format: key=value). Enter parameters as key-value pairs (one per line, format: key=value).
These will be used to template the prompt. These will be used to template the prompt.
</p> </p>
<textarea x-model="executionParametersText" <textarea x-model="executionParametersText"
rows="6" rows="6"
placeholder="user_name=Alice&#10;job_title=Software Engineer&#10;task_description=Review code changes" placeholder="user_name=Alice&#10;job_title=Software Engineer&#10;task_description=Review code changes"
class="w-full bg-[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> class="w-full bg-[#101827] border border-[#38BDF8]/20 rounded px-4 py-2 text-[#E5E7EB] font-mono text-sm focus:border-[#38BDF8] focus:ring-2 focus:ring-[#38BDF8]/50"></textarea>
<p class="text-xs text-[var(--color-text-secondary)] mt-1"> <p class="text-xs text-[#94A3B8] mt-1">
Example: <code class="bg-[var(--color-bg-primary)] px-1 py-0.5 rounded text-[var(--color-primary)]">user_name=Alice</code> Example: <code class="bg-[#101827] px-1 py-0.5 rounded text-[#38BDF8]">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-[var(--color-text-secondary)] mb-3"> <p class="text-xs text-[#94A3B8] 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-[var(--color-text-primary)] mb-2">Images</label> <label class="block text-sm font-medium text-[#E5E7EB] mb-2">Images</label>
<textarea x-model="executionMultimedia.images" <textarea x-model="executionMultimedia.images"
rows="3" rows="3"
placeholder="https://example.com/image.png&#10;data:image/png;base64,iVBORw0KG..." placeholder="https://example.com/image.png&#10;data:image/png;base64,iVBORw0KG..."
class="w-full bg-[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> 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>
<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-[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"> 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">
</div> </div>
<!-- Videos --> <!-- Videos -->
<div> <div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Videos</label> <label class="block text-sm font-medium text-[#E5E7EB] mb-2">Videos</label>
<textarea x-model="executionMultimedia.videos" <textarea x-model="executionMultimedia.videos"
rows="3" rows="3"
placeholder="https://example.com/video.mp4&#10;data:video/mp4;base64,..." placeholder="https://example.com/video.mp4&#10;data:video/mp4;base64,..."
class="w-full bg-[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> 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>
<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-[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"> 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">
</div> </div>
<!-- Audios --> <!-- Audios -->
<div> <div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Audios</label> <label class="block text-sm font-medium text-[#E5E7EB] mb-2">Audios</label>
<textarea x-model="executionMultimedia.audios" <textarea x-model="executionMultimedia.audios"
rows="3" rows="3"
placeholder="https://example.com/audio.mp3&#10;data:audio/mpeg;base64,..." placeholder="https://example.com/audio.mp3&#10;data:audio/mpeg;base64,..."
class="w-full bg-[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> 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>
<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-[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"> 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">
</div> </div>
<!-- Files --> <!-- Files -->
<div> <div>
<label class="block text-sm font-medium text-[var(--color-text-primary)] mb-2">Files</label> <label class="block text-sm font-medium text-[#E5E7EB] mb-2">Files</label>
<textarea x-model="executionMultimedia.files" <textarea x-model="executionMultimedia.files"
rows="3" rows="3"
placeholder="https://example.com/file.pdf&#10;data:application/pdf;base64,..." placeholder="https://example.com/file.pdf&#10;data:application/pdf;base64,..."
class="w-full bg-[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> 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>
<input type="file" @change="handleFileUpload($event, 'file')" multiple <input type="file" @change="handleFileUpload($event, 'file')" multiple
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"> 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">
</div> </div>
</div> </div>
</div> </div>
<div class="flex justify-end space-x-4 p-8 pt-6 border-t border-[var(--color-primary)]/20 bg-[var(--color-bg-secondary)]"> <div class="flex justify-end space-x-4 p-8 pt-6 border-t border-[#38BDF8]/20 bg-[#1E293B]">
<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-[var(--color-bg-primary)] hover:bg-[var(--color-bg-tertiary)] text-[var(--color-text-primary)] rounded-lg transition-colors"> class="px-4 py-2 bg-[#101827] hover:bg-[#0A0E1A] text-[#E5E7EB] rounded-lg transition-colors">
Cancel Cancel
</button> </button>
<button @click="executeTaskWithParameters()" <button @click="executeTaskWithParameters()"
@@ -1137,9 +1135,6 @@ Provide a detailed response that addresses their specific needs.</pre>
} }
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -2,12 +2,10 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[#101827] text-[#E5E7EB]">
<div class="app-layout"> <div class="flex flex-col min-h-screen" x-data="backendsGallery()">
{{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;">
@@ -46,25 +44,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-[var(--color-bg-primary)] rounded-lg px-4 py-2"> <div class="flex items-center bg-[#101827] rounded-lg px-4 py-2">
<div class="w-2 h-2 bg-[var(--color-success)] rounded-full mr-2"></div> <div class="w-2 h-2 bg-emerald-400 rounded-full mr-2"></div>
<span class="font-semibold text-[var(--color-success)]" x-text="availableBackends"></span> <span class="font-semibold text-emerald-300" x-text="availableBackends"></span>
<span class="text-[var(--color-text-secondary)] ml-1">backends available</span> <span class="text-[#94A3B8] ml-1">backends available</span>
</div> </div>
<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"> <a href="/manage" class="flex items-center bg-[#101827] hover:bg-[#1E293B] rounded-lg px-4 py-2 transition-colors border border-[#8B5CF6]/30 hover:border-[#8B5CF6]/50">
<div class="w-2 h-2 bg-[var(--color-primary)] rounded-full mr-2"></div> <div class="w-2 h-2 bg-cyan-400 rounded-full mr-2"></div>
<span class="font-semibold text-[var(--color-primary)]" x-text="installedBackends"></span> <span class="font-semibold text-cyan-300" x-text="installedBackends"></span>
<span class="text-[var(--color-text-secondary)] ml-1">installed</span> <span class="text-[#94A3B8] ml-1">installed</span>
</a> </a>
<div class="flex items-center bg-[var(--color-bg-primary)] rounded-lg px-4 py-2 border border-[var(--color-primary-border)]"> <div class="flex items-center bg-[#101827] rounded-lg px-4 py-2 border border-[#38BDF8]/30">
<i class="fas fa-microchip text-[var(--color-primary)] mr-2"></i> <i class="fas fa-microchip text-[#38BDF8] mr-2"></i>
<span class="text-[var(--color-text-secondary)] mr-1">Capability:</span> <span class="text-[#94A3B8] mr-1">Capability:</span>
<span class="font-semibold text-[var(--color-primary)]" x-text="systemCapability"></span> <span class="font-semibold text-[#38BDF8]" x-text="systemCapability"></span>
</div> </div>
<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"> <a href="https://localai.io/backends/" target="_blank" class="btn-primary">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle mr-2"></i>
<span>Documentation</span> <span>Documentation</span>
<i class="fas fa-external-link-alt text-[10px]"></i> <i class="fas fa-external-link-alt ml-2 text-xs"></i>
</a> </a>
</div> </div>
</div> </div>
@@ -79,55 +77,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-[var(--color-primary)] text-lg"></i> <i class="fas fa-plus-circle text-[#38BDF8] text-lg"></i>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">Install Backend Manually</h3> <h3 class="text-lg font-semibold text-[#E5E7EB]">Install Backend Manually</h3>
</div> </div>
<i class="fas text-[var(--color-text-secondary)] transition-transform duration-200" :class="showManualInstall ? 'fa-chevron-up' : 'fa-chevron-down'"></i> <i class="fas text-[#94A3B8] 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-[var(--color-text-secondary)] mt-4 mb-6">Install a backend from an OCI image, URL, or local path</p> <p class="text-sm text-[#94A3B8] 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-[var(--color-text-secondary)] mb-2">OCI Image / URL / Path *</label> <label class="block text-sm font-medium text-[#94A3B8] 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="input w-full px-4 py-3 text-sm" 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]"
> >
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Name (required for OCI)</label> <label class="block text-sm font-medium text-[#94A3B8] 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="input w-full px-4 py-3 text-sm" 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]"
> >
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2">Alias (optional)</label> <label class="block text-sm font-medium text-[#94A3B8] 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="input w-full px-4 py-3 text-sm" 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]"
> >
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<button type="button" <button
@click="installExternalBackend()" @click="installExternalBackend()"
:disabled="installingExternal || !externalBackend.uri" :disabled="installingExternal || !externalBackend.uri"
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)]" 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"
> >
<i class="text-[10px]" :class="installingExternal ? 'fas fa-spinner fa-spin' : 'fas fa-download'"></i> <i class="mr-2" :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-[var(--color-text-secondary)]" x-text="externalBackendProgress"></span> <span x-show="externalBackendProgress" class="text-sm text-[#94A3B8]" x-text="externalBackendProgress"></span>
</div> </div>
</div> </div>
</div> </div>
@@ -137,13 +135,13 @@
<div> <div>
<!-- Search Input --> <!-- Search Input -->
<div class="mb-8"> <div class="mb-8">
<h3 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center"> <h3 class="text-xl font-semibold text-[#E5E7EB] mb-4 flex items-center">
<i class="fas fa-search mr-3 text-[var(--color-accent)]"></i> <i class="fas fa-search mr-3 text-[#8B5CF6]"></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-[var(--color-text-secondary)]"></i> <i class="fas fa-search text-[#94A3B8]"></i>
</div> </div>
<input <input
x-model="searchTerm" x-model="searchTerm"
@@ -153,7 +151,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-[var(--color-accent)]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin h-6 w-6 text-[#8B5CF6]" 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>
@@ -163,33 +161,33 @@
<!-- Filter by Type --> <!-- Filter by Type -->
<div> <div>
<h3 class="text-lg font-semibold text-[var(--color-text-primary)] mb-4 flex items-center"> <h3 class="text-lg font-semibold text-white mb-4 flex items-center">
<i class="fas fa-filter mr-3 text-[var(--color-secondary)]"></i> <i class="fas fa-filter mr-3 text-teal-400"></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-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-text-primary)] border border-[var(--color-accent)]/30 transition-colors"> 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">
<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-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-[var(--color-text-primary)] border border-[var(--color-accent)]/30 transition-colors"> 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">
<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-[var(--color-primary-light)] hover:bg-[var(--color-primary)]/30 text-[var(--color-text-primary)] border border-[var(--color-primary-border)] transition-colors"> 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">
<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-[var(--color-success-light)] hover:bg-[var(--color-success)]/30 text-[var(--color-success)] border border-[var(--color-success)]/30 transition-colors"> 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">
<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-[var(--color-error-light)] hover:bg-[var(--color-error)]/30 text-[var(--color-error)] border border-[var(--color-error)]/30 transition-colors"> 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">
<i class="fas fa-eye mr-2"></i> <i class="fas fa-eye mr-2"></i>
<span>Vision</span> <span>Vision</span>
</button> </button>
@@ -201,97 +199,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-[var(--color-primary)] 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-emerald-500 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-[var(--color-text-secondary)]">Loading backends...</p> <p class="text-gray-400">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-[var(--color-text-muted)] text-4xl mb-4"></i> <i class="fas fa-search text-gray-500 text-4xl mb-4"></i>
<p class="text-[var(--color-text-secondary)]">No backends found matching your criteria</p> <p class="text-gray-400">No backends found matching your criteria</p>
</div> </div>
<!-- Table View --> <!-- Table View -->
<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 x-show="backends.length > 0" class="bg-[#1E293B] rounded-2xl border border-[#38BDF8]/20 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-[var(--color-primary-light)] border-b border-[var(--color-border-subtle)]"> <tr class="bg-gradient-to-r from-[#38BDF8]/20 to-[#8B5CF6]/20 border-b border-[#38BDF8]/30">
<th class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider">Icon</th> <th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Icon</th>
<th @click="setSort('name')" <th @click="setSort('name')"
:class="sortBy === 'name' ? 'bg-[var(--color-primary-light)]' : ''" :class="sortBy === 'name' ? 'bg-[#38BDF8]/20' : ''"
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"> class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 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-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'" :class="sortBy === 'name' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
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-[var(--color-text-primary)] uppercase tracking-wider">Description</th> <th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Description</th>
<th @click="setSort('repository')" <th @click="setSort('repository')"
:class="sortBy === 'repository' ? 'bg-[var(--color-primary-light)]' : ''" :class="sortBy === 'repository' ? 'bg-[#38BDF8]/20' : ''"
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"> class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 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-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'" :class="sortBy === 'repository' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
class="text-xs"></i> class="text-xs"></i>
</div> </div>
</th> </th>
<th @click="setSort('license')" <th @click="setSort('license')"
:class="sortBy === 'license' ? 'bg-[var(--color-primary-light)]' : ''" :class="sortBy === 'license' ? 'bg-[#38BDF8]/20' : ''"
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"> class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 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-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'" :class="sortBy === 'license' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
class="text-xs"></i> class="text-xs"></i>
</div> </div>
</th> </th>
<th @click="setSort('status')" <th @click="setSort('status')"
:class="sortBy === 'status' ? 'bg-[var(--color-primary-light)]' : ''" :class="sortBy === 'status' ? 'bg-[#38BDF8]/20' : ''"
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"> class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 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-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'" :class="sortBy === 'status' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
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-[var(--color-text-primary)] uppercase tracking-wider">Actions</th> <th class="px-6 py-4 text-right text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-[var(--color-border-subtle)]"> <tbody class="divide-y divide-[#38BDF8]/20">
<template x-for="backend in backends" :key="backend.id"> <template x-for="backend in backends" :key="backend.id">
<tr class="hover:bg-[var(--color-bg-primary)] transition-colors duration-200"> <tr class="hover:bg-[#38BDF8]/10 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-[var(--color-border-subtle)] flex items-center justify-center bg-[var(--color-bg-primary)]"> <div class="w-12 h-12 rounded-lg border border-[#38BDF8]/30 flex items-center justify-center bg-[#101827]">
<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-[var(--color-accent)]"></i> <i x-show="!backend.icon" class="fas fa-cog text-xl text-[#8B5CF6]"></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-[var(--color-text-primary)]" x-text="backend.name"></span> <span class="text-sm font-semibold text-[#E5E7EB]" 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-[var(--color-text-secondary)] max-w-xs truncate" x-text="backend.description" :title="backend.description"></div> <div class="text-sm text-[#94A3B8] 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-[var(--color-primary-light)] text-[var(--color-text-primary)] border border-[var(--color-primary-border)]"> <span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#38BDF8]/10 text-[#E5E7EB] border border-[#38BDF8]/30">
<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>
@@ -299,21 +297,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-[var(--color-accent-light)] text-[var(--color-text-primary)] border border-[var(--color-accent)]/30"> <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">
<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-[var(--color-text-secondary)]">-</span> <span x-show="!backend.license" class="text-xs text-[#94A3B8]">-</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-[var(--color-text-primary)] mb-1"> <div class="text-xs font-medium text-[#E5E7EB] 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-[var(--color-primary)]"> <div x-show="(jobProgress[backend.jobID] || 0) === 0" class="text-xs text-[#38BDF8]">
<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">
@@ -323,7 +321,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-[var(--color-success-light)] text-[var(--color-success)] border border-[var(--color-success)]/30"> <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">
<i class="fas fa-check-circle mr-1"></i> <i class="fas fa-check-circle mr-1"></i>
Installed Installed
</span> </span>
@@ -331,7 +329,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-[var(--color-bg-primary)] text-[var(--color-text-secondary)] border border-[var(--color-border-subtle)]"> <span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#1E293B] text-[#94A3B8] border border-[#38BDF8]/30">
<i class="fas fa-circle mr-1"></i> <i class="fas fa-circle mr-1"></i>
Not Installed Not Installed
</span> </span>
@@ -343,7 +341,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-[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)]" 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"
title="View details"> title="View details">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
</button> </button>
@@ -352,12 +350,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-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-xs font-medium text-white transition duration-200" 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"
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-[var(--color-error)] hover:bg-[var(--color-error)]/80 text-xs font-medium text-white transition duration-200" 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"
title="Delete"> title="Delete">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
</button> </button>
@@ -367,7 +365,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-[var(--color-primary)] hover:bg-[var(--color-primary-hover)] text-xs font-medium text-white transition duration-200" 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"
title="Install"> title="Install">
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
</button> </button>
@@ -385,15 +383,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-black/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-gray-900/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-[var(--color-bg-secondary)] rounded-lg shadow h-full flex flex-col border border-[var(--color-border-subtle)]"> <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 h-full flex flex-col">
<!-- Modal Header --> <!-- Modal Header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b border-[var(--color-border-subtle)] rounded-t"> <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-[var(--color-text-primary)]" x-text="selectedBackend?.name"></h3> <h3 class="text-xl font-semibold text-gray-900 dark:text-white" x-text="selectedBackend?.name"></h3>
<button @click="closeModal()" <button @click="closeModal()"
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"> 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">
<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>
@@ -403,21 +401,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-[var(--color-border-subtle)] flex items-center justify-center bg-[var(--color-bg-primary)] mt-3"> <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">
<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-[var(--color-text-muted)]"></i> <i x-show="!selectedBackend?.icon" class="fas fa-cog text-6xl text-gray-400 dark:text-gray-500"></i>
</div> </div>
</div> </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> <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>
<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-[var(--color-text-primary)]">Tags</p> <p class="text-sm mb-3 font-semibold text-gray-900 dark:text-white">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-[var(--color-bg-primary)] text-[var(--color-text-secondary)] border border-[var(--color-border-subtle)]"> <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">
<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>
@@ -427,11 +425,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-[var(--color-text-primary)] mb-2">Links</p> <p class="text-sm font-semibold text-gray-900 dark:text-white 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-[var(--color-primary)] hover:underline"> <a :href="url" target="_blank" class="text-blue-500 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>
@@ -442,9 +440,9 @@
</template> </template>
</div> </div>
<!-- Modal Footer --> <!-- Modal Footer -->
<div class="flex items-center p-4 md:p-5 border-t border-[var(--color-border-subtle)] rounded-b"> <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
<button @click="closeModal()" <button @click="closeModal()"
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"> 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">
Close Close
</button> </button>
</div> </div>
@@ -455,29 +453,30 @@
<!-- 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-[var(--color-bg-secondary)] rounded-2xl p-4 backdrop-blur-sm border border-[var(--color-border-subtle)]"> <div class="flex items-center gap-4 bg-gray-800/60 rounded-2xl p-4 backdrop-blur-sm border border-gray-700/50">
<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-[var(--color-bg-primary)] hover:bg-[var(--color-success)] text-[var(--color-text-secondary)] hover:text-white rounded-lg transition-colors"> 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">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
</button> </button>
<div class="text-[var(--color-text-primary)] text-sm font-medium px-4"> <div class="text-gray-300 text-sm font-medium px-4">
<span class="text-[var(--color-text-secondary)]">Page</span> <span class="text-gray-400">Page</span>
<span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="currentPage"></span> <span class="text-white font-bold text-lg mx-2" x-text="currentPage"></span>
<span class="text-[var(--color-text-secondary)]">of</span> <span class="text-gray-400">of</span>
<span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="totalPages"></span> <span class="text-white 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-[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"> 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">
<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>
@@ -517,16 +516,16 @@
/* Table progress bar styling */ /* Table progress bar styling */
.progress-table { .progress-table {
background: var(--color-primary-light); background: linear-gradient(135deg, rgba(56, 189, 248, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
border-radius: 0.25rem; border-radius: 0.25rem;
border: 1px solid var(--color-primary-border); border: 1px solid rgba(56, 189, 248, 0.3);
height: 6px; height: 6px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
} }
.progress-bar-table-backend { .progress-bar-table-backend {
background: var(--gradient-primary); background: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 100%);
height: 100%; height: 100%;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
@@ -535,7 +534,6 @@
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 {
@@ -907,10 +905,5 @@ function backendsGallery() {
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -587,25 +587,19 @@ 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)]" 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 })"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen" x-data="{ sidebarOpen: true, showClearAlert: false }">
<div class="app-layout chat-layout"> {{template "views/partials/navbar" .}}
{{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 settings panel --> <!-- Main container with sidebar toggle -->
<div class="flex flex-1 min-h-0 relative"> <div class="flex flex-1 overflow-hidden relative">
<!-- Backdrop for mobile when settings panel is open (click to close) --> <!-- Sidebar -->
<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="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="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="settingsPanelOpen ? 'translate-x-0' : 'translate-x-full'"> :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
<div class="p-3 flex justify-between items-center border-b border-[var(--color-border-subtle)]"> <div class="p-3 flex justify-between items-center border-b border-[var(--color-bg-primary)]">
<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)]">Chat Settings</h2> <h2 class="text-sm font-semibold text-[var(--color-text-primary)]">Settings</h2>
<a <a
href="https://localai.io/features/text-generation/" href="https://localai.io/features/text-generation/"
target="_blank" target="_blank"
@@ -615,10 +609,10 @@ SOFTWARE.
</a> </a>
</div> </div>
<button <button
@click="settingsPanelOpen = false" @click="sidebarOpen = 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 settings"> title="Hide sidebar">
<i class="fa-solid fa-chevron-right"></i> <i class="fa-solid fa-chevron-left"></i>
</button> </button>
</div> </div>
@@ -1105,57 +1099,61 @@ SOFTWARE.
</div> </div>
</div> </div>
<!-- Main chat container (shifts with settings panel on desktop only; on mobile panel overlays) --> <!-- Main chat container (shifts with sidebar) -->
<div <div
class="flex-1 flex flex-col min-h-0 transition-all duration-300 ease-in-out" class="flex-1 flex flex-col transition-all duration-300 ease-in-out"
:class="settingsPanelOpen ? 'md:mr-56' : 'mr-0'"> :class="sidebarOpen ? 'ml-56' : 'ml-0'">
<!-- Chat header with toggle button --> <!-- Chat header with toggle button -->
<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="border-b border-[var(--color-bg-secondary)] p-4 flex items-center justify-between">
<div class="flex items-center min-w-0 flex-1"> <div class="flex items-center">
<i class="fa-solid fa-comments mr-2 text-[var(--color-primary)] flex-shrink-0"></i> <!-- Sidebar toggle button moved to be the first element in the header and with clear styling -->
<!-- Model icon - reactive to active chat --> <button
<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"> @click="sidebarOpen = !sidebarOpen"
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2 flex-shrink-0"> 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"
</template> style="min-width: 36px;"
<!-- Fallback icon for initial model from server (when no active chat yet) --> title="Toggle settings">
<template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon"> <i class="fa-solid" :class="sidebarOpen ? 'fa-chevron-left' : 'fa-bars'"></i>
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2 flex-shrink-0"> </button>
</template>
<h1 class="text-lg font-semibold text-[var(--color-text-primary)] truncate min-w-0"> <div class="flex items-center">
Chat <i class="fa-solid fa-comments mr-2 text-[var(--color-primary)]"></i>
<template x-if="$store.chat.activeChat() && $store.chat.activeChat().model"> <!-- Model icon - reactive to active chat -->
<span x-text="' with ' + $store.chat.activeChat().model"></span> <template x-if="$store.chat.activeChat() && $store.chat.activeChat().model && window.__galleryConfigs && window.__galleryConfigs[$store.chat.activeChat().model] && window.__galleryConfigs[$store.chat.activeChat().model].Icon">
<img :src="window.__galleryConfigs[$store.chat.activeChat().model].Icon" class="rounded-lg w-8 h-8 mr-2">
</template> </template>
<template x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model"> <!-- Fallback icon for initial model from server (when no active chat yet) -->
{{ if .Model }}<span> with {{.Model}}</span>{{ end }} <template x-if="(!$store.chat.activeChat() || !$store.chat.activeChat().model) && window.__galleryConfigs && window.__galleryConfigs['{{$model}}'] && window.__galleryConfigs['{{$model}}'].Icon">
<img :src="window.__galleryConfigs['{{$model}}'].Icon" class="rounded-lg w-8 h-8 mr-2">
</template> </template>
</h1> <h1 class="text-lg font-semibold text-[var(--color-text-primary)]">
<!-- Loading indicator next to model name --> Chat
<div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)] flex-shrink-0" style="display: none;"> <template x-if="$store.chat.activeChat() && $store.chat.activeChat().model">
<i class="fas fa-spinner fa-spin text-sm"></i> <span x-text="' with ' + $store.chat.activeChat().model"></span>
</template>
<template x-if="!$store.chat.activeChat() || !$store.chat.activeChat().model">
{{ if .Model }}<span> with {{.Model}}</span>{{ end }}
</template>
</h1>
<!-- Loading indicator next to model name -->
<div id="header-loading-indicator" class="ml-3 text-[var(--color-primary)]" style="display: none;">
<i class="fas fa-spinner fa-spin text-sm"></i>
</div>
</div> </div>
</div> </div>
<div class="flex items-center gap-2 flex-shrink-0"> <div class="flex items-center gap-2">
<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 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)]" class="text-[var(--color-text-secondary)] hover:text-[var(--color-primary)] transition-colors p-2 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 (bottom on mobile to avoid covering header) --> <!-- Clear Chat Alert -->
<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"
@@ -1163,7 +1161,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 max-md:top-auto max-md:bottom-4 max-md:left-4 max-md:right-4 z-50 max-w-sm pointer-events-none"> class="fixed top-20 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>
@@ -1174,8 +1172,7 @@ SOFTWARE.
</div> </div>
<!-- Chat messages area --> <!-- Chat messages area -->
<div class="flex-1 min-h-0 overflow-y-auto" id="chat"> <div class="flex-1 p-4 overflow-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">
@@ -1370,11 +1367,11 @@ SOFTWARE.
</div> </div>
</template> </template>
</div> </div>
</div>
</div> </div>
<!-- Chat Input --> <!-- Chat Input -->
<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: [] }"> <div class="p-4 border-t border-[var(--color-bg-secondary)]" 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">
@@ -1394,38 +1391,38 @@ SOFTWARE.
</template> </template>
</div> </div>
<!-- Token Usage and Context Window - responsive: two rows on mobile --> <!-- Token Usage and Context Window - Compact above input -->
<div class="mb-3 flex flex-col md:flex-row md:items-center md:justify-between gap-3 text-xs"> <div class="mb-3 flex items-center justify-between gap-4 text-xs">
<!-- Token Usage (wraps on mobile) --> <!-- Token Usage -->
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-[var(--color-text-secondary)]"> <div class="flex items-center gap-3 text-[var(--color-text-secondary)]">
<div class="flex items-center gap-1 max-md:hidden"> <div class="flex items-center gap-1">
<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 max-md:hidden"> <div class="flex items-center gap-1">
<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 md:border-l border-[var(--color-bg-secondary)] pl-0 md:pl-3"> <div class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] 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-2 md:pl-3"> <div id="tokens-per-second-container" class="flex items-center gap-1 border-l border-[var(--color-bg-secondary)] 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 (second row on mobile) --> <!-- Context Window -->
<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)] flex-shrink-0"> <div class="flex items-center gap-2 text-[var(--color-text-secondary)]">
<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)]">
@@ -1446,36 +1443,38 @@ 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-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" class="input w-full p-3 pr-16 resize-none border-0"
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>
<!-- Attachment buttons (desktop only - inside input) --> <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> type="button"
<button type="button" onclick="document.getElementById('input_audio').click()" class="hidden md:flex fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200 items-center justify-center" title="Attach an audio file" aria-label="Attach audio"></button> onclick="document.getElementById('input_image').click()"
<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> class="fa-solid fa-image text-[var(--color-text-secondary)] absolute right-12 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
title="Attach images"
></button>
<button
type="button"
onclick="document.getElementById('input_audio').click()"
class="fa-solid fa-microphone text-[var(--color-text-secondary)] absolute right-20 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
title="Attach an audio file"
></button>
<button
type="button"
onclick="document.getElementById('input_file').click()"
class="fa-solid fa-file text-[var(--color-text-secondary)] absolute right-28 top-3 text-base p-1.5 hover:text-[var(--color-primary)] transition-colors duration-200"
title="Upload text, markdown or PDF file"
></button>
<!-- 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">
@@ -1484,7 +1483,7 @@ SOFTWARE.
id="stop-button" id="stop-button"
type="button" type="button"
onclick="stopRequest()" onclick="stopRequest()"
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" class="text-lg p-2 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"
> >
@@ -1495,7 +1494,7 @@ SOFTWARE.
<button <button
id="send-button" id="send-button"
type="submit" type="submit"
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" class="text-lg p-2 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>
@@ -1537,11 +1536,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-[var(--color-bg-secondary)] rounded-lg shadow border border-[var(--color-border-subtle)]"> <div class="relative p-4 w-full max-w-2xl max-h-full bg-white rounded-lg shadow dark:bg-gray-700">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b border-[var(--color-border-subtle)] rounded-t"> <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 id="model-info-modal-title" class="text-xl font-semibold text-[var(--color-text-primary)]">{{ if $model }}{{ $model }}{{ end }}</h3> <h3 id="model-info-modal-title" class="text-xl font-semibold text-gray-900 dark:text-white">{{ if $model }}{{ $model }}{{ end }}</h3>
<button class="text-[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(); }"> <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(); }">
<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>
@@ -1554,16 +1553,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-[var(--color-text-secondary)] break-words max-w-full"></div> <div id="model-info-description" class="text-base leading-relaxed text-gray-500 dark:text-gray-400 break-words max-w-full"></div>
<hr class="border-[var(--color-border-subtle)]"> <hr>
<p class="text-sm font-semibold text-[var(--color-text-primary)]">Links</p> <p class="text-sm font-semibold text-gray-900 dark:text-white">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-[var(--color-border-subtle)] rounded-b"> <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
<button data-modal-hide="model-info-modal" class="py-2.5 px-5 ms-3 text-sm font-medium text-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(); }"> <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(); }">
Close Close
</button> </button>
</div> </div>
@@ -1875,7 +1874,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-black/50 z-40'; backdrop.className = 'modal-backdrop fixed inset-0 bg-gray-900 bg-opacity-50 dark:bg-opacity-80 z-40';
document.body.appendChild(backdrop); document.body.appendChild(backdrop);
backdrop.addEventListener('click', () => { backdrop.addEventListener('click', () => {
closeModelInfoModal(); closeModelInfoModal();
@@ -1963,7 +1962,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-black/50 z-40'; backdrop.className = 'modal-backdrop fixed inset-0 bg-gray-900 bg-opacity-50 dark:bg-opacity-80 z-40';
document.body.appendChild(backdrop); document.body.appendChild(backdrop);
backdrop.addEventListener('click', () => { backdrop.addEventListener('click', () => {
window.closeModelInfoModal(); window.closeModelInfoModal();
@@ -2066,8 +2065,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: var(--color-bg-primary) !important; background: #101827 !important;
border: 1px solid var(--color-border-subtle); border: 1px solid #1E293B;
border-radius: 6px; border-radius: 6px;
padding: 12px; padding: 12px;
margin: 0; margin: 0;
@@ -2080,7 +2079,7 @@ SOFTWARE.
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: pre; white-space: pre;
background: transparent !important; background: transparent !important;
color: var(--color-text-primary); color: #E5E7EB;
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;
@@ -2183,13 +2182,13 @@ SOFTWARE.
height: 6px; height: 6px;
} }
.chat-settings-panel::-webkit-scrollbar-track, .sidebar::-webkit-scrollbar-track,
#chat::-webkit-scrollbar-track, #chat::-webkit-scrollbar-track,
#messages::-webkit-scrollbar-track { #messages::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.chat-settings-panel::-webkit-scrollbar-thumb, .sidebar::-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);
@@ -2197,14 +2196,14 @@ SOFTWARE.
transition: background 0.2s ease; transition: background 0.2s ease;
} }
.chat-settings-panel::-webkit-scrollbar-thumb:hover, .sidebar::-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 */
.chat-settings-panel, .sidebar,
#chat, #chat,
#messages { #messages {
scrollbar-width: thin; scrollbar-width: thin;
@@ -2235,8 +2234,5 @@ SOFTWARE.
scrollbar-color: rgba(148, 163, 184, 0.15) transparent; scrollbar-color: rgba(148, 163, 184, 0.15) transparent;
} }
</style> </style>
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -2,51 +2,51 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[#101827] text-[#E5E7EB]">
<div class="app-layout"> <div class="flex flex-col min-h-screen">
{{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">
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-error)]/20 rounded-xl p-8 mb-10"> <!-- Error Section -->
<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-[var(--color-error)]"> <div class="mb-6 text-6xl text-red-400">
<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-[var(--color-text-secondary)] mb-6">{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}An unexpected error occurred{{end}}</p> <p class="text-xl text-[#94A3B8] mb-6">{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}An unexpected error occurred{{end}}</p>
<div class="flex flex-wrap justify-center gap-2"> <div class="flex flex-wrap justify-center gap-4">
<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"> <a href="./"
<i class="fas fa-home"></i> 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 mr-2"></i>
<span>Return Home</span> <span>Return Home</span>
</a> </a>
<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"> <a href="browse/"
<i class="fas fa-images"></i> 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 mr-2"></i>
<span>Browse Gallery</span> <span>Browse Gallery</span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl p-8"> <!-- Additional Information -->
<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-[var(--color-warning-light)] border border-[var(--color-warning)]/20 mb-4"> <div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-yellow-500/10 border border-yellow-500/20 mb-4">
<i class="text-[var(--color-warning)] text-2xl fa-solid fa-triangle-exclamation"></i> <i class="text-yellow-400 text-2xl fa-solid fa-triangle-exclamation"></i>
</div> </div>
<h2 class="text-2xl md:text-3xl font-semibold text-[var(--color-text-primary)] mb-4">Need help?</h2> <h2 class="text-2xl md:text-3xl font-semibold text-[#E5E7EB] mb-4">Need help?</h2>
<p class="text-lg text-[var(--color-text-secondary)] mb-6">Visit our <a class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors" href="browse">Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-[var(--color-primary)] hover:text-[var(--color-accent)] underline underline-offset-2 transition-colors">Getting started documentation</a></p> <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>
</div> </div>
</div> </div>
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -5,8 +5,8 @@
<style> <style>
body { body {
background-color: var(--color-bg-primary); background-color: #101827;
color: var(--color-text-primary); color: #E5E7EB;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
} }
.token { .token {
@@ -19,53 +19,51 @@
position: relative; position: relative;
} }
.network-card { .network-card {
background-color: var(--color-bg-secondary); background-color: #2d3748;
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: var(--color-bg-tertiary); background-color: #374151;
} }
.network-title { .network-title {
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
color: var(--color-primary); color: #63b3ed;
} }
.network-token { .network-token {
font-size: 14px; font-size: 14px;
font-style: italic; font-style: italic;
color: var(--color-text-secondary); color: #cbd5e0;
margin-bottom: 10px; margin-bottom: 10px;
word-break: break-word; word-break: break-word; /* Breaks words to prevent overflow */
overflow-wrap: break-word; overflow-wrap: break-word; /* Ensures long strings break */
white-space: pre-wrap; white-space: pre-wrap; /* Preserves whitespace for breaking */
} }
.cluster { .cluster {
margin-top: 10px; margin-top: 10px;
background-color: var(--color-bg-tertiary); background-color: #4a5568;
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: var(--color-bg-secondary); background-color: #5a6b78;
} }
.cluster-title { .cluster-title {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
color: var(--color-text-primary); color: #e2e8f0;
} }
.form-container { .form-container {
background-color: var(--color-bg-secondary); background-color: #2d3748;
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
border: 1px solid var(--color-border-subtle); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
} }
.form-control { .form-control {
margin-bottom: 15px; margin-bottom: 15px;
@@ -74,50 +72,47 @@
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 var(--color-border-subtle); border: 1px solid #4a5568;
background-color: var(--color-bg-primary); background-color: #3a4250;
color: var(--color-text-primary); color: #e2e8f0;
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: var(--color-primary); border-color: #63b3ed;
background-color: var(--color-bg-tertiary); background-color: #4a5568;
} }
button { button {
background-color: var(--color-primary); background-color: #3182ce;
color: white; color: #e2e8f0;
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: var(--color-error); color: #e53e3e;
margin-top: 5px; margin-top: 5px;
} }
.success { .success {
color: var(--color-success); color: #38a169;
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 var(--color-border-subtle); border: 5px solid rgba(255, 255, 255, 0.2);
border-radius: 50%; border-radius: 50%;
border-top-color: var(--color-primary); border-top-color: #3182ce;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto; margin: 0 auto;
} }
@@ -126,46 +121,43 @@
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: var(--color-bg-tertiary); background-color: #4a5568;
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 {
.token-box:hover { background-color: #5a6b7e;
background-color: var(--color-bg-secondary); }
} .token-text {
.token-text { overflow-wrap: break-word;
overflow-wrap: break-word; font-family: monospace;
font-family: monospace; }
} .copy-icon {
.copy-icon { position: absolute;
position: absolute; top: 10px;
top: 10px; right: 10px;
right: 10px; color: #e2e8f0;
color: var(--color-text-primary); }
}
</style> </style>
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-gray-900 text-gray-200">
<div class="app-layout"> <div class="flex flex-col min-h-screen" x-data="networkClusters()" x-init="init()">
{{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">
@@ -186,8 +178,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-[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"> <div class="warning-box bg-yellow-100 text-gray-800 mb-20 pt-5 pb-5 pr-5 pl-5 text-lg">
<i class="fa-solid fa-triangle-exclamation text-[var(--color-warning)]"></i><i class="fa-solid fa-flask text-[var(--color-warning)]"></i> <i class="fa-solid fa-triangle-exclamation"></i><i class="fa-solid fa-flask"></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.
@@ -195,8 +187,9 @@
</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 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()"> <button class="btn-primary float-right mb-2" @click="toggleForm()">
<i :class="showForm ? 'fa-solid fa-times' : 'fa-solid fa-plus'"></i> <!-- Conditional icon display -->
<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>
@@ -215,7 +208,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 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> <button @click="addNetwork" class="btn-primary"><i class="fa-solid fa-plus"></i> Add Network</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>
@@ -266,19 +259,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-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words border border-[var(--color-border-subtle)]" @click="copyToken($el.textContent)" > <code class="block bg-gray-700 text-yellow-300 p-4 rounded-lg break-words" @click="copyToken($el.textContent)" >
docker run -d --restart=always -e ADDRESS=":80" -e LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> -e LOCALAI_P2P_LOGLEVEL=debug --name local-ai -e TOKEN="<span class="token" x-text="network.token"></span>" --net host -ti localai/localai:master federated --debug 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-[var(--color-bg-primary)] text-[var(--color-warning)] p-4 rounded-lg break-words border border-[var(--color-border-subtle)]" @click="copyToken($el.textContent)" > <code class="block bg-gray-700 text-yellow-300 p-4 rounded-lg break-words" @click="copyToken($el.textContent)" >
ADDRESS=":80" LOCALAI_P2P_NETWORK_ID=<span class="token" x-text="cluster.NetworkID"></span> LOCALAI_P2P_LOGLEVEL=debug TOKEN="<span class="token" x-text="network.token"></span>" local-ai federated --debug 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>
@@ -378,8 +371,6 @@
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div> </div>
</main>
</div>
</body> </body>

View File

@@ -3,12 +3,10 @@
{{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)]"> <body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)] flex flex-col h-screen">
<div class="app-layout"> <div class="flex flex-col flex-1 overflow-hidden">
{{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">
@@ -239,8 +237,6 @@
</div> </div>
</div> </div>
</div>
</main>
</div> </div>
<script> <script>

View File

@@ -3,11 +3,9 @@
{{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="app-layout"> <div class="flex flex-col min-h-screen">
{{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">
@@ -87,19 +85,19 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-center gap-2 mb-8"> <div class="flex flex-wrap justify-center gap-4 mb-8">
<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"> <a href="/browse/" class="btn-primary">
<i class="fas fa-images"></i> <i class="fas fa-images mr-2"></i>
<span>Browse Model Gallery</span> Browse Model Gallery
</a> </a>
<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"> <a href="/import-model" class="btn-primary">
<i class="fas fa-upload"></i> <i class="fas fa-upload mr-2"></i>
<span>Import Model</span> Import Model
</a> </a>
<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"> <a href="https://localai.io/basics/getting_started/" target="_blank" class="btn-secondary">
<i class="fas fa-graduation-cap"></i> <i class="fas fa-graduation-cap mr-2"></i>
<span>Getting Started</span> Getting Started
<i class="fas fa-external-link-alt text-[10px]"></i> <i class="fas fa-external-link-alt ml-2 text-sm"></i>
</a> </a>
</div> </div>
{{ else }} {{ else }}
@@ -526,8 +524,6 @@
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
<script> <script>

View File

@@ -2,14 +2,14 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[#101827] text-[#E5E7EB]">
<div class="app-layout no-sidebar"> <div class="flex flex-col min-h-screen">
<main class="main-content">
<div class="main-content-inner"> {{template "views/partials/navbar" .}}
<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-[var(--color-bg-secondary)] border border-[var(--color-border-subtle)] rounded-xl overflow-hidden"> <div class="max-w-md w-full bg-[#1E293B] border border-[#38BDF8]/20 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-[var(--color-text-secondary)] mt-2">Please enter your access token to continue</p> <p class="text-[#94A3B8] 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-[var(--color-text-secondary)] mb-2">Access Token</label> <label for="token" class="block text-sm font-medium text-[#94A3B8] 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-[var(--color-primary)]"></i> <i class="fas fa-key text-[#38BDF8]"></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="input" 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"
style="padding-left: 3.5rem !important;" style="padding-left: 3.5rem !important;"
required required
/> />
@@ -44,17 +44,19 @@
</div> </div>
<div> <div>
<button type="submit" <button
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"> type="submit"
<i class="fas fa-sign-in-alt"></i> 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 mr-2"></i>
<span>Login</span> <span>Login</span>
</button> </button>
</div> </div>
</form> </form>
<div class="mt-8 pt-6 border-t border-[var(--color-border-subtle)] text-center text-sm text-[var(--color-text-secondary)]"> <div class="mt-8 pt-6 border-t border-[#1E293B] text-center text-sm text-[#94A3B8]">
<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-[var(--color-primary)]"></i> <i class="fas fa-shield-alt mr-2 text-[#38BDF8]"></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>
@@ -64,8 +66,6 @@
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
<script> <script>

View File

@@ -3,11 +3,9 @@
{{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="app-layout"> <div class="flex flex-col min-h-screen" x-data="indexDashboard()">
{{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;">
@@ -42,6 +40,36 @@
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>
@@ -160,17 +188,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="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"> <a href="browse" class="btn-primary text-sm py-1.5 px-3">
<i class="fas fa-images text-[10px]"></i> <i class="fas fa-images mr-1.5 text-[10px]"></i>
<span>Browse Model Gallery</span> Browse Model Gallery
</a> </a>
<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"> <a href="/import-model" class="btn-primary text-sm py-1.5 px-3">
<i class="fas fa-upload text-[10px]"></i> <i class="fas fa-upload mr-1.5 text-[10px]"></i>
<span>Import Model</span> Import Model
</a> </a>
<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"> <a href="https://localai.io/basics/getting_started/" target="_blank" class="btn-secondary text-sm py-1.5 px-3">
<i class="fas fa-book text-[10px]"></i> <i class="fas fa-book mr-1.5 text-[10px]"></i>
<span>Documentation</span> Documentation
</a> </a>
</div> </div>
@@ -198,22 +226,13 @@
{{ $modelsN := len .ModelsConfig}} {{ $modelsN := len .ModelsConfig}}
{{ $modelsN = add $modelsN (len .Models)}} {{ $modelsN = add $modelsN (len .Models)}}
<div class="mb-6"> <div class="mb-6">
<div class="flex items-center justify-between gap-3 mb-1"> <h2 class="h3 mb-1 flex items-center">
<div> <i class="fas fa-brain mr-2 text-[var(--color-primary)] text-sm"></i>
<h2 class="h3 flex items-center"> Installed Models
<i class="fas fa-brain mr-2 text-[var(--color-primary)] text-sm"></i> </h2>
Installed Models <p class="text-sm text-[var(--color-text-secondary)] mb-4">
</h2> <span class="text-[var(--color-primary)] font-medium">{{$modelsN}}</span> model{{if gt $modelsN 1}}s{{end}} ready to use
<p class="text-sm text-[var(--color-text-secondary)] mt-0.5"> </p>
<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">
@@ -318,13 +337,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 type="button" class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors" <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 type="button" class="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors" <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>
@@ -376,12 +395,12 @@
Installed Backends Installed Backends
</h2> </h2>
{{ if gt (len .InstalledBackends) 0 }} {{ if gt (len .InstalledBackends) 0 }}
<button type="button" <button
@click="reinstallAllBackends()" @click="reinstallAllBackends()"
:disabled="reinstallingAll" :disabled="reinstallingAll"
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)]" class="btn-primary text-sm py-1.5 px-3"
title="Reinstall all backends"> title="Reinstall all backends">
<i class="fas fa-arrow-rotate-right text-[10px]" :class="reinstallingAll ? 'fa-spin' : ''"></i> <i class="fas fa-arrow-rotate-right mr-1.5 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 }}
@@ -401,14 +420,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-2"> <div class="flex flex-wrap justify-center gap-3">
<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"> <a href="/browse/backends" class="btn-primary">
<i class="fas fa-cogs text-[10px]"></i> <i class="fas fa-cogs mr-2 text-xs"></i>
<span>Browse Backend Gallery</span> Browse Backend Gallery
</a> </a>
<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"> <a href="https://localai.io/backends/" target="_blank" class="btn-secondary">
<i class="fas fa-book text-[10px]"></i> <i class="fas fa-book mr-2 text-xs"></i>
<span>Documentation</span> Documentation
</a> </a>
</div> </div>
</div> </div>
@@ -481,14 +500,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 type="button" <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 type="button" <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}}">
@@ -508,6 +527,7 @@
</div> </div>
</div> </div>
{{template "views/partials/footer" .}}
</div> </div>
<script> <script>
@@ -849,10 +869,6 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -2,12 +2,10 @@
<html lang="en"> <html lang="en">
{{template "views/partials/head" .}} {{template "views/partials/head" .}}
<body class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]"> <body class="bg-[#101827] text-[#E5E7EB]">
<div class="app-layout"> <div class="flex flex-col min-h-screen" x-data="importModel()" x-init="init()">
{{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">
@@ -24,30 +22,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 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"> <button @click="toggleMode()" class="btn-secondary">
<i class="fas" :class="isAdvancedMode ? 'fa-magic' : 'fa-code'"></i> <i class="fas" :class="isAdvancedMode ? 'fa-magic mr-2' : 'fa-code mr-2'"></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-2"> <div class="flex gap-3">
<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"> <button id="validateBtn" class="btn-primary">
<i class="fas fa-check"></i> <i class="fas fa-check mr-2"></i>
<span>Validate</span> <span>Validate</span>
</button> </button>
<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"> <button id="saveBtn" class="btn-primary">
<i class="fas fa-save"></i> <i class="fas fa-save mr-2"></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 type="button" @click="submitImport()" <button @click="submitImport()"
:disabled="isSubmitting || !importUri.trim()" :disabled="isSubmitting || !importUri.trim()"
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)]"> class="btn-primary">
<i class="fas text-[10px]" :class="isSubmitting ? 'fa-spinner fa-spin' : 'fa-upload'"></i> <i class="fas" :class="isSubmitting ? 'fa-spinner fa-spin mr-2' : 'fa-upload mr-2'"></i>
<span x-text="isSubmitting ? 'Importing...' : 'Import Model'"></span> <span x-text="isSubmitting ? 'Importing...' : 'Import Model'"></span>
</button> </button>
</template> </template>
@@ -66,9 +64,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-[var(--color-text-primary)] flex items-center gap-3 mb-6"> <h2 class="text-2xl font-semibold text-[#E5E7EB] flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-lg bg-[var(--color-success-light)] flex items-center justify-center"> <div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center">
<i class="fas fa-link text-[var(--color-success)]"></i> <i class="fas fa-link text-green-400"></i>
</div> </div>
Import from URI Import from URI
</h2> </h2>
@@ -76,20 +74,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-[var(--color-text-secondary)]"> <label class="block text-sm font-medium text-[#94A3B8]">
<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-[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"> 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">
<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-[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"> 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">
<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>
@@ -102,14 +100,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-[var(--color-text-secondary)]"> <p class="mt-2 text-xs text-[#94A3B8]">
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-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"> class="flex items-center gap-2 text-sm text-[#94A3B8] hover:text-[#E5E7EB] 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>
@@ -119,34 +117,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-[var(--color-bg-primary)] border border-[var(--color-border-subtle)] rounded-lg space-y-4"> class="mt-3 p-4 bg-[#101827] border border-[#1E293B] rounded-lg space-y-4">
<!-- HuggingFace --> <!-- HuggingFace -->
<div> <div>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
<i class="fab fa-huggingface text-[var(--color-accent)]"></i> <i class="fab fa-huggingface text-purple-400"></i>
HuggingFace HuggingFace
</h4> </h4>
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<code class="text-[var(--color-success)]">huggingface://</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span> <code class="text-[#10B981]">huggingface://</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Standard HuggingFace format</p> <p class="text-[#6B7280] 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-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<code class="text-[var(--color-success)]">hf://</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span> <code class="text-[#10B981]">hf://</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Short HuggingFace format</p> <p class="text-[#6B7280] 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-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<code class="text-[var(--color-success)]">https://huggingface.co/</code><span class="text-[var(--color-text-secondary)]">TheBloke/Llama-2-7B-Chat-GGUF</span> <code class="text-[#10B981]">https://huggingface.co/</code><span class="text-[#94A3B8]">TheBloke/Llama-2-7B-Chat-GGUF</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Full HuggingFace URL</p> <p class="text-[#6B7280] mt-0.5">Full HuggingFace URL</p>
</div> </div>
</div> </div>
</div> </div>
@@ -154,16 +152,16 @@
<!-- HTTP/HTTPS --> <!-- HTTP/HTTPS -->
<div> <div>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
<i class="fas fa-globe text-[var(--color-primary)]"></i> <i class="fas fa-globe text-blue-400"></i>
HTTP/HTTPS URLs HTTP/HTTPS URLs
</h4> </h4>
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<code class="text-[var(--color-success)]">https://</code><span class="text-[var(--color-text-secondary)]">example.com/model.gguf</span> <code class="text-[#10B981]">https://</code><span class="text-[#94A3B8]">example.com/model.gguf</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Direct download from any HTTPS URL</p> <p class="text-[#6B7280] mt-0.5">Direct download from any HTTPS URL</p>
</div> </div>
</div> </div>
</div> </div>
@@ -171,23 +169,23 @@
<!-- Local Files --> <!-- Local Files -->
<div> <div>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[#E5E7EB] mb-2 flex items-center gap-2">
<i class="fas fa-file text-[var(--color-warning)]"></i> <i class="fas fa-file text-yellow-400"></i>
Local Files Local Files
</h4> </h4>
<div class="space-y-1.5 text-xs text-[var(--color-text-secondary)] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<code class="text-[var(--color-success)]">file://</code><span class="text-[var(--color-text-secondary)]">/path/to/model.gguf</span> <code class="text-[#10B981]">file://</code><span class="text-[#94A3B8]">/path/to/model.gguf</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Local file path (absolute)</p> <p class="text-[#6B7280] 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-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<span class="text-[var(--color-text-secondary)]">/path/to/model.yaml</span> <code class="text-[#94A3B8]">/path/to/model.yaml</code>
<p class="text-[var(--color-text-muted)] mt-0.5">Direct local YAML config file</p> <p class="text-[#6B7280] mt-0.5">Direct local YAML config file</p>
</div> </div>
</div> </div>
</div> </div>
@@ -195,23 +193,23 @@
<!-- OCI --> <!-- OCI -->
<div> <div>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[#E5E7EB] 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-[var(--color-text-secondary)] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<code class="text-[var(--color-success)]">oci://</code><span class="text-[var(--color-text-secondary)]">registry.example.com/model:tag</span> <code class="text-[#10B981]">oci://</code><span class="text-[#94A3B8]">registry.example.com/model:tag</span>
<p class="text-[var(--color-text-muted)] mt-0.5">OCI container registry</p> <p class="text-[#6B7280] 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-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<code class="text-[var(--color-success)]">ocifile://</code><span class="text-[var(--color-text-secondary)]">/path/to/image.tar</span> <code class="text-[#10B981]">ocifile://</code><span class="text-[#94A3B8]">/path/to/image.tar</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Local OCI tarball file</p> <p class="text-[#6B7280] mt-0.5">Local OCI tarball file</p>
</div> </div>
</div> </div>
</div> </div>
@@ -219,16 +217,16 @@
<!-- Ollama --> <!-- Ollama -->
<div> <div>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[#E5E7EB] 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-[var(--color-text-secondary)] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<code class="text-[var(--color-success)]">ollama://</code><span class="text-[var(--color-text-secondary)]">llama2:7b</span> <code class="text-[#10B981]">ollama://</code><span class="text-[#94A3B8]">llama2:7b</span>
<p class="text-[var(--color-text-muted)] mt-0.5">Ollama model format</p> <p class="text-[#6B7280] mt-0.5">Ollama model format</p>
</div> </div>
</div> </div>
</div> </div>
@@ -236,31 +234,31 @@
<!-- YAML Config Files --> <!-- YAML Config Files -->
<div> <div>
<h4 class="text-sm font-semibold text-[var(--color-text-primary)] mb-2 flex items-center gap-2"> <h4 class="text-sm font-semibold text-[#E5E7EB] 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-[var(--color-text-secondary)] font-mono pl-6"> <div class="space-y-1.5 text-xs text-[#94A3B8] font-mono pl-6">
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<span class="text-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<span class="text-[var(--color-text-secondary)]">https://example.com/model.yaml</span> <code class="text-[#94A3B8]">https://example.com/model.yaml</code>
<p class="text-[var(--color-text-muted)] mt-0.5">Remote YAML config file</p> <p class="text-[#6B7280] 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-[var(--color-success)]"></span> <span class="text-green-400"></span>
<div> <div>
<span class="text-[var(--color-text-secondary)]">file:///path/to/config.yaml</span> <code class="text-[#94A3B8]">file:///path/to/config.yaml</code>
<p class="text-[var(--color-text-muted)] mt-0.5">Local YAML config file</p> <p class="text-[#6B7280] 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-[var(--color-border-subtle)]"> <div class="pt-2 mt-3 border-t border-[#1E293B]">
<p class="text-xs text-[var(--color-text-muted)] italic"> <p class="text-xs text-[#6B7280] italic">
<i class="fas fa-lightbulb mr-1.5 text-[var(--color-warning)]"></i> <i class="fas fa-lightbulb mr-1.5 text-yellow-400"></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>
@@ -271,25 +269,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-[var(--color-text-secondary)]"> <label class="block text-sm font-medium text-gray-300">
<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-[var(--color-bg-primary)]/50 rounded-xl border border-[var(--color-border-subtle)]/50"> <div class="space-y-4 mb-6 p-4 bg-gray-900/50 rounded-xl border border-gray-700/50">
<h3 class="text-sm font-semibold text-[var(--color-text-secondary)] mb-3 flex items-center"> <h3 class="text-sm font-semibold text-gray-300 mb-3 flex items-center">
<i class="fas fa-star mr-2 text-[var(--color-warning)]"></i>Common Preferences <i class="fas fa-star mr-2 text-yellow-400"></i>Common Preferences
</h3> </h3>
<!-- Backend Selection --> <!-- Backend Selection -->
<div> <div>
<label class="block text-sm font-medium text-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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="input w-full px-4 py-2" 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"
: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>
@@ -299,30 +297,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-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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="input w-full px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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
@@ -331,39 +329,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-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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="input w-full px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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="input w-full px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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>
@@ -374,77 +372,77 @@
<input <input
x-model="commonPreferences.embeddings" x-model="commonPreferences.embeddings"
type="checkbox" type="checkbox"
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" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<span class="ml-3 text-sm font-medium text-[var(--color-text-secondary)]"> <span class="ml-3 text-sm font-medium text-gray-300">
<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-[var(--color-text-muted)]"> <p class="mt-1 ml-8 text-xs text-gray-400">
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-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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="input w-full px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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="input w-full px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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="input w-full px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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-[var(--color-text-secondary)] mb-2"> <label class="block text-sm font-medium text-gray-300 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="input w-full px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<p class="mt-1 text-xs text-[var(--color-text-muted)]"> <p class="mt-1 text-xs text-gray-400">
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>
@@ -455,13 +453,13 @@
<input <input
x-model="commonPreferences.cuda" x-model="commonPreferences.cuda"
type="checkbox" type="checkbox"
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" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<span class="ml-3 text-sm font-medium text-[var(--color-text-secondary)]"> <span class="ml-3 text-sm font-medium text-gray-300">
<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-[var(--color-text-muted)]"> <p class="mt-1 ml-8 text-xs text-gray-400">
Enable CUDA support for GPU acceleration with diffusers backend. Enable CUDA support for GPU acceleration with diffusers backend.
</p> </p>
</div> </div>
@@ -470,12 +468,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-[var(--color-text-secondary)]"> <label class="block text-sm font-medium text-gray-300">
<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-[var(--color-success-light)] hover:bg-[var(--color-success)]/30 text-[var(--color-success)] border border-[var(--color-success)]/30 transition-all"> 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">
<i class="fas fa-plus mr-1"></i>Add Custom <i class="fas fa-plus mr-1"></i>Add Custom
</button> </button>
</div> </div>
@@ -487,24 +485,24 @@
x-model="pref.key" x-model="pref.key"
type="text" type="text"
placeholder="Key" placeholder="Key"
class="flex-1 input px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<span class="text-[var(--color-text-secondary)]">:</span> <span class="text-gray-400">:</span>
<input <input
x-model="pref.value" x-model="pref.value"
type="text" type="text"
placeholder="Value" placeholder="Value"
class="flex-1 input px-4 py-2" 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"
:disabled="isSubmitting"> :disabled="isSubmitting">
<button @click="removePreference(index)" <button @click="removePreference(index)"
:disabled="isSubmitting" :disabled="isSubmitting"
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"> 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">
<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-[var(--color-text-muted)]"> <p class="mt-2 text-xs text-gray-400">
Add custom key-value pairs for advanced configuration Add custom key-value pairs for advanced configuration
</p> </p>
</div> </div>
@@ -517,19 +515,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-[var(--color-bg-secondary)] border border-[var(--color-accent)]/20 rounded-xl overflow-hidden h-[calc(100vh-250px)]"> class="bg-[#1E293B] border border-[#8B5CF6]/20 rounded-xl overflow-hidden h-[calc(100vh-250px)]">
<div class="sticky top-0 bg-[var(--color-bg-secondary)] border-b border-[var(--color-border-subtle)] p-6 flex items-center justify-between z-10"> <div class="sticky top-0 bg-[#1E293B] border-b border-[#101827] p-6 flex items-center justify-between z-10">
<h2 class="text-xl font-semibold text-[var(--color-text-primary)] flex items-center gap-3"> <h2 class="text-xl font-semibold text-[#E5E7EB] 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-[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"> <button id="formatYamlBtn" class="text-[#94A3B8] hover:text-[#E5E7EB] text-sm px-3 py-1.5 rounded-lg hover:bg-[#101827] 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-[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"> <button id="copyYamlBtn" class="text-[#94A3B8] hover:text-[#E5E7EB] text-sm px-3 py-1.5 rounded-lg hover:bg-[#101827] 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>
@@ -539,6 +537,8 @@
</div> </div>
</div> </div>
</div> </div>
{{template "views/partials/footer" .}}
</div> </div>
<!-- Include JS-YAML library --> <!-- Include JS-YAML library -->
@@ -553,8 +553,8 @@
<style> <style>
/* Enhanced CodeMirror styling */ /* Enhanced CodeMirror styling */
.CodeMirror { .CodeMirror {
background: var(--color-bg-primary) !important; background: linear-gradient(135deg, #111827 0%, #1f2937 100%) !important;
color: var(--color-text-primary) !important; color: #e5e7eb !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 +564,7 @@
} }
.CodeMirror-cursor { .CodeMirror-cursor {
border-left: 2px solid var(--color-accent) !important; border-left: 2px solid #a78bfa !important;
animation: blink 1s infinite; animation: blink 1s infinite;
} }
@@ -574,20 +574,20 @@
} }
.CodeMirror-gutters { .CodeMirror-gutters {
background: var(--color-bg-secondary) !important; background: linear-gradient(135deg, #1f2937 0%, #374151 100%) !important;
border-right: 1px solid var(--color-border-subtle) !important; border-right: 1px solid rgba(75, 85, 99, 0.5) !important;
color: var(--color-text-secondary) !important; color: #9ca3af !important;
padding-right: 8px !important; padding-right: 8px !important;
} }
.CodeMirror-linenumber { .CodeMirror-linenumber {
color: var(--color-text-muted) !important; color: #6b7280 !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: var(--color-accent-light) !important; background: rgba(139, 92, 246, 0.1) !important;
} }
.CodeMirror-selected { .CodeMirror-selected {
@@ -614,27 +614,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: var(--color-text-muted) !important; font-style: italic !important; } .cm-comment { color: #6b7280 !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: var(--color-text-secondary) !important; } .cm-bracket { color: #d1d5db !important; }
.cm-punctuation { color: var(--color-text-secondary) !important; } .cm-punctuation { color: #d1d5db !important; }
.cm-quote { color: #10b981 !important; } .cm-quote { color: #10b981 !important; }
.cm-meta { color: var(--color-text-muted) !important; } .cm-meta { color: #6b7280 !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: var(--color-bg-secondary) !important; background: #1f2937 !important;
} }
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar { .CodeMirror-vscrollbar, .CodeMirror-hscrollbar {
background: var(--color-bg-secondary) !important; background: #1f2937 !important;
} }
.CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar { .CodeMirror-vscrollbar::-webkit-scrollbar, .CodeMirror-hscrollbar::-webkit-scrollbar {
@@ -643,17 +643,17 @@
} }
.CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track { .CodeMirror-vscrollbar::-webkit-scrollbar-track, .CodeMirror-hscrollbar::-webkit-scrollbar-track {
background: var(--color-bg-secondary); background: #1f2937;
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: var(--color-text-muted); background: linear-gradient(135deg, #6b7280 0%, #9ca3af 100%);
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: var(--color-text-secondary); background: linear-gradient(135deg, #9ca3af 0%, #d1d5db 100%);
} }
/* Focus ring styling */ /* Focus ring styling */
@@ -682,27 +682,27 @@
} }
.alert-success { .alert-success {
background: var(--color-success-light); background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
border-color: var(--color-success); border-color: rgba(16, 185, 129, 0.3);
color: var(--color-success); color: #10b981;
} }
.alert-error { .alert-error {
background: var(--color-error-light); background: linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(220, 38, 38, 0.1) 100%);
border-color: var(--color-error); border-color: rgba(239, 68, 68, 0.3);
color: var(--color-error); color: #ef4444;
} }
.alert-warning { .alert-warning {
background: var(--color-warning-light); background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(217, 119, 6, 0.1) 100%);
border-color: var(--color-warning); border-color: rgba(245, 158, 11, 0.3);
color: var(--color-warning); color: #f59e0b;
} }
.alert-info { .alert-info {
background: var(--color-info-light); background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%);
border-color: var(--color-info); border-color: rgba(59, 130, 246, 0.3);
color: var(--color-info); color: #3b82f6;
} }
</style> </style>
@@ -1180,10 +1180,5 @@ parameters:
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -3,11 +3,9 @@
{{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="app-layout"> <div class="flex flex-col min-h-screen" x-data="modelsGallery()">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="modelsGallery()">
<!-- 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;">
@@ -61,14 +59,14 @@
<span class="font-semibold text-purple-300" x-text="repositories.length"></span> <span class="font-semibold text-purple-300" x-text="repositories.length"></span>
<span class="text-[var(--color-text-secondary)] ml-1">repositories</span> <span class="text-[var(--color-text-secondary)] ml-1">repositories</span>
</div> </div>
<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"> <a href="/import-model" class="btn-primary">
<i class="fas fa-upload"></i> <i class="fas fa-upload mr-2"></i>
<span>Import Model</span> <span>Import Model</span>
</a> </a>
<a href="https://localai.io/models/" 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"> <a href="https://localai.io/models/" target="_blank" class="btn-secondary">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle mr-2"></i>
<span>Documentation</span> <span>Documentation</span>
<i class="fas fa-external-link-alt text-[10px]"></i> <i class="fas fa-external-link-alt ml-2 text-xs"></i>
</a> </a>
</div> </div>
</div> </div>
@@ -192,75 +190,75 @@
</div> </div>
<!-- Table View --> <!-- Table View -->
<div x-show="models.length > 0" class="bg-[var(--color-bg-secondary)] rounded-2xl border border-[var(--color-border-subtle)] overflow-hidden shadow-xl backdrop-blur-sm"> <div x-show="models.length > 0" class="bg-[#1E293B] rounded-2xl border border-[#38BDF8]/20 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-[var(--color-primary-light)] border-b border-[var(--color-border-subtle)]"> <tr class="bg-gradient-to-r from-[#38BDF8]/20 to-[#8B5CF6]/20 border-b border-[#38BDF8]/30">
<th class="px-6 py-4 text-left text-xs font-semibold text-[var(--color-text-primary)] uppercase tracking-wider">Icon</th> <th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Icon</th>
<th @click="setSort('name')" <th @click="setSort('name')"
:class="sortBy === 'name' ? 'bg-[var(--color-primary-light)]' : ''" :class="sortBy === 'name' ? 'bg-[#38BDF8]/20' : ''"
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"> class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 transition-colors">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span>Model Name</span> <span>Model 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-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'" :class="sortBy === 'name' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
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-[var(--color-text-primary)] uppercase tracking-wider">Description</th> <th class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Description</th>
<th @click="setSort('repository')" <th @click="setSort('repository')"
:class="sortBy === 'repository' ? 'bg-[var(--color-primary-light)]' : ''" :class="sortBy === 'repository' ? 'bg-[#38BDF8]/20' : ''"
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"> class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 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-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'" :class="sortBy === 'repository' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
class="text-xs"></i> class="text-xs"></i>
</div> </div>
</th> </th>
<th @click="setSort('license')" <th @click="setSort('license')"
:class="sortBy === 'license' ? 'bg-[var(--color-primary-light)]' : ''" :class="sortBy === 'license' ? 'bg-[#38BDF8]/20' : ''"
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"> class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 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-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'" :class="sortBy === 'license' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
class="text-xs"></i> class="text-xs"></i>
</div> </div>
</th> </th>
<th @click="setSort('status')" <th @click="setSort('status')"
:class="sortBy === 'status' ? 'bg-[var(--color-primary-light)]' : ''" :class="sortBy === 'status' ? 'bg-[#38BDF8]/20' : ''"
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"> class="px-6 py-4 text-left text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider cursor-pointer hover:bg-[#38BDF8]/10 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-[var(--color-primary)]' : 'text-[var(--color-text-secondary)]'" :class="sortBy === 'status' ? 'text-[#38BDF8]' : 'text-[#94A3B8]'"
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-[var(--color-text-primary)] uppercase tracking-wider">Actions</th> <th class="px-6 py-4 text-right text-xs font-semibold text-[#E5E7EB] uppercase tracking-wider">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-[var(--color-border-subtle)]"> <tbody class="divide-y divide-[#38BDF8]/20">
<template x-for="model in models" :key="model.id"> <template x-for="model in models" :key="model.id">
<tr class="hover:bg-[var(--color-bg-primary)] transition-colors duration-200"> <tr class="hover:bg-[#38BDF8]/10 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-[var(--color-border-subtle)] flex items-center justify-center bg-[var(--color-bg-primary)]"> <div class="w-12 h-12 rounded-lg border border-[#38BDF8]/30 flex items-center justify-center bg-[#101827]">
<img x-show="model.icon" <img x-show="model.icon"
:src="model.icon" :src="model.icon"
class="w-full h-full object-cover rounded-lg" class="w-full h-full object-cover rounded-lg"
loading="lazy" loading="lazy"
:alt="model.name"> :alt="model.name">
<i x-show="!model.icon" class="fas fa-brain text-xl text-[var(--color-accent)]"></i> <i x-show="!model.icon" class="fas fa-brain text-xl text-[#8B5CF6]"></i>
</div> </div>
</td> </td>
<!-- Model Name --> <!-- Model Name -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-sm font-semibold text-[var(--color-text-primary)]" x-text="model.name"></span> <span class="text-sm font-semibold text-[#E5E7EB]" x-text="model.name"></span>
<div x-show="model.trustRemoteCode" class="mt-1"> <div x-show="model.trustRemoteCode" class="mt-1">
<span class="inline-flex items-center text-xs px-2 py-1 rounded bg-red-500/20 text-red-300 border border-red-500/30"> <span class="inline-flex items-center text-xs px-2 py-1 rounded bg-red-500/20 text-red-300 border border-red-500/30">
<i class="fa-solid fa-circle-exclamation mr-1"></i> <i class="fa-solid fa-circle-exclamation mr-1"></i>
@@ -272,12 +270,12 @@
<!-- Description --> <!-- Description -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="text-sm text-[var(--color-text-secondary)] max-w-xs truncate" x-text="model.description" :title="model.description"></div> <div class="text-sm text-[#94A3B8] max-w-xs truncate" x-text="model.description" :title="model.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-[var(--color-primary-light)] text-[var(--color-text-primary)] border border-[var(--color-primary-border)]"> <span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#38BDF8]/10 text-[#E5E7EB] border border-[#38BDF8]/30">
<i class="fa-brands fa-git-alt mr-1"></i> <i class="fa-brands fa-git-alt mr-1"></i>
<span x-text="model.gallery"></span> <span x-text="model.gallery"></span>
</span> </span>
@@ -285,21 +283,21 @@
<!-- License --> <!-- License -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<span x-show="model.license" class="inline-flex items-center text-xs px-2 py-1 rounded bg-[var(--color-accent-light)] text-[var(--color-text-primary)] border border-[var(--color-accent)]/30"> <span x-show="model.license" class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#8B5CF6]/10 text-[#E5E7EB] border border-[#8B5CF6]/30">
<i class="fas fa-book mr-1"></i> <i class="fas fa-book mr-1"></i>
<span x-text="model.license"></span> <span x-text="model.license"></span>
</span> </span>
<span x-show="!model.license" class="text-xs text-[var(--color-text-secondary)]">-</span> <span x-show="!model.license" class="text-xs text-[#94A3B8]">-</span>
</td> </td>
<!-- Status --> <!-- Status -->
<td class="px-6 py-4"> <td class="px-6 py-4">
<!-- Processing State --> <!-- Processing State -->
<div x-show="model.processing" class="min-w-[200px]"> <div x-show="model.processing" class="min-w-[200px]">
<div class="text-xs font-medium text-[var(--color-text-primary)] mb-1"> <div class="text-xs font-medium text-[#E5E7EB] mb-1">
<span x-text="model.isDeletion ? 'Deleting...' : 'Installing...'"></span> <span x-text="model.isDeletion ? 'Deleting...' : 'Installing...'"></span>
</div> </div>
<div x-show="(jobProgress[model.jobID] || 0) === 0" class="text-xs text-[var(--color-primary)]"> <div x-show="(jobProgress[model.jobID] || 0) === 0" class="text-xs text-[#38BDF8]">
<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">
@@ -309,7 +307,7 @@
<!-- Installed State --> <!-- Installed State -->
<div x-show="!model.processing && model.installed"> <div x-show="!model.processing && model.installed">
<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"> <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">
<i class="fas fa-check-circle mr-1"></i> <i class="fas fa-check-circle mr-1"></i>
Installed Installed
</span> </span>
@@ -317,7 +315,7 @@
<!-- Not Installed State --> <!-- Not Installed State -->
<div x-show="!model.processing && !model.installed"> <div x-show="!model.processing && !model.installed">
<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)]"> <span class="inline-flex items-center text-xs px-2 py-1 rounded bg-[#1E293B] text-[#94A3B8] border border-[#38BDF8]/30">
<i class="fas fa-circle mr-1"></i> <i class="fas fa-circle mr-1"></i>
Not Installed Not Installed
</span> </span>
@@ -329,7 +327,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(model)" <button @click="openModal(model)"
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)]" 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"
title="View details"> title="View details">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
</button> </button>
@@ -338,12 +336,12 @@
<template x-if="!model.processing && model.installed"> <template x-if="!model.processing && model.installed">
<div class="flex gap-2"> <div class="flex gap-2">
<button @click="reinstallModel(model.id)" <button @click="reinstallModel(model.id)"
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" 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"
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="deleteModel(model.id)" <button @click="deleteModel(model.id)"
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" 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"
title="Delete"> title="Delete">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
</button> </button>
@@ -354,12 +352,12 @@
<template x-if="!model.processing && !model.installed"> <template x-if="!model.processing && !model.installed">
<div class="flex gap-2"> <div class="flex gap-2">
<button @click="getConfig(model.id)" <button @click="getConfig(model.id)"
class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-accent-light)] hover:bg-[var(--color-accent)]/30 text-xs font-medium text-[var(--color-text-primary)] transition duration-200 border border-[var(--color-accent)]/30" class="inline-flex items-center px-3 py-1.5 rounded-lg bg-[var(--color-accent)]/20 hover:bg-[var(--color-accent)]/40 text-xs font-medium text-[var(--color-text-primary)] transition duration-200 border border-[var(--color-accent-border)]/30"
title="Get config"> title="Get config">
<i class="fa-solid fa-file-code"></i> <i class="fa-solid fa-file-code"></i>
</button> </button>
<button @click="installModel(model.id)" <button @click="installModel(model.id)"
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" 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"
title="Install"> title="Install">
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
</button> </button>
@@ -378,15 +376,15 @@
<div x-show="selectedModel" <div x-show="selectedModel"
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-black/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-gray-900/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-[var(--color-bg-secondary)] rounded-lg shadow h-full flex flex-col border border-[var(--color-border-subtle)]"> <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 h-full flex flex-col">
<!-- Modal Header --> <!-- Modal Header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b border-[var(--color-border-subtle)] rounded-t"> <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-[var(--color-text-primary)]" x-text="selectedModel?.name"></h3> <h3 class="text-xl font-semibold text-gray-900 dark:text-white" x-text="selectedModel?.name"></h3>
<button @click="closeModal()" <button @click="closeModal()"
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"> class="text-[var(--color-text-secondary)] bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
<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>
@@ -396,23 +394,23 @@
<!-- 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-[var(--color-border-subtle)] flex items-center justify-center bg-[var(--color-bg-primary)] mt-3"> <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">
<img x-show="selectedModel?.icon" <img x-show="selectedModel?.icon"
:src="selectedModel?.icon" :src="selectedModel?.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="!selectedModel?.icon" class="fas fa-brain text-6xl text-[var(--color-text-muted)]"></i> <i x-show="!selectedModel?.icon" class="fas fa-brain text-6xl text-[var(--color-text-secondary)] dark:text-[var(--color-text-muted)]"></i>
</div> </div>
</div> </div>
<div class="text-base leading-relaxed text-[var(--color-text-secondary)] break-words max-w-full markdown-content" x-html="renderMarkdown(selectedModel?.description)"></div> <div class="text-base leading-relaxed text-[var(--color-text-muted)] dark:text-[var(--color-text-secondary)] break-words max-w-full markdown-content" x-html="renderMarkdown(selectedModel?.description)"></div>
<hr> <hr>
<template x-if="selectedModel?.urls && selectedModel.urls.length > 0"> <template x-if="selectedModel?.urls && selectedModel.urls.length > 0">
<div> <div>
<p class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Links</p> <p class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Links</p>
<ul> <ul>
<template x-for="url in selectedModel.urls" :key="url"> <template x-for="url in selectedModel.urls" :key="url">
<li> <li>
<a :href="url" target="_blank" class="text-base leading-relaxed text-[var(--color-text-secondary)] hover:text-[var(--color-primary)]"> <a :href="url" target="_blank" class="text-base leading-relaxed text-[var(--color-text-muted)] dark:text-[var(--color-text-secondary)] hover:text-[var(--color-primary)]">
<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>
@@ -423,11 +421,11 @@
</template> </template>
<template x-if="selectedModel?.additionalFiles && selectedModel.additionalFiles.length > 0"> <template x-if="selectedModel?.additionalFiles && selectedModel.additionalFiles.length > 0">
<div> <div>
<p class="text-sm font-semibold text-[var(--color-text-primary)] mb-2">Files</p> <p class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Files</p>
<ul> <ul>
<template x-for="file in selectedModel.additionalFiles" :key="file"> <template x-for="file in selectedModel.additionalFiles" :key="file">
<li class="mb-0"> <li class="mb-0">
<p class="text-base leading-tight text-[var(--color-text-secondary)]"> <p class="text-base leading-tight text-[var(--color-text-muted)] dark:text-[var(--color-text-secondary)]">
<i class="fas fa-file pr-2"></i> <i class="fas fa-file pr-2"></i>
<span x-text="file.filename"></span> <span x-text="file.filename"></span>
</p> </p>
@@ -438,11 +436,11 @@
</template> </template>
<template x-if="selectedModel?.tags && selectedModel.tags.length > 0"> <template x-if="selectedModel?.tags && selectedModel.tags.length > 0">
<div> <div>
<p class="text-sm mb-3 font-semibold text-[var(--color-text-primary)]">Tags</p> <p class="text-sm mb-3 font-semibold text-gray-900 dark:text-white">Tags</p>
<div class="flex flex-row flex-wrap content-center"> <div class="flex flex-row flex-wrap content-center">
<template x-for="tag in selectedModel.tags" :key="tag"> <template x-for="tag in selectedModel.tags" :key="tag">
<a :href="'browse?term=' + tag" <a :href="'browse?term=' + tag"
class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-[var(--color-bg-primary)] text-[var(--color-text-secondary)] border border-[var(--color-border-subtle)] hover:bg-[var(--color-primary-light)] hover:text-[var(--color-text-primary)] transition duration-200 ease-in-out mr-2 mb-2"> class="inline-flex items-center text-xs px-3 py-1 rounded-full bg-gray-700/60 text-gray-300 border border-gray-600/50 hover:bg-gray-600 hover:text-gray-100 transition duration-200 ease-in-out mr-2 mb-2">
<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>
</a> </a>
@@ -452,9 +450,9 @@
</template> </template>
</div> </div>
<!-- Modal Footer --> <!-- Modal Footer -->
<div class="flex items-center p-4 md:p-5 border-t border-[var(--color-border-subtle)] rounded-b"> <div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600">
<button type="button" @click="closeModal()" <button @click="closeModal()"
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="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-[var(--color-text-secondary)] dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
Close Close
</button> </button>
</div> </div>
@@ -465,29 +463,30 @@
<!-- 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-[var(--color-bg-secondary)] rounded-2xl p-4 backdrop-blur-sm border border-[var(--color-border-subtle)]"> <div class="flex items-center gap-4 bg-gray-800/60 rounded-2xl p-4 backdrop-blur-sm border border-gray-700/50">
<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-[var(--color-bg-primary)] hover:bg-[var(--color-primary)] text-[var(--color-text-secondary)] hover:text-white rounded-lg transition-colors"> class="flex items-center justify-center h-12 w-12 bg-[var(--color-bg-secondary)] hover:bg-indigo-600 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-[var(--color-text-primary)] text-sm font-medium px-4"> <div class="text-gray-300 text-sm font-medium px-4">
<span class="text-[var(--color-text-secondary)]">Page</span> <span class="text-[var(--color-text-secondary)]">Page</span>
<span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="currentPage"></span> <span class="text-white font-bold text-lg mx-2" x-text="currentPage"></span>
<span class="text-[var(--color-text-secondary)]">of</span> <span class="text-[var(--color-text-secondary)]">of</span>
<span class="text-[var(--color-text-primary)] font-bold text-lg mx-2" x-text="totalPages"></span> <span class="text-white 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-[var(--color-bg-primary)] hover:bg-[var(--color-primary)] text-[var(--color-text-secondary)] hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110"> class="group flex items-center justify-center h-12 w-12 bg-gray-700/80 hover:bg-indigo-600 text-gray-300 hover:text-white rounded-xl shadow-lg transition-all duration-300 ease-in-out transform hover:scale-110">
<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>
@@ -527,16 +526,16 @@
/* Table progress bar styling */ /* Table progress bar styling */
.progress-table { .progress-table {
background: var(--color-primary-light); background: linear-gradient(135deg, rgba(56, 189, 248, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
border-radius: 0.25rem; border-radius: 0.25rem;
border: 1px solid var(--color-primary-border); border: 1px solid rgba(56, 189, 248, 0.3);
height: 6px; height: 6px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
} }
.progress-bar-table { .progress-bar-table {
background: var(--gradient-primary); background: linear-gradient(135deg, #38BDF8 0%, #8B5CF6 100%);
height: 100%; height: 100%;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
@@ -545,7 +544,6 @@
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 {
@@ -833,10 +831,5 @@ function modelsGallery() {
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,9 @@
{{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="app-layout"> <div class="flex flex-col min-h-screen" x-data="settingsDashboard()">
{{template "views/partials/navbar" .}}
{{template "views/partials/navbar" .}}
<main class="main-content">
<div class="main-content-inner" x-data="settingsDashboard()">
<!-- 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;">
@@ -36,22 +34,22 @@
<div class="container mx-auto px-4 py-6 flex-grow max-w-4xl"> <div class="container mx-auto px-4 py-6 flex-grow max-w-4xl">
<!-- Header --> <!-- Header -->
<div class="mb-4"> <div class="mb-6">
<h1 class="h2 mb-2">Application Settings</h1> <div class="flex items-center justify-between mb-2">
<h1 class="h2">
Application Settings
</h1>
<a href="/manage"
class="inline-flex items-center text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors">
<i class="fas fa-arrow-left mr-2 text-sm"></i>
<span class="text-sm">Back to Manage</span>
</a>
</div>
<p class="text-sm text-[var(--color-text-secondary)]">Configure watchdog and backend request settings</p> <p class="text-sm text-[var(--color-text-secondary)]">Configure watchdog and backend request settings</p>
</div> </div>
<!-- Settings Form --> <!-- Settings Form -->
<form @submit.prevent="saveSettings()" class="space-y-6"> <form @submit.prevent="saveSettings()" class="space-y-6">
<!-- Sticky Save bar -->
<div class="sticky top-0 z-10 -mx-4 px-4 py-3 -mt-2 mb-2 bg-[var(--color-bg-primary)] border-b border-[var(--color-border-subtle)] flex justify-end">
<button type="submit"
:disabled="saving"
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 fa-save text-[10px]" :class="saving ? 'fa-spin fa-spinner' : ''"></i>
<span x-text="saving ? 'Saving...' : 'Save Settings'"></span>
</button>
</div>
<!-- Watchdog Settings Section --> <!-- Watchdog Settings Section -->
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded-lg p-6"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary-border)]/20 rounded-lg p-6">
<h2 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center"> <h2 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
@@ -73,7 +71,7 @@
<input type="checkbox" x-model="settings.watchdog_enabled" <input type="checkbox" x-model="settings.watchdog_enabled"
@change="updateWatchdogEnabled()" @change="updateWatchdogEnabled()"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label> </label>
</div> </div>
@@ -87,7 +85,7 @@
<input type="checkbox" x-model="settings.watchdog_idle_enabled" <input type="checkbox" x-model="settings.watchdog_idle_enabled"
:disabled="!settings.watchdog_enabled" :disabled="!settings.watchdog_enabled"
class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''"> class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label> </label>
</div> </div>
@@ -112,7 +110,7 @@
<input type="checkbox" x-model="settings.watchdog_busy_enabled" <input type="checkbox" x-model="settings.watchdog_busy_enabled"
:disabled="!settings.watchdog_enabled" :disabled="!settings.watchdog_enabled"
class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''"> class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label> </label>
</div> </div>
@@ -148,7 +146,7 @@
<input type="checkbox" x-model="settings.force_eviction_when_busy" <input type="checkbox" x-model="settings.force_eviction_when_busy"
:disabled="!settings.watchdog_enabled" :disabled="!settings.watchdog_enabled"
class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''"> class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label> </label>
</div> </div>
@@ -228,7 +226,7 @@
<input type="checkbox" x-model="settings.memory_reclaimer_enabled" <input type="checkbox" x-model="settings.memory_reclaimer_enabled"
:disabled="!settings.watchdog_enabled" :disabled="!settings.watchdog_enabled"
class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''"> class="sr-only peer" :class="!settings.watchdog_enabled ? 'opacity-50' : ''">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label> </label>
</div> </div>
@@ -281,7 +279,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.parallel_backend_requests" <input type="checkbox" x-model="settings.parallel_backend_requests"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
</label> </label>
</div> </div>
</div> </div>
@@ -327,7 +325,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.f16" <input type="checkbox" x-model="settings.f16"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
</label> </label>
</div> </div>
@@ -340,7 +338,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.debug" <input type="checkbox" x-model="settings.debug"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
</label> </label>
</div> </div>
@@ -353,7 +351,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.enable_tracing" <input type="checkbox" x-model="settings.enable_tracing"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-success-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-success)]"></div>
</label> </label>
</div> </div>
@@ -392,7 +390,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.cors" <input type="checkbox" x-model="settings.cors"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-warning-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-warning)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-warning-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-warning)]"></div>
</label> </label>
</div> </div>
@@ -414,7 +412,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.csrf" <input type="checkbox" x-model="settings.csrf"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-warning-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-warning)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-warning-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-warning)]"></div>
</label> </label>
</div> </div>
</div> </div>
@@ -458,7 +456,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.federated" <input type="checkbox" x-model="settings.federated"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
</label> </label>
</div> </div>
</div> </div>
@@ -553,7 +551,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.autoload_galleries" <input type="checkbox" x-model="settings.autoload_galleries"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
</label> </label>
</div> </div>
@@ -566,7 +564,7 @@
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" x-model="settings.autoload_backend_galleries" <input type="checkbox" x-model="settings.autoload_backend_galleries"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-accent)]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-accent)]"></div>
</label> </label>
</div> </div>
@@ -591,9 +589,34 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Source Info -->
<div class="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4" x-show="sourceInfo">
<div class="flex items-start">
<i class="fas fa-info-circle text-yellow-400 mr-2 mt-0.5"></i>
<div class="flex-1">
<p class="text-sm text-yellow-300 font-medium mb-1">Configuration Source</p>
<p class="text-xs text-yellow-200" x-text="'Settings are currently loaded from: ' + sourceInfo"></p>
<p class="text-xs text-yellow-200 mt-1" x-show="sourceInfo === 'env'">
Environment variables take precedence. To modify settings via the UI, unset the relevant environment variables first.
</p>
</div>
</div>
</div>
<!-- Save Button -->
<div class="flex justify-end">
<button type="submit"
:disabled="saving"
class="btn-primary">
<i class="fas fa-save mr-2" :class="saving ? 'fa-spin fa-spinner' : ''"></i>
<span x-text="saving ? 'Saving...' : 'Save Settings'"></span>
</button>
</div>
</form> </form>
</div> </div>
{{template "views/partials/footer" .}}
</div> </div>
<script> <script>
@@ -635,6 +658,7 @@ function settingsDashboard() {
agent_job_retention_days: 30, agent_job_retention_days: 30,
open_responses_store_ttl: '0' open_responses_store_ttl: '0'
}, },
sourceInfo: '',
saving: false, saving: false,
init() { init() {
@@ -682,6 +706,7 @@ function settingsDashboard() {
agent_job_retention_days: data.agent_job_retention_days || 30, agent_job_retention_days: data.agent_job_retention_days || 30,
open_responses_store_ttl: data.open_responses_store_ttl || '0' open_responses_store_ttl: data.open_responses_store_ttl || '0'
}; };
this.sourceInfo = data.source || 'default';
} else { } else {
this.addNotification('Failed to load settings: ' + (data.error || 'Unknown error'), 'error'); this.addNotification('Failed to load settings: ' + (data.error || 'Unknown error'), 'error');
} }
@@ -853,6 +878,7 @@ function settingsDashboard() {
if (response.ok && data.success) { if (response.ok && data.success) {
this.addNotification('Settings saved successfully!', 'success'); this.addNotification('Settings saved successfully!', 'success');
// Reload settings to get updated source info
setTimeout(() => this.loadSettings(), 1000); setTimeout(() => this.loadSettings(), 1000);
} else { } else {
this.addNotification('Failed to save settings: ' + (data.error || 'Unknown error'), 'error'); this.addNotification('Failed to save settings: ' + (data.error || 'Unknown error'), 'error');
@@ -896,10 +922,6 @@ function resourceStatus() {
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -4,11 +4,9 @@
<script defer src="static/sound.js"></script> <script defer src="static/sound.js"></script>
<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="app-layout"> <div class="flex flex-col min-h-screen">
{{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">
<!-- Hero Section --> <!-- Hero Section -->
<div class="hero-section"> <div class="hero-section">
@@ -149,7 +147,7 @@
</div> </div>
<div class="pt-4"> <div class="pt-4">
<button type="submit" id="generate-btn" 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"> <button type="submit" id="generate-btn" class="btn-primary w-full py-3 flex items-center justify-center gap-2">
<i class="fas fa-music"></i> <i class="fas fa-music"></i>
<span>Generate sound</span> <span>Generate sound</span>
</button> </button>
@@ -171,8 +169,6 @@
</div> </div>
{{template "views/partials/footer" .}} {{template "views/partials/footer" .}}
</div>
</main>
</div> </div>
<script> <script>

View File

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

View File

@@ -3,11 +3,9 @@
{{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="app-layout"> <div class="flex flex-col min-h-screen" x-data="tracesApp()" x-init="init()">
{{template "views/partials/navbar" .}}
<main class="main-content"> {{template "views/partials/navbar" .}}
<div class="main-content-inner" x-data="tracesApp()" x-init="init()">
<!-- 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,55 +38,29 @@
<div class="hero-section"> <div class="hero-section">
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title"> <h1 class="hero-title">
Traces API Traces
</h1> </h1>
<p class="hero-subtitle">View logged API requests, responses, and backend operations</p> <p class="hero-subtitle">View logged API requests and responses</p>
<div class="flex flex-wrap justify-center gap-2" x-show="activeTab === 'api'"> <div class="flex flex-wrap justify-center gap-3">
<button type="button" @click="clearTraces()" 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"> <button @click="clearTraces()" class="btn-secondary text-sm py-1.5 px-3">
<i class="fas fa-trash text-[10px]"></i> <i class="fas fa-trash mr-1.5 text-[10px]"></i>
<span>Clear Traces</span> <span>Clear Traces</span>
</button> </button>
<a href="/api/traces" download="traces.json" 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"> <a href="/api/traces" download="traces.json" class="btn-secondary text-sm py-1.5 px-3">
<i class="fas fa-download text-[10px]"></i> <i class="fas fa-download mr-1.5 text-[10px]"></i>
<span>Export Traces</span> <span>Export Traces</span>
</a> </a>
</div> </div>
<div class="flex flex-wrap justify-center gap-2" x-show="activeTab === 'backend'">
<button type="button" @click="clearBackendTraces()" 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-trash text-[10px]"></i>
<span>Clear Backend Traces</span>
</button>
<a href="/api/backend-traces" download="backend-traces.json" 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-download text-[10px]"></i>
<span>Export Backend Traces</span>
</a>
</div>
</div> </div>
</div> </div>
<!-- Tab Bar -->
<div class="flex border-b border-[var(--color-border-subtle)] mb-6">
<button @click="switchTab('api')"
:class="activeTab === 'api' ? 'border-[var(--color-primary)] text-[var(--color-primary)]' : 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'"
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors">
<i class="fas fa-exchange-alt mr-1.5 text-xs"></i>API Traces
<span class="ml-1 text-xs opacity-70" x-text="'(' + traces.length + ')'"></span>
</button>
<button @click="switchTab('backend')"
:class="activeTab === 'backend' ? 'border-[var(--color-primary)] text-[var(--color-primary)]' : 'border-transparent text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'"
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors">
<i class="fas fa-cogs mr-1.5 text-xs"></i>Backend Traces
<span class="ml-1 text-xs opacity-70" x-text="'(' + backendTraces.length + ')'"></span>
</button>
</div>
<!-- Tracing Settings --> <!-- Tracing Settings -->
<div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-lg p-6 mb-8"> <div class="bg-[var(--color-bg-secondary)] border border-[var(--color-primary)]/20 rounded-lg p-6 mb-8">
<h2 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center"> <h2 class="text-xl font-semibold text-[var(--color-text-primary)] mb-4 flex items-center">
<i class="fas fa-bug mr-2 text-[var(--color-primary)] text-sm"></i> <i class="fas fa-bug mr-2 text-[var(--color-primary)] text-sm"></i>
Tracing Settings Tracing Settings
</h2> </h2>
<p class="text-xs text-[var(--color-text-secondary)] mb-4">Configure API and backend tracing</p> <p class="text-xs text-[var(--color-text-secondary)] mb-4">Configure API tracing</p>
<div class="space-y-4"> <div class="space-y-4">
<!-- Enable Tracing --> <!-- Enable Tracing -->
@@ -101,7 +73,7 @@
<input type="checkbox" x-model="settings.enable_tracing" <input type="checkbox" x-model="settings.enable_tracing"
@change="updateTracingEnabled()" @change="updateTracingEnabled()"
class="sr-only peer"> class="sr-only peer">
<div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-[var(--color-border-subtle)] after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div> <div class="w-11 h-6 bg-[var(--color-bg-primary)] peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[var(--color-primary-light)] rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[var(--color-primary)]"></div>
</label> </label>
</div> </div>
@@ -119,18 +91,18 @@
<!-- Save Button --> <!-- Save Button -->
<div class="flex justify-end pt-2"> <div class="flex justify-end pt-2">
<button type="button" @click="saveTracingSettings()" <button @click="saveTracingSettings()"
:disabled="saving" :disabled="saving"
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)]"> class="btn-primary px-4 py-2 text-sm">
<i class="fas fa-save text-[10px]" :class="saving ? 'fa-spin fa-spinner' : ''"></i> <i class="fas fa-save mr-2" :class="saving ? 'fa-spin fa-spinner' : ''"></i>
<span x-text="saving ? 'Saving...' : 'Save Settings'"></span> <span x-text="saving ? 'Saving...' : 'Save Settings'"></span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- API Traces Table --> <!-- Traces Table -->
<div class="mt-8" x-show="activeTab === 'api'"> <div class="mt-8">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full border-collapse"> <table class="w-full border-collapse">
<thead> <thead>
@@ -156,67 +128,14 @@
</template> </template>
</tbody> </tbody>
</table> </table>
<div x-show="traces.length === 0" class="text-center py-8 text-[var(--color-text-secondary)] text-sm">
No API traces recorded yet.
</div>
</div> </div>
</div> </div>
<!-- Backend Traces Table --> <!-- Details Modal -->
<div class="mt-8" x-show="activeTab === 'backend'">
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-[var(--color-bg-secondary)]">
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Type</th>
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Timestamp</th>
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Model</th>
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Summary</th>
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Duration</th>
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Status</th>
<th class="text-right p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Actions</th>
</tr>
</thead>
<tbody>
<template x-for="(trace, index) in backendTraces" :key="index">
<tr class="hover:bg-[var(--color-bg-secondary)]/50 border-b border-[var(--color-bg-secondary)] transition-colors">
<td class="p-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
:class="getTypeClass(trace.type)"
x-text="trace.type"></span>
</td>
<td class="p-2 text-xs text-[var(--color-text-secondary)]" x-text="formatTimestamp(trace.timestamp)"></td>
<td class="p-2 text-sm" x-text="trace.model_name || '-'"></td>
<td class="p-2 text-sm max-w-xs truncate" x-text="trace.summary || '-'"></td>
<td class="p-2 text-xs text-[var(--color-text-secondary)]" x-text="formatDuration(trace.duration)"></td>
<td class="p-2">
<template x-if="!trace.error">
<i class="fas fa-check-circle text-green-500 text-xs"></i>
</template>
<template x-if="trace.error">
<i class="fas fa-times-circle text-red-500 text-xs" :title="trace.error"></i>
</template>
</td>
<td class="p-2 text-right">
<button @click="showBackendDetails(index)" class="text-[var(--color-primary)]/60 hover:text-[var(--color-primary)] hover:bg-[var(--color-primary)]/10 rounded p-1 transition-colors">
<i class="fas fa-eye text-xs"></i>
</button>
</td>
</tr>
</template>
</tbody>
</table>
<div x-show="backendTraces.length === 0" class="text-center py-8 text-[var(--color-text-secondary)] text-sm">
No backend traces recorded yet.
</div>
</div>
</div>
<!-- API Trace Details Modal -->
<div x-show="selectedTrace !== null" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" @click="selectedTrace = null"> <div x-show="selectedTrace !== null" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" @click="selectedTrace = null">
<div class="bg-[var(--color-bg-secondary)] rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-auto" @click.stop> <div class="bg-[var(--color-bg-secondary)] rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-auto" @click.stop>
<div class="flex justify-between mb-4"> <div class="flex justify-between mb-4">
<h2 class="h3">API Trace Details</h2> <h2 class="h3">Trace Details</h2>
<button @click="selectedTrace = null" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"> <button @click="selectedTrace = null" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
@@ -234,98 +153,9 @@
</div> </div>
</div> </div>
<!-- Backend Trace Details Modal -->
<div x-show="selectedBackendTrace !== null" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" @click="selectedBackendTrace = null; detailKey = null; detailValue = null;">
<div class="bg-[var(--color-bg-secondary)] rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-auto" @click.stop>
<template x-if="selectedBackendTrace !== null">
<div>
<div class="flex justify-between mb-4">
<h2 class="h3">Backend Trace Details</h2>
<button @click="selectedBackendTrace = null; detailKey = null; detailValue = null;" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Header info -->
<div class="grid grid-cols-4 gap-4 mb-4">
<div class="bg-[var(--color-bg-primary)] rounded p-3">
<div class="text-xs text-[var(--color-text-secondary)] mb-1">Type</div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
:class="getTypeClass(backendTraces[selectedBackendTrace].type)"
x-text="backendTraces[selectedBackendTrace].type"></span>
</div>
<div class="bg-[var(--color-bg-primary)] rounded p-3">
<div class="text-xs text-[var(--color-text-secondary)] mb-1">Model</div>
<div class="text-sm font-medium" x-text="backendTraces[selectedBackendTrace].model_name || '-'"></div>
</div>
<div class="bg-[var(--color-bg-primary)] rounded p-3">
<div class="text-xs text-[var(--color-text-secondary)] mb-1">Backend</div>
<div class="text-sm font-medium" x-text="backendTraces[selectedBackendTrace].backend || '-'"></div>
</div>
<div class="bg-[var(--color-bg-primary)] rounded p-3">
<div class="text-xs text-[var(--color-text-secondary)] mb-1">Duration</div>
<div class="text-sm font-medium" x-text="formatDuration(backendTraces[selectedBackendTrace].duration)"></div>
</div>
</div>
<!-- Error banner -->
<div x-show="backendTraces[selectedBackendTrace].error" class="bg-red-500/10 border border-red-500/30 rounded-lg p-3 mb-4">
<div class="flex items-center gap-2">
<i class="fas fa-exclamation-triangle text-red-500 text-sm"></i>
<span class="text-sm text-red-400" x-text="backendTraces[selectedBackendTrace].error"></span>
</div>
</div>
<!-- Data fields table -->
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-[var(--color-bg-primary)]">
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)] w-1/4">Field</th>
<th class="text-left p-2 text-xs font-semibold text-[var(--color-text-secondary)]">Value</th>
</tr>
</thead>
<tbody>
<template x-for="[key, value] in getDataEntries(selectedBackendTrace)" :key="key">
<tr class="border-b border-[var(--color-bg-primary)] hover:bg-[var(--color-bg-primary)]/50 transition-colors">
<td class="p-2 text-sm font-mono text-[var(--color-primary)]" x-text="key"></td>
<td class="p-2 text-sm">
<template x-if="isLargeValue(value)">
<button @click="showValueDetail(key, value)"
class="text-left max-w-full">
<span class="block truncate max-w-lg text-[var(--color-text-secondary)]" x-text="truncateValue(value, 120)"></span>
<span class="text-xs text-[var(--color-primary)] hover:underline mt-0.5 inline-block">View full value</span>
</button>
</template>
<template x-if="!isLargeValue(value)">
<span class="font-mono text-xs" x-text="formatValue(value)"></span>
</template>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>
</div>
</div>
<!-- Value Detail Modal -->
<div x-show="detailValue !== null" class="fixed inset-0 bg-black/50 flex items-center justify-center z-[60]" @click="detailValue = null; detailKey = null;">
<div class="bg-[var(--color-bg-secondary)] rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-auto" @click.stop>
<div class="flex justify-between mb-4">
<h2 class="h3 font-mono" x-text="detailKey"></h2>
<button @click="detailValue = null; detailKey = null;" class="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<i class="fas fa-times"></i>
</button>
</div>
<div id="detailEditor" class="h-[70vh] border border-[var(--color-primary-border)]/20"></div>
</div>
</div>
</div> </div>
{{template "views/partials/footer" .}}
</div> </div>
@@ -345,44 +175,21 @@
<script> <script>
function tracesApp() { function tracesApp() {
return { return {
activeTab: 'api',
traces: [], traces: [],
backendTraces: [],
selectedTrace: null, selectedTrace: null,
selectedBackendTrace: null,
detailKey: null,
detailValue: null,
requestEditor: null, requestEditor: null,
responseEditor: null, responseEditor: null,
detailEditor: null,
notifications: [], notifications: [],
settings: { settings: {
enable_tracing: false, enable_tracing: false,
tracing_max_items: 0 tracing_max_items: 0
}, },
saving: false, saving: false,
refreshInterval: null,
init() { init() {
this.loadTracingSettings(); this.loadTracingSettings();
this.fetchTraces(); this.fetchTraces();
this.fetchBackendTraces(); setInterval(() => this.fetchTraces(), 5000);
this.startAutoRefresh();
},
switchTab(tab) {
this.activeTab = tab;
},
startAutoRefresh() {
if (this.refreshInterval) clearInterval(this.refreshInterval);
this.refreshInterval = setInterval(() => {
if (this.activeTab === 'api') {
this.fetchTraces();
} else {
this.fetchBackendTraces();
}
}, 5000);
}, },
async loadTracingSettings() { async loadTracingSettings() {
@@ -453,37 +260,17 @@ function tracesApp() {
}, },
async fetchTraces() { async fetchTraces() {
try { const response = await fetch('/api/traces');
const response = await fetch('/api/traces'); this.traces = await response.json();
this.traces = await response.json();
} catch (e) {
console.error('Error fetching API traces:', e);
}
},
async fetchBackendTraces() {
try {
const response = await fetch('/api/backend-traces');
this.backendTraces = await response.json();
} catch (e) {
console.error('Error fetching backend traces:', e);
}
}, },
async clearTraces() { async clearTraces() {
if (confirm('Clear all API traces?')) { if (confirm('Clear all traces?')) {
await fetch('/api/traces/clear', { method: 'POST' }); await fetch('/api/traces/clear', { method: 'POST' });
this.traces = []; this.traces = [];
} }
}, },
async clearBackendTraces() {
if (confirm('Clear all backend traces?')) {
await fetch('/api/backend-traces/clear', { method: 'POST' });
this.backendTraces = [];
}
},
showDetails(index) { showDetails(index) {
this.selectedTrace = index; this.selectedTrace = index;
this.$nextTick(() => { this.$nextTick(() => {
@@ -538,106 +325,10 @@ function tracesApp() {
this.responseEditor.setValue(resBody); this.responseEditor.setValue(resBody);
} }
}); });
},
showBackendDetails(index) {
this.selectedBackendTrace = index;
},
showValueDetail(key, value) {
this.detailKey = key;
let formatted = '';
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
formatted = JSON.stringify(parsed, null, 2);
} catch {
formatted = value;
}
} else if (typeof value === 'object') {
formatted = JSON.stringify(value, null, 2);
} else {
formatted = String(value);
}
this.detailValue = formatted;
this.$nextTick(() => {
const el = document.getElementById('detailEditor');
if (el) {
el.innerHTML = '';
this.detailEditor = CodeMirror(el, {
value: formatted,
mode: 'javascript',
json: true,
theme: 'default',
lineNumbers: true,
readOnly: true,
lineWrapping: true
});
}
});
},
formatTimestamp(ts) {
if (!ts) return '-';
const d = new Date(ts);
return d.toLocaleTimeString() + '.' + String(d.getMilliseconds()).padStart(3, '0');
},
formatDuration(ns) {
if (!ns) return '-';
const ms = ns / 1000000;
if (ms < 1000) return ms.toFixed(1) + 'ms';
return (ms / 1000).toFixed(2) + 's';
},
getTypeClass(type) {
const classes = {
'llm': 'bg-blue-500/20 text-blue-400',
'embedding': 'bg-purple-500/20 text-purple-400',
'transcription': 'bg-yellow-500/20 text-yellow-400',
'image_generation': 'bg-green-500/20 text-green-400',
'video_generation': 'bg-pink-500/20 text-pink-400',
'tts': 'bg-orange-500/20 text-orange-400',
'sound_generation': 'bg-teal-500/20 text-teal-400',
'rerank': 'bg-indigo-500/20 text-indigo-400',
'tokenize': 'bg-gray-500/20 text-gray-400',
};
return classes[type] || 'bg-gray-500/20 text-gray-400';
},
isLargeValue(value) {
if (typeof value === 'string') return value.length > 120;
if (typeof value === 'object') return JSON.stringify(value).length > 120;
return false;
},
truncateValue(value, maxLen) {
let str = typeof value === 'object' ? JSON.stringify(value) : String(value);
if (str.length <= maxLen) return str;
return str.substring(0, maxLen) + '...';
},
formatValue(value) {
if (value === null || value === undefined) return 'null';
if (typeof value === 'boolean') return value ? 'true' : 'false';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
},
getDataEntries(index) {
const trace = this.backendTraces[index];
if (!trace || !trace.data) return [];
return Object.entries(trace.data);
} }
} }
} }
</script> </script>
{{template "views/partials/footer" .}}
</div>
</main>
</div>
</body> </body>
</html> </html>

View File

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

View File

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

View File

@@ -27,8 +27,8 @@ import (
"github.com/mudler/LocalAI/pkg/model" "github.com/mudler/LocalAI/pkg/model"
"github.com/mudler/LocalAI/pkg/xsync" "github.com/mudler/LocalAI/pkg/xsync"
"github.com/mudler/cogito" "github.com/mudler/cogito"
"github.com/mudler/xlog"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"github.com/mudler/xlog"
) )
// AgentJobService manages agent tasks and job execution // AgentJobService manages agent tasks and job execution
@@ -894,6 +894,17 @@ func (s *AgentJobService) executeJobInternal(job schema.Job, task schema.Task, c
return fmt.Errorf("failed to execute tools: %w", err) return fmt.Errorf("failed to execute tools: %w", err)
} }
// Get final response
f, err = defaultLLM.Ask(ctx, f)
if err != nil {
job.Status = schema.JobStatusFailed
job.Error = fmt.Sprintf("failed to get response: %v", err)
completedAt := time.Now()
job.CompletedAt = &completedAt
s.jobs.Set(job.ID, job)
return fmt.Errorf("failed to get response: %w", err)
}
// Extract traces from fragment.Status after execution // Extract traces from fragment.Status after execution
// This provides complete information about tool calls and results // This provides complete information about tool calls and results
// We use Status data to supplement/replace callback data for completeness // We use Status data to supplement/replace callback data for completeness

View File

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

View File

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

View File

@@ -1,130 +0,0 @@
package trace
import (
"encoding/json"
"sort"
"sync"
"time"
"github.com/emirpasic/gods/v2/queues/circularbuffer"
"github.com/mudler/LocalAI/core/schema"
"github.com/mudler/xlog"
)
type BackendTraceType string
const (
BackendTraceLLM BackendTraceType = "llm"
BackendTraceEmbedding BackendTraceType = "embedding"
BackendTraceTranscription BackendTraceType = "transcription"
BackendTraceImageGeneration BackendTraceType = "image_generation"
BackendTraceVideoGeneration BackendTraceType = "video_generation"
BackendTraceTTS BackendTraceType = "tts"
BackendTraceSoundGeneration BackendTraceType = "sound_generation"
BackendTraceRerank BackendTraceType = "rerank"
BackendTraceTokenize BackendTraceType = "tokenize"
)
type BackendTrace struct {
Timestamp time.Time `json:"timestamp"`
Duration time.Duration `json:"duration"`
Type BackendTraceType `json:"type"`
ModelName string `json:"model_name"`
Backend string `json:"backend"`
Summary string `json:"summary"`
Error string `json:"error,omitempty"`
Data map[string]any `json:"data"`
}
var backendTraceBuffer *circularbuffer.Queue[*BackendTrace]
var backendMu sync.Mutex
var backendLogChan = make(chan *BackendTrace, 100)
var backendInitOnce sync.Once
func InitBackendTracingIfEnabled(maxItems int) {
backendInitOnce.Do(func() {
if maxItems <= 0 {
maxItems = 100
}
backendMu.Lock()
backendTraceBuffer = circularbuffer.New[*BackendTrace](maxItems)
backendMu.Unlock()
go func() {
for t := range backendLogChan {
backendMu.Lock()
if backendTraceBuffer != nil {
backendTraceBuffer.Enqueue(t)
}
backendMu.Unlock()
}
}()
})
}
func RecordBackendTrace(t BackendTrace) {
select {
case backendLogChan <- &t:
default:
xlog.Warn("Backend trace channel full, dropping trace")
}
}
func GetBackendTraces() []BackendTrace {
backendMu.Lock()
if backendTraceBuffer == nil {
backendMu.Unlock()
return []BackendTrace{}
}
ptrs := backendTraceBuffer.Values()
backendMu.Unlock()
traces := make([]BackendTrace, len(ptrs))
for i, p := range ptrs {
traces[i] = *p
}
sort.Slice(traces, func(i, j int) bool {
return traces[i].Timestamp.Before(traces[j].Timestamp)
})
return traces
}
func ClearBackendTraces() {
backendMu.Lock()
if backendTraceBuffer != nil {
backendTraceBuffer.Clear()
}
backendMu.Unlock()
}
func GenerateLLMSummary(messages schema.Messages, prompt string) string {
if len(messages) > 0 {
last := messages[len(messages)-1]
text := ""
switch content := last.Content.(type) {
case string:
text = content
default:
b, err := json.Marshal(content)
if err == nil {
text = string(b)
}
}
if text != "" {
return TruncateString(text, 200)
}
}
if prompt != "" {
return TruncateString(prompt, 200)
}
return ""
}
func TruncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "..."
}

View File

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

View File

@@ -1,3 +1,3 @@
{ {
"version": "v3.12.0" "version": "v3.11.0"
} }

View File

@@ -1,46 +1,4 @@
--- ---
- &nanbeige4
name: "nanbeige4.1-3b-q8"
url: "github:mudler/LocalAI/gallery/nanbeige4.1.yaml@master"
urls:
- https://huggingface.co/Nanbeige/Nanbeige4.1-3B
- https://huggingface.co/Edge-Quant/Nanbeige4.1-3B-Q8_0-GGUF
icon: https://cdn-avatars.huggingface.co/v1/production/uploads/646f0d118ff94af23bc44aab/GXHCollpMRgvYqUXQ2BQ7.png
license: apache-2.0
description: |
Nanbeige4.1-3B is built upon Nanbeige4-3B-Base and represents an enhanced iteration of our previous reasoning model, Nanbeige4-3B-Thinking-2511, achieved through further post-training optimization with supervised fine-tuning (SFT) and reinforcement learning (RL). As a highly competitive open-source model at a small parameter scale, Nanbeige4.1-3B illustrates that compact models can simultaneously achieve robust reasoning, preference alignment, and effective agentic behaviors.
Key features:
Strong Reasoning: Capable of solving complex, multi-step problems through sustained and coherent reasoning within a single forward pass, reliably producing correct answers on benchmarks like LiveCodeBench-Pro, IMO-Answer-Bench, and AIME 2026 I.
Robust Preference Alignment: Outperforms same-scale models (e.g., Qwen3-4B-2507, Nanbeige4-3B-2511) and larger models (e.g., Qwen3-30B-A3B, Qwen3-32B) on Arena-Hard-v2 and Multi-Challenge.
Agentic Capability: First general small model to natively support deep-search tasks and sustain complex problem-solving with >500 rounds of tool invocations; excels in benchmarks like xBench-DeepSearch (75), Browse-Comp (39), and others.
tags:
- llm
- gguf
- gpu
- cpu
- nanbeige
- reasoning
- agent
overrides:
parameters:
model: nanbeige4.1-3b-q8_0.gguf
files:
- filename: nanbeige4.1-3b-q8_0.gguf
sha256: a5a4379e50605c5e5a31bb1716a211fb16691fea7e13ede7f88796e1f617d9e0
uri: huggingface://Edge-Quant/Nanbeige4.1-3B-Q8_0-GGUF/nanbeige4.1-3b-q8_0.gguf
- !!merge <<: *nanbeige4
name: "nanbeige4.1-3b-q4"
urls:
- https://huggingface.co/Nanbeige/Nanbeige4.1-3B
- https://huggingface.co/Edge-Quant/Nanbeige4.1-3B-Q4_K_M-GGUF
overrides:
parameters:
model: nanbeige4.1-3b-q4_k_m.gguf
files:
- filename: nanbeige4.1-3b-q4_k_m.gguf
sha256: 043246350c952877b38958a9e35c480419008b6b2d52bedaf2b805ed2447b4df
uri: huggingface://Edge-Quant/Nanbeige4.1-3B-Q4_K_M-GGUF/nanbeige4.1-3b-q4_k_m.gguf
- name: nemo-parakeet-tdt-0.6b - name: nemo-parakeet-tdt-0.6b
license: apache-2.0 license: apache-2.0
url: "github:mudler/LocalAI/gallery/virtual.yaml@master" url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
@@ -340,7 +298,7 @@
files: files:
- filename: llama-cpp/models/Qwen3-Coder-Next-MXFP4_MOE.gguf - filename: llama-cpp/models/Qwen3-Coder-Next-MXFP4_MOE.gguf
uri: https://huggingface.co/noctrex/Qwen3-Coder-Next-MXFP4_MOE-GGUF/resolve/main/Qwen3-Coder-Next-MXFP4_MOE.gguf uri: https://huggingface.co/noctrex/Qwen3-Coder-Next-MXFP4_MOE-GGUF/resolve/main/Qwen3-Coder-Next-MXFP4_MOE.gguf
sha256: 7d8ee34faa65a5ac5b3e7b00bb5ec5b4f4bfda58a4775a61372676e27081f9c2 sha256: 7c3c1622bb2954cf304dc917a382ee7437f433f703fc28330e632c34ab4bbfdf
- name: "deepseek-ai.deepseek-v3.2" - name: "deepseek-ai.deepseek-v3.2"
url: "github:mudler/LocalAI/gallery/virtual.yaml@master" url: "github:mudler/LocalAI/gallery/virtual.yaml@master"
urls: urls:

View File

@@ -1,16 +0,0 @@
---
name: nanbeige4.1
config_file: |
backend: llama-cpp
function:
grammar:
disable: true
known_usecases:
- chat
options:
- use_jinja:true
parameters:
model: llama-cpp/models/nanbeige4.1-3b-q8_0.gguf
template:
use_tokenizer_template: true

14
go.mod
View File

@@ -24,7 +24,7 @@ require (
github.com/gpustack/gguf-parser-go v0.23.1 github.com/gpustack/gguf-parser-go v0.23.1
github.com/hpcloud/tail v1.0.0 github.com/hpcloud/tail v1.0.0
github.com/ipfs/go-log v1.0.5 github.com/ipfs/go-log v1.0.5
github.com/jaypipes/ghw v0.23.0 github.com/jaypipes/ghw v0.22.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/klauspost/cpuid/v2 v2.3.0 github.com/klauspost/cpuid/v2 v2.3.0
github.com/labstack/echo/v4 v4.15.0 github.com/labstack/echo/v4 v4.15.0
@@ -32,8 +32,8 @@ require (
github.com/lithammer/fuzzysearch v1.1.8 github.com/lithammer/fuzzysearch v1.1.8
github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.27 github.com/microcosm-cc/bluemonday v1.0.27
github.com/modelcontextprotocol/go-sdk v1.3.0 github.com/modelcontextprotocol/go-sdk v1.2.0
github.com/mudler/cogito v0.9.1-0.20260217143801-bb7f986ed2c7 github.com/mudler/cogito v0.8.1
github.com/mudler/edgevpn v0.31.1 github.com/mudler/edgevpn v0.31.1
github.com/mudler/go-processmanager v0.1.0 github.com/mudler/go-processmanager v0.1.0
github.com/mudler/memory v0.0.0-20251216220809-d1256471a6c2 github.com/mudler/memory v0.0.0-20251216220809-d1256471a6c2
@@ -60,7 +60,8 @@ require (
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 go.opentelemetry.io/otel/exporters/prometheus v0.62.0
go.opentelemetry.io/otel/metric v1.40.0 go.opentelemetry.io/otel/metric v1.40.0
go.opentelemetry.io/otel/sdk/metric v1.40.0 go.opentelemetry.io/otel/sdk/metric v1.40.0
google.golang.org/grpc v1.79.1 google.golang.org/grpc v1.78.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
oras.land/oras-go/v2 v2.6.0 oras.land/oras-go/v2 v2.6.0
) )
@@ -75,7 +76,6 @@ require (
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )
require ( require (
@@ -103,7 +103,7 @@ require (
github.com/go-text/render v0.2.0 // indirect github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/jsonschema-go v0.3.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
@@ -334,7 +334,7 @@ require (
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
gonum.org/v1/gonum v0.16.0 // indirect gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect howett.net/plist v1.0.2-0.20250314012144-ee69052608d9 // indirect

30
go.sum
View File

@@ -275,8 +275,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
@@ -342,8 +342,8 @@ github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jaypipes/ghw v0.23.0 h1:WOL4hpLcIu1kIm+z5Oz19Tk1HNw/Sncrx/6GS8O0Kl0= github.com/jaypipes/ghw v0.22.0 h1:v3G5E1Q7UO61xV15lls5a+2jkQNjM3Z3fE+KOWRt1j4=
github.com/jaypipes/ghw v0.23.0/go.mod h1:fUNUjMZ0cjahKo+/u+32m9FutIx53Nkbi0Ti0m7j5HY= github.com/jaypipes/ghw v0.22.0/go.mod h1:fUNUjMZ0cjahKo+/u+32m9FutIx53Nkbi0Ti0m7j5HY=
github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro= github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro=
github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8= github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8=
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
@@ -496,8 +496,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modelcontextprotocol/go-sdk v1.3.0 h1:gMfZkv3DzQF5q/DcQePo5rahEY+sguyPfXDfNBcT0Zs= github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
github.com/modelcontextprotocol/go-sdk v1.3.0/go.mod h1:AnQ//Qc6+4nIyyrB4cxBU7UW9VibK4iOZBeyP/rF1IE= github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -509,10 +509,8 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mudler/cogito v0.8.2-0.20260214201734-da0d4ceb2b44 h1:joGszpItINnZdoL/0p2077Wz2xnxMGRSRgYN5mS7I4c= github.com/mudler/cogito v0.8.1 h1:66qPJkAMrq/Vo8AC/PvXWuVxYPhi7X2DQuJIilL8+3I=
github.com/mudler/cogito v0.8.2-0.20260214201734-da0d4ceb2b44/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4= github.com/mudler/cogito v0.8.1/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
github.com/mudler/cogito v0.9.1-0.20260217143801-bb7f986ed2c7 h1:z3AcM7LbaQb+C955JdSXksHB9B0uWGQpdgl05gJM+9Y=
github.com/mudler/cogito v0.9.1-0.20260217143801-bb7f986ed2c7/go.mod h1:6sfja3lcu2nWRzEc0wwqGNu/eCG3EWgij+8s7xyUeQ4=
github.com/mudler/edgevpn v0.31.1 h1:7qegiDWd0kAg6ljhNHxqvp8hbo/6BbzSdbb7/2WZfiY= github.com/mudler/edgevpn v0.31.1 h1:7qegiDWd0kAg6ljhNHxqvp8hbo/6BbzSdbb7/2WZfiY=
github.com/mudler/edgevpn v0.31.1/go.mod h1:ftV5B0nKFzm4R8vR80UYnCb2nf7lxCRgAALxUEEgCf8= github.com/mudler/edgevpn v0.31.1/go.mod h1:ftV5B0nKFzm4R8vR80UYnCb2nf7lxCRgAALxUEEgCf8=
github.com/mudler/go-piper v0.0.0-20241023091659-2494246fd9fc h1:RxwneJl1VgvikiX28EkpdAyL4yQVnJMrbquKospjHyA= github.com/mudler/go-piper v0.0.0-20241023091659-2494246fd9fc h1:RxwneJl1VgvikiX28EkpdAyL4yQVnJMrbquKospjHyA=
@@ -1059,10 +1057,10 @@ google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
@@ -1071,8 +1069,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View File

@@ -17,15 +17,11 @@ import (
var forceBackendShutdown bool = os.Getenv("LOCALAI_FORCE_BACKEND_SHUTDOWN") == "true" var forceBackendShutdown bool = os.Getenv("LOCALAI_FORCE_BACKEND_SHUTDOWN") == "true"
var (
modelNotFoundErr = errors.New("model not found")
)
func (ml *ModelLoader) deleteProcess(s string) error { func (ml *ModelLoader) deleteProcess(s string) error {
model, ok := ml.models[s] model, ok := ml.models[s]
if !ok { if !ok {
xlog.Debug("Model not found", "model", s) xlog.Debug("Model not found", "model", s)
return modelNotFoundErr return fmt.Errorf("model %s not found", s)
} }
retries := 1 retries := 1

View File

@@ -33,7 +33,6 @@ type WatchDog struct {
addressModelMap map[string]string addressModelMap map[string]string
pm ProcessManager pm ProcessManager
stop chan bool stop chan bool
done chan bool // Signals when Run() has completely shut down
busyCheck, idleCheck bool busyCheck, idleCheck bool
lruLimit int // Maximum number of active backends (0 = unlimited) lruLimit int // Maximum number of active backends (0 = unlimited)
@@ -79,7 +78,6 @@ func NewWatchDog(opts ...WatchDogOption) *WatchDog {
lruLimit: o.lruLimit, lruLimit: o.lruLimit,
addressModelMap: make(map[string]string), addressModelMap: make(map[string]string),
stop: make(chan bool, 1), stop: make(chan bool, 1),
done: make(chan bool, 1),
memoryReclaimerEnabled: o.memoryReclaimerEnabled, memoryReclaimerEnabled: o.memoryReclaimerEnabled,
memoryReclaimerThreshold: o.memoryReclaimerThreshold, memoryReclaimerThreshold: o.memoryReclaimerThreshold,
watchdogInterval: o.watchdogInterval, watchdogInterval: o.watchdogInterval,
@@ -130,12 +128,6 @@ func (wd *WatchDog) Shutdown() {
wd.stop <- true wd.stop <- true
} }
// WaitDone blocks until the watchdog's Run() goroutine has completely shut down.
// This should be called after Shutdown() to ensure the watchdog is fully stopped.
func (wd *WatchDog) WaitDone() {
<-wd.done
}
func (wd *WatchDog) AddAddressModelMap(address string, model string) { func (wd *WatchDog) AddAddressModelMap(address string, model string) {
wd.Lock() wd.Lock()
defer wd.Unlock() defer wd.Unlock()
@@ -181,71 +173,6 @@ func (wd *WatchDog) GetLoadedModelCount() int {
return len(wd.addressModelMap) return len(wd.addressModelMap)
} }
// WatchDogState holds the current state of models tracked by the watchdog
type WatchDogState struct {
AddressModelMap map[string]string
BusyTime map[string]time.Time
IdleTime map[string]time.Time
LastUsed map[string]time.Time
AddressMap map[string]*process.Process
}
// GetState returns the current state of models tracked by the watchdog
// This can be used to restore state when creating a new watchdog
func (wd *WatchDog) GetState() WatchDogState {
wd.Lock()
defer wd.Unlock()
// Create copies to avoid race conditions
addressModelMap := make(map[string]string, len(wd.addressModelMap))
for k, v := range wd.addressModelMap {
addressModelMap[k] = v
}
busyTime := make(map[string]time.Time, len(wd.busyTime))
for k, v := range wd.busyTime {
busyTime[k] = v
}
idleTime := make(map[string]time.Time, len(wd.idleTime))
for k, v := range wd.idleTime {
idleTime[k] = v
}
lastUsed := make(map[string]time.Time, len(wd.lastUsed))
for k, v := range wd.lastUsed {
lastUsed[k] = v
}
addressMap := make(map[string]*process.Process, len(wd.addressMap))
for k, v := range wd.addressMap {
addressMap[k] = v
}
return WatchDogState{
AddressModelMap: addressModelMap,
BusyTime: busyTime,
IdleTime: idleTime,
LastUsed: lastUsed,
AddressMap: addressMap,
}
}
// RestoreState restores the model state from a previous watchdog
// This should be called after the new watchdog is created but before Run() is started
func (wd *WatchDog) RestoreState(state WatchDogState) {
wd.Lock()
defer wd.Unlock()
wd.addressModelMap = state.AddressModelMap
wd.busyTime = state.BusyTime
wd.idleTime = state.IdleTime
wd.lastUsed = state.LastUsed
wd.addressMap = state.AddressMap
xlog.Info("[WatchDog] Restored model state", "modelCount", len(wd.addressModelMap))
}
// modelUsageInfo holds information about a model's usage for LRU sorting // modelUsageInfo holds information about a model's usage for LRU sorting
type modelUsageInfo struct { type modelUsageInfo struct {
address string address string
@@ -352,7 +279,6 @@ func (wd *WatchDog) Run() {
select { select {
case <-wd.stop: case <-wd.stop:
xlog.Info("[WatchDog] Stopping watchdog") xlog.Info("[WatchDog] Stopping watchdog")
wd.done <- true
return return
case <-time.After(wd.watchdogInterval): case <-time.After(wd.watchdogInterval):
// Check if any monitoring is enabled // Check if any monitoring is enabled
@@ -364,7 +290,6 @@ func (wd *WatchDog) Run() {
if !busyCheck && !idleCheck && !memoryCheck { if !busyCheck && !idleCheck && !memoryCheck {
xlog.Info("[WatchDog] No checks enabled, stopping watchdog") xlog.Info("[WatchDog] No checks enabled, stopping watchdog")
wd.done <- true
return return
} }
if busyCheck { if busyCheck {
@@ -537,16 +462,14 @@ func (wd *WatchDog) evictLRUModel() {
xlog.Info("[WatchDog] Memory reclaimer evicting LRU model", "model", lruModel.model, "lastUsed", lruModel.lastUsed) xlog.Info("[WatchDog] Memory reclaimer evicting LRU model", "model", lruModel.model, "lastUsed", lruModel.lastUsed)
// Untrack the model
wd.untrack(lruModel.address)
wd.Unlock() wd.Unlock()
// Shutdown the model // Shutdown the model
if err := wd.pm.ShutdownModel(lruModel.model); err != nil && err != modelNotFoundErr { if err := wd.pm.ShutdownModel(lruModel.model); err != nil {
xlog.Error("[WatchDog] error shutting down model during memory reclamation", "error", err, "model", lruModel.model) xlog.Error("[WatchDog] error shutting down model during memory reclamation", "error", err, "model", lruModel.model)
} else { } else {
// Untrack the model
wd.Lock()
wd.untrack(lruModel.address)
wd.Unlock()
xlog.Info("[WatchDog] Memory reclaimer eviction complete", "model", lruModel.model) xlog.Info("[WatchDog] Memory reclaimer eviction complete", "model", lruModel.model)
} }
} }

View File

@@ -45,8 +45,9 @@ const (
) )
var ( var (
cuda13DirExists bool cuda13DirExists bool
cuda12DirExists bool cuda12DirExists bool
capabilityLogged bool
) )
func init() { func init() {
@@ -71,15 +72,9 @@ func (s *SystemState) Capability(capMap map[string]string) string {
} }
func (s *SystemState) getSystemCapabilities() string { func (s *SystemState) getSystemCapabilities() string {
if s.systemCapabilities != "" {
return s.systemCapabilities
}
capability := os.Getenv(capabilityEnv) capability := os.Getenv(capabilityEnv)
if capability != "" { if capability != "" {
xlog.Info("Using forced capability from environment variable", "capability", capability, "env", capabilityEnv) xlog.Info("Using forced capability from environment variable", "capability", capability, "env", capabilityEnv)
s.systemCapabilities = capability
return capability return capability
} }
@@ -96,23 +91,20 @@ func (s *SystemState) getSystemCapabilities() string {
capability, err := os.ReadFile(capabilityRunFile) capability, err := os.ReadFile(capabilityRunFile)
if err == nil { if err == nil {
xlog.Info("Using forced capability run file", "capabilityRunFile", capabilityRunFile, "capability", string(capability), "env", capabilityRunFileEnv) xlog.Info("Using forced capability run file", "capabilityRunFile", capabilityRunFile, "capability", string(capability), "env", capabilityRunFileEnv)
s.systemCapabilities = strings.Trim(strings.TrimSpace(string(capability)), "\n") return strings.Trim(strings.TrimSpace(string(capability)), "\n")
return s.systemCapabilities
} }
} }
// If we are on mac and arm64, we will return metal // If we are on mac and arm64, we will return metal
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
xlog.Info("Using metal capability (arm64 on mac)", "env", capabilityEnv) xlog.Info("Using metal capability (arm64 on mac)", "env", capabilityEnv)
s.systemCapabilities = metal return metal
return s.systemCapabilities
} }
// If we are on mac and x86, we will return darwin-x86 // If we are on mac and x86, we will return darwin-x86
if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" { if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
xlog.Info("Using darwin-x86 capability (amd64 on mac)", "env", capabilityEnv) xlog.Info("Using darwin-x86 capability (amd64 on mac)", "env", capabilityEnv)
s.systemCapabilities = darwinX86 return darwinX86
return s.systemCapabilities
} }
// If arm64 on linux and a nvidia gpu is detected, we will return nvidia-l4t // If arm64 on linux and a nvidia gpu is detected, we will return nvidia-l4t
@@ -120,43 +112,39 @@ func (s *SystemState) getSystemCapabilities() string {
if s.GPUVendor == Nvidia { if s.GPUVendor == Nvidia {
xlog.Info("Using nvidia-l4t capability (arm64 on linux)", "env", capabilityEnv) xlog.Info("Using nvidia-l4t capability (arm64 on linux)", "env", capabilityEnv)
if cuda13DirExists { if cuda13DirExists {
s.systemCapabilities = nvidiaL4TCuda13 return nvidiaL4TCuda13
return s.systemCapabilities
} }
if cuda12DirExists { if cuda12DirExists {
s.systemCapabilities = nvidiaL4TCuda12 return nvidiaL4TCuda12
return s.systemCapabilities
} }
s.systemCapabilities = nvidiaL4T return nvidiaL4T
return s.systemCapabilities
} }
} }
if cuda13DirExists { if cuda13DirExists {
s.systemCapabilities = nvidiaCuda13 return nvidiaCuda13
return s.systemCapabilities
} }
if cuda12DirExists { if cuda12DirExists {
s.systemCapabilities = nvidiaCuda12 return nvidiaCuda12
return s.systemCapabilities
} }
if s.GPUVendor == "" { if s.GPUVendor == "" {
xlog.Info("Default capability (no GPU detected)", "env", capabilityEnv) xlog.Info("Default capability (no GPU detected)", "env", capabilityEnv)
s.systemCapabilities = defaultCapability return defaultCapability
return s.systemCapabilities
} }
if !capabilityLogged {
xlog.Info("Capability automatically detected", "capability", s.GPUVendor, "env", capabilityEnv)
capabilityLogged = true
}
// If vram is less than 4GB, let's default to CPU but warn the user that they can override that via env // If vram is less than 4GB, let's default to CPU but warn the user that they can override that via env
if s.VRAM <= 4*1024*1024*1024 { if s.VRAM <= 4*1024*1024*1024 {
xlog.Warn("VRAM is less than 4GB, defaulting to CPU", "env", capabilityEnv) xlog.Warn("VRAM is less than 4GB, defaulting to CPU", "env", capabilityEnv)
s.systemCapabilities = defaultCapability return defaultCapability
return s.systemCapabilities
} }
s.systemCapabilities = s.GPUVendor return s.GPUVendor
return s.systemCapabilities
} }
// BackendPreferenceTokens returns a list of substrings that represent the preferred // BackendPreferenceTokens returns a list of substrings that represent the preferred

View File

@@ -19,8 +19,6 @@ type SystemState struct {
Backend Backend Backend Backend
Model Model Model Model
VRAM uint64 VRAM uint64
systemCapabilities string
} }
type SystemStateOptions func(*SystemState) type SystemStateOptions func(*SystemState)
@@ -55,7 +53,5 @@ func GetSystemState(opts ...SystemStateOptions) (*SystemState, error) {
state.VRAM, _ = xsysinfo.TotalAvailableVRAM() state.VRAM, _ = xsysinfo.TotalAvailableVRAM()
xlog.Debug("Total available VRAM", "vram", state.VRAM) xlog.Debug("Total available VRAM", "vram", state.VRAM)
state.getSystemCapabilities()
return state, nil return state, nil
} }