Docker runtime - SSH, runtime args

frdel committed Nov 15, 2024 at 09:49 UTC 66f1ab7baff0bbeda6c34aeee45be9df8d7c7d90
14 files changed +550 -387
docker/run/Dockerfile
+4 -1
@@ -15,7 +15,8 @@ RUN apt-get update && apt-get install -y \
15 sudo \
16 curl \
17 wget \
18 - git
18 + git \
19 + ffmpeg
20
21 # Cleanup package list
22 RUN rm -rf /var/lib/apt/lists/*
@@ -49,6 +50,8 @@ RUN $VIRTUAL_ENV/bin/pip install \
50
51 # Install A0 python packages
52 RUN $VIRTUAL_ENV/bin/pip install -r /git/agent-zero/requirements.txt
53 +# Preload A0
54 +RUN $VIRTUAL_ENV/bin/python /git/agent-zero/preload.py
55
56 # Expose ports
57 EXPOSE 22 80
docker/run/build.txt
+1 -1
@@ -3,4 +3,4 @@ docker build -t agent-zero-run:latest .
3
4 # dockerhub
5 docker login
6 -docker buildx build --platform linux/amd64,linux/arm64 -t frdel/agent-zero-exe:latest --push .
\ No newline at end of file
6 +docker buildx build --platform linux/amd64,linux/arm64 -t frdel/agent-zero-run:latest --push .
\ No newline at end of file
docker/run/fs/exe/run_A0.sh
+20 -5
@@ -1,14 +1,11 @@
1 #!/bin/bash
2
3 # Paths
4 -PYTHON_SCRIPT="/a0/run_ui.py"
4 SOURCE_DIR="/git/agent-zero"
5 TARGET_DIR="/a0"
6
7
9 -# Loop to restart the Python script when it finishes
10 -while true; do
11 -
8 +function setup_venv() {
9 # Create virtual environment if it doesn't exist
10 if [ ! -d /opt/venv ]; then
11 echo "Creating virtual environment..."
@@ -18,6 +15,16 @@ while true; do
15
16 # Activate the virtual environment
17 source /opt/venv/bin/activate
18 +}
19 +
20 +# preload A0
21 +setup_venv
22 +python /a0/preload.py
23 +
24 +# Loop to restart the Python script when it finishes
25 +while true; do
26 +
27 + setup_venv
28
29 # Copy repository files if target is empty
30 if [ -z "$(ls -A "$TARGET_DIR")" ]; then
@@ -26,7 +33,15 @@ while true; do
33 fi
34
35 echo "Starting A0..."
29 - python "$PYTHON_SCRIPT" --port 80 --host "0.0.0.0"
36 + python /a0/run_ui.py \
37 + --port 80 \
38 + --host "0.0.0.0" \
39 + --code_exec_docker_enabled False \
40 + --code_exec_ssh_enabled True \
41 + --code_exec_ssh_addr "localhost" \
42 + --code_exec_ssh_port 22 \
43 + --code_exec_ssh_user "root" \
44 + --code_exec_ssh_pass "toor"
45
46 # Check the exit status
47 if [ $? -ne 0 ]; then
docker/run/initialize.sh
+8 -4
@@ -10,8 +10,12 @@ chmod 444 /root/.profile
10 # update package list to save time later
11 apt-get update
12
13 -# Start A0
14 -bash /exe/run_A0.sh
13 +# Start SSH service in background
14 +/usr/sbin/sshd -D &
15
16 -# Start SSH service
17 -exec /usr/sbin/sshd -D
\ No newline at end of file
16 +# Start A0 and restart on exit
17 +bash /exe/run_A0.sh
18 +if [ $? -ne 0 ]; then
19 + echo "A0 script exited with an error. Restarting container..."
20 + exit 1
21 +fi
\ No newline at end of file
initialize.py
+13 -2
@@ -2,6 +2,12 @@ import models
2 from agent import AgentConfig
3 from python.helpers import files, settings
4
5 +global_kwargs = {}
6 +
7 +def set_global_kwargs(**kwargs):
8 + global global_kwargs
9 + global_kwargs = kwargs
10 +
11 def initialize():
12
13 # main chat model used by agents (smarter, more accurate)
@@ -45,7 +51,7 @@ def initialize():
51 # msgs_keep_end = 10,
52 max_tool_response_length = 3000,
53 # response_timeout_seconds = 60,
48 - code_exec_docker_enabled = True,
54 + # code_exec_docker_enabled = True,
55 # code_exec_docker_name = "agent-zero-exe",
56 # code_exec_docker_image = "frdel/agent-zero-exe:latest",
57 # code_exec_docker_ports = { "22/tcp": 50022 }
@@ -53,7 +59,7 @@ def initialize():
59 # files.get_abs_path("work_dir"): {"bind": "/root", "mode": "rw"},
60 # files.get_abs_path("instruments"): {"bind": "/instruments", "mode": "rw"},
61 # },
56 - code_exec_ssh_enabled = True,
62 + # code_exec_ssh_enabled = True,
63 # code_exec_ssh_addr = "localhost",
64 # code_exec_ssh_port = 50022,
65 # code_exec_ssh_user = "root",
@@ -61,5 +67,10 @@ def initialize():
67 # additional = {},
68 )
69
70 + # update config with kwargs
71 + for key, value in global_kwargs.items():
72 + if hasattr(config, key):
73 + setattr(config, key, value)
74 +
75 # return config object
76 return config
models.py
+13 -12
@@ -26,6 +26,7 @@ from langchain_google_genai import (
26 )
27 from langchain_mistralai import ChatMistralAI
28 from pydantic.v1.types import SecretStr
29 +from python.helpers import dotenv
30 from python.helpers.dotenv import load_dotenv
31
32 # environment variables
@@ -57,7 +58,7 @@ class ModelProvider(Enum):
58
59 # Utility function to get API keys from environment variables
60 def get_api_key(service):
60 - return os.getenv(f"API_KEY_{service.upper()}") or os.getenv(f"{service.upper()}_API_KEY") or "None"
61 + return dotenv.get_dotenv_value(f"API_KEY_{service.upper()}") or dotenv.get_dotenv_value(f"{service.upper()}_API_KEY") or "None"
62
63
64 def get_model(type: ModelType, provider: ModelProvider, name: str, **kwargs):
@@ -70,7 +71,7 @@ def get_model(type: ModelType, provider: ModelProvider, name: str, **kwargs):
71 def get_ollama_chat(
72 model_name: str,
73 temperature=DEFAULT_TEMPERATURE,
73 - base_url=os.getenv("OLLAMA_BASE_URL") or "http://127.0.0.1:11434",
74 + base_url=dotenv.get_dotenv_value("OLLAMA_BASE_URL") or "http://127.0.0.1:11434",
75 num_ctx=8192,
76 **kwargs,
77 ):
@@ -86,7 +87,7 @@ def get_ollama_chat(
87 def get_ollama_embedding(
88 model_name: str,
89 temperature=DEFAULT_TEMPERATURE,
89 - base_url=os.getenv("OLLAMA_BASE_URL") or "http://127.0.0.1:11434",
90 + base_url=dotenv.get_dotenv_value("OLLAMA_BASE_URL") or "http://127.0.0.1:11434",
91 **kwargs,
92 ):
93 return OllamaEmbeddings(
@@ -126,7 +127,7 @@ def get_huggingface_embedding(model_name: str, **kwargs):
127 def get_lmstudio_chat(
128 model_name: str,
129 temperature=DEFAULT_TEMPERATURE,
129 - base_url=os.getenv("LM_STUDIO_BASE_URL") or "http://127.0.0.1:1234/v1",
130 + base_url=dotenv.get_dotenv_value("LM_STUDIO_BASE_URL") or "http://127.0.0.1:1234/v1",
131 **kwargs,
132 ):
133 return ChatOpenAI(model_name=model_name, base_url=base_url, temperature=temperature, api_key="none", **kwargs) # type: ignore
@@ -134,7 +135,7 @@ def get_lmstudio_chat(
135
136 def get_lmstudio_embedding(
137 model_name: str,
137 - base_url=os.getenv("LM_STUDIO_BASE_URL") or "http://127.0.0.1:1234/v1",
138 + base_url=dotenv.get_dotenv_value("LM_STUDIO_BASE_URL") or "http://127.0.0.1:1234/v1",
139 **kwargs,
140 ):
141 return OpenAIEmbeddings(model=model_name, api_key="none", base_url=base_url, check_embedding_ctx_length=False, **kwargs) # type: ignore
@@ -186,7 +187,7 @@ def get_azure_openai_chat(
187 deployment_name: str,
188 api_key=get_api_key("openai_azure"),
189 temperature=DEFAULT_TEMPERATURE,
189 - azure_endpoint=os.getenv("OPENAI_AZURE_ENDPOINT"),
190 + azure_endpoint=dotenv.get_dotenv_value("OPENAI_AZURE_ENDPOINT"),
191 **kwargs,
192 ):
193 return AzureChatOpenAI(deployment_name=deployment_name, temperature=temperature, api_key=api_key, azure_endpoint=azure_endpoint, **kwargs) # type: ignore
@@ -196,7 +197,7 @@ def get_azure_openai_instruct(
197 deployment_name: str,
198 api_key=get_api_key("openai_azure"),
199 temperature=DEFAULT_TEMPERATURE,
199 - azure_endpoint=os.getenv("OPENAI_AZURE_ENDPOINT"),
200 + azure_endpoint=dotenv.get_dotenv_value("OPENAI_AZURE_ENDPOINT"),
201 **kwargs,
202 ):
203 return AzureOpenAI(deployment_name=deployment_name, temperature=temperature, api_key=api_key, azure_endpoint=azure_endpoint, **kwargs) # type: ignore
@@ -205,7 +206,7 @@ def get_azure_openai_instruct(
206 def get_azure_openai_embedding(
207 deployment_name: str,
208 api_key=get_api_key("openai_azure"),
208 - azure_endpoint=os.getenv("OPENAI_AZURE_ENDPOINT"),
209 + azure_endpoint=dotenv.get_dotenv_value("OPENAI_AZURE_ENDPOINT"),
210 **kwargs,
211 ):
212 return AzureOpenAIEmbeddings(deployment_name=deployment_name, api_key=api_key, azure_endpoint=azure_endpoint, **kwargs) # type: ignore
@@ -254,7 +255,7 @@ def get_openrouter_chat(
255 model_name: str,
256 api_key=get_api_key("openrouter"),
257 temperature=DEFAULT_TEMPERATURE,
257 - base_url=os.getenv("OPEN_ROUTER_BASE_URL") or "https://openrouter.ai/api/v1",
258 + base_url=dotenv.get_dotenv_value("OPEN_ROUTER_BASE_URL") or "https://openrouter.ai/api/v1",
259 **kwargs,
260 ):
261 return ChatOpenAI(api_key=api_key, model=model_name, temperature=temperature, base_url=base_url, **kwargs) # type: ignore
@@ -263,7 +264,7 @@ def get_openrouter_chat(
264 def get_openrouter_embedding(
265 model_name: str,
266 api_key=get_api_key("openrouter"),
266 - base_url=os.getenv("OPEN_ROUTER_BASE_URL") or "https://openrouter.ai/api/v1",
267 + base_url=dotenv.get_dotenv_value("OPEN_ROUTER_BASE_URL") or "https://openrouter.ai/api/v1",
268 **kwargs,
269 ):
270 return OpenAIEmbeddings(model=model_name, api_key=api_key, base_url=base_url, **kwargs) # type: ignore
@@ -274,7 +275,7 @@ def get_sambanova_chat(
275 model_name: str,
276 api_key=get_api_key("sambanova"),
277 temperature=DEFAULT_TEMPERATURE,
277 - base_url=os.getenv("SAMBANOVA_BASE_URL") or "https://fast-api.snova.ai/v1",
278 + base_url=dotenv.get_dotenv_value("SAMBANOVA_BASE_URL") or "https://fast-api.snova.ai/v1",
279 max_tokens=1024,
280 **kwargs,
281 ):
@@ -285,7 +286,7 @@ def get_sambanova_chat(
286 def get_sambanova_embedding(
287 model_name: str,
288 api_key=get_api_key("sambanova"),
288 - base_url=os.getenv("SAMBANOVA_BASE_URL") or "https://fast-api.snova.ai/v1",
289 + base_url=dotenv.get_dotenv_value("SAMBANOVA_BASE_URL") or "https://fast-api.snova.ai/v1",
290 **kwargs,
291 ):
292 return OpenAIEmbeddings(model=model_name, api_key=api_key, base_url=base_url, **kwargs) # type: ignore
preload.py new
+3
@@ -0,0 +1,3 @@
1 +from python.helpers import whisper_oai
2 +
3 +whisper_oai.preload() # preload transcription model
\ No newline at end of file
python/helpers/dotenv.py
+27 -2
@@ -1,6 +1,31 @@
1 +import os
2 +import re
3 from .files import get_abs_path
4 from dotenv import load_dotenv as _load_dotenv
5
6 +
7 def load_dotenv():
5 - dotenv_path = get_abs_path(".env")
6 - _load_dotenv(dotenv_path)
\ No newline at end of file
8 + _load_dotenv(get_dotenv_file_path())
9 +
10 +
11 +def get_dotenv_file_path():
12 + return get_abs_path(".env")
13 +
14 +def get_dotenv_value(key: str):
15 + load_dotenv()
16 + return os.getenv(key)
17 +
18 +def save_dotenv_value(key: str, value: str):
19 + dotenv_path = get_dotenv_file_path()
20 + with open(dotenv_path, "r+") as f:
21 + lines = f.readlines()
22 + found = False
23 + for i, line in enumerate(lines):
24 + if re.match(rf"^\s*{key}\s*=", line):
25 + lines[i] = f"{key}={value}\n"
26 + found = True
27 + if not found:
28 + lines.append(f"\n{key}={value}")
29 + f.seek(0)
30 + f.writelines(lines)
31 + f.truncate()
python/helpers/settings.py
+65 -12
@@ -2,11 +2,14 @@ import json
2 import os
3 import re
4 from typing import Any, Optional, TypedDict
5 -from . import files
5 +
6 +import models
7 +from . import files, dotenv
8 from models import get_model, ModelProvider, ModelType
9 from langchain_core.language_models.chat_models import BaseChatModel
10 from langchain_core.embeddings import Embeddings
11
12 +
13 class Settings(TypedDict):
14 chat_model_provider: str
15 chat_model_name: str
@@ -22,6 +25,8 @@ class Settings(TypedDict):
25 embed_model_name: str
26 embed_model_kwargs: dict[str, str]
27
28 + api_keys: dict[str, str]
29 +
30
31 class PartialSettings(Settings, total=False):
32 pass
@@ -212,17 +217,55 @@ def convert_out(settings: Settings) -> dict[str, Any]:
217 "fields": embed_model_fields,
218 }
219
215 - result = {"sections": [chat_model_section, util_model_section, embed_model_section]}
220 + # embedding model section
221 + api_keys_fields = []
222 + api_keys_fields.append(_get_api_key_field(settings, "openai", "OpenAI API Key"))
223 + api_keys_fields.append(_get_api_key_field(settings, "anthropic", "Anthropic API Key"))
224 + api_keys_fields.append(_get_api_key_field(settings, "groq", "Groq API Key"))
225 + api_keys_fields.append(_get_api_key_field(settings, "google", "Google API Key"))
226 + api_keys_fields.append(_get_api_key_field(settings, "openrouter", "OpenRouter API Key"))
227 + api_keys_fields.append(_get_api_key_field(settings, "sambanova", "Sambanova API Key"))
228 + api_keys_fields.append(_get_api_key_field(settings, "mistralai", "MistralAI API Key"))
229 +
230 + api_keys_section = {
231 + "title": "API Keys",
232 + "description": "API keys for model providers and services used by Agent Zero.",
233 + "fields": api_keys_fields,
234 + }
235 +
236 + result = {
237 + "sections": [
238 + chat_model_section,
239 + util_model_section,
240 + embed_model_section,
241 + api_keys_section,
242 + ]
243 + }
244 return result
245
246 +
247 +def _get_api_key_field(settings: Settings, provider: str, title: str):
248 + key = settings["api_keys"].get(provider, models.get_api_key(provider))
249 + return {
250 + "id": f"api_key_{provider}",
251 + "title": title,
252 + "type": "password",
253 + "value": key if key != "None" else "",
254 + }
255 +
256 +
257 def convert_in(settings: dict[str, Any]) -> Settings:
258 current = get_settings()
259 for section in settings["sections"]:
260 for field in section["fields"]:
261 if field["id"].endswith("_kwargs"):
223 - current[field["id"]] = _env_to_dict(field["value"]) #parse KWARGS from env format
262 + current[field["id"]] = _env_to_dict(
263 + field["value"]
264 + ) # parse KWARGS from env format
265 + elif field["id"].startswith("api_key_"):
266 + current["api_keys"][field["id"]] = field["value"]
267 else:
225 - current[field["id"]] = field["value"]
268 + current[field["id"]] = field["value"]
269 return current
270
271
@@ -238,7 +281,7 @@ def get_settings() -> Settings:
281 def set_settings(settings: Settings):
282 global _settings
283 _settings = normalize_settings(settings)
241 - _apply_settings()
284 + _apply_settings()
285 _write_settings_file(_settings)
286
287
@@ -291,6 +334,12 @@ def _read_settings_file() -> Settings | None:
334
335
336 def _write_settings_file(settings: Settings):
337 + #write api keys
338 + for key, val in settings["api_keys"].items():
339 + dotenv.save_dotenv_value(key.upper(), val)
340 + settings["api_keys"] = {} # remove API keys before saving
341 +
342 + #write settings
343 content = json.dumps(settings, indent=4)
344 files.write_file(SETTINGS_FILE, content)
345
@@ -308,8 +357,10 @@ def _get_default_settings() -> Settings:
357 embed_model_provider=ModelProvider.OPENAI.name,
358 embed_model_name="text-embedding-3-small",
359 embed_model_kwargs={},
360 + api_keys={},
361 )
362
363 +
364 def _apply_settings():
365 global _settings
366 if _settings:
@@ -317,16 +368,17 @@ def _apply_settings():
368 from initialize import initialize
369
370 for ctx in AgentContext._contexts.values():
320 - ctx.config = initialize() # reinitialize context config with new settings
321 - #apply config to agents
371 + ctx.config = initialize() # reinitialize context config with new settings
372 + # apply config to agents
373 agent = ctx.agent0
374 while agent:
375 agent.config = ctx.config
376 agent = agent.get_data("subordinate")
377
327 -def _env_to_dict(data:str):
378 +
379 +def _env_to_dict(data: str):
380 env_dict = {}
329 - line_pattern = re.compile(r'\s*([^#][^=]*)\s*=\s*(.*)')
381 + line_pattern = re.compile(r"\s*([^#][^=]*)\s*=\s*(.*)")
382 for line in data.splitlines():
383 match = line_pattern.match(line)
384 if match:
@@ -336,12 +388,13 @@ def _env_to_dict(data:str):
388 env_dict[key.strip()] = value
389 return env_dict
390
391 +
392 def _dict_to_env(data_dict):
393 lines = []
394 for key, value in data_dict.items():
342 - if '\n' in value:
395 + if "\n" in value:
396 value = f"'{value}'"
344 - elif ' ' in value or value == '' or any(c in value for c in '"\''):
397 + elif " " in value or value == "" or any(c in value for c in "\"'"):
398 value = f'"{value}"'
399 lines.append(f"{key}={value}")
347 - return "\n".join(lines)
\ No newline at end of file
400 + return "\n".join(lines)
python/helpers/whisper_oai.py
+17 -7
@@ -1,13 +1,23 @@
1 # Import the necessary libraries
2 import whisper
3 import files
4 +import tempfile
5
5 -# Load the base model from Whisper
6 -model = whisper.load_model("base")
6 +model = None
7
8 -# Add your Audio File
9 -audio = files.get_abs_path("audio.ogg")
8 +def preload():
9 + global model
10 + model = whisper.load_model("base")
11 + return model
12
11 -# Transcribe the audio file
12 -result = model.transcribe(audio, fp16=False)
13 -print(result["text"])
\ No newline at end of file
13 +def transcribe(audio_bytes):
14 + global model
15 + if model is None:
16 + model = preload()
17 +
18 + #create temp audio file
19 + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as audio_file:
20 + audio_file.write(audio_bytes)
21 +
22 + # Transcribe the audio file
23 + result = model.transcribe(audio_file.name, fp16=False)
\ No newline at end of file
python/tools/knowledge_tool.py
+2 -2
@@ -1,6 +1,6 @@
1 import os
2 import asyncio
3 -from python.helpers import memory, perplexity_search, duckduckgo_search
3 +from python.helpers import dotenv, memory, perplexity_search, duckduckgo_search
4 from python.helpers.tool import Tool, Response
5 from python.helpers.print_style import PrintStyle
6 from python.helpers.errors import handle_error
@@ -33,7 +33,7 @@ class Knowledge(Tool):
33 return Response(message=msg, break_loop=False)
34
35 async def perplexity_search(self, question):
36 - if os.getenv("API_KEY_PERPLEXITY"):
36 + if dotenv.get_dotenv_value("API_KEY_PERPLEXITY"):
37 return await asyncio.to_thread(perplexity_search.perplexity_search, question)
38 else:
39 PrintStyle.hint("No API key provided for Perplexity. Skipping Perplexity search.")
run_ui.py
+10 -2
@@ -8,7 +8,7 @@ import uuid
8 from flask import Flask, request, jsonify, Response
9 from flask_basicauth import BasicAuth
10 from agent import AgentContext
11 -from initialize import initialize
11 +from initialize import initialize, set_global_kwargs
12 from python.helpers import files
13 from python.helpers.files import get_abs_path
14 from python.helpers.print_style import PrintStyle
@@ -572,7 +572,15 @@ def run():
572 def log_request(self, code="-", size="-"):
573 pass # Override to suppress request logging
574
575 - args,_ = parser.parse_known_args()
575 + args, add_args = parser.parse_known_args()
576 + #add_args to dict
577 + glob_args = {}
578 + for arg in add_args:
579 + if "=" in arg:
580 + key, value = arg.split("=", 1)
581 + key = key.lstrip("-")
582 + glob_args[key] = value
583 + set_global_kwargs(**glob_args)
584
585 # Get configuration from environment
586 port = args.port or int(os.environ.get("WEB_UI_PORT", 0)) or None
webui/index.html
+22 -14
@@ -232,7 +232,7 @@
232 });
233 }
234 }">
235 -
235 +
236 <!-- Preview section -->
237 <div x-show="hasAttachments" class="preview-section">
238 <template x-for="(attachment, index) in attachments" :key="index">
@@ -246,12 +246,12 @@
246 <span class="extension" x-text="attachment.extension.toUpperCase()"></span>
247 </div>
248 </template>
249 - <button @click="attachments.splice(index, 1); hasAttachments = attachments.length > 0"
250 - class="remove-attachment">&times;</button>
249 + <button @click="attachments.splice(index, 1); hasAttachments = attachments.length > 0"
250 + class="remove-attachment">&times;</button>
251 </div>
252 </template>
253 </div>
254 -
254 +
255 <!-- Top row with input and buttons -->
256 <div class="input-row">
257 <!-- Attachment icon with tooltip -->
@@ -264,20 +264,21 @@
264 d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
265 </svg>
266 </label>
267 - <input type="file" id="file-input" accept=".png, .jpg, .jpeg, .txt, .pdf, .csv, .html, .json, .md, .py, .js, .sh, .css" multiple style="display: none"
268 - @change="handleFileUpload($event)">
267 + <input type="file" id="file-input"
268 + accept=".png, .jpg, .jpeg, .txt, .pdf, .csv, .html, .json, .md, .py, .js, .sh, .css"
269 + multiple style="display: none" @change="handleFileUpload($event)">
270
270 - <div x-show="showTooltip" class="tooltip">
271 - Limit: 4 attachments per message
271 + <div x-show="showTooltip" class="tooltip">
272 + Limit: 4 attachments per message
273 + </div>
274 </div>
273 - </div>
275
275 - <!-- Text input -->
276 - <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
276 + <!-- Text input -->
277 + <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
278
278 - <div id="chat-buttons-wrapper">
279 - <!-- Send button -->
280 - <button class="chat-button" id="send-button" aria-label="Send message">
279 + <div id="chat-buttons-wrapper">
280 + <!-- Send button -->
281 + <button class="chat-button" id="send-button" aria-label="Send message">
282 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
283 <path d="M25 20 L75 50 L25 80" fill="none" stroke="currentColor" stroke-width="15">
284 </path>
@@ -377,6 +378,13 @@
378 @input="field.value = $event.target.value">
379 </template>
380
381 + <!-- Password field -->
382 + <template x-if="field.type === 'password'">
383 + <input type="password" :class="field.classes" :value="field.value"
384 + :readonly="field.readonly === true"
385 + @input="field.value = $event.target.value">
386 + </template>
387 +
388 <!-- Textarea field -->
389 <template x-if="field.type === 'textarea'">
390 <textarea :class="field.classes" :value="field.value"
webui/settings.css
+345 -323
@@ -1,336 +1,358 @@
1 select {
2 - transition: none;
3 - }
4 -
5 - .modal-overlay {
6 - position: fixed;
7 - top: 0;
8 - left: 0;
9 - right: 0;
10 - bottom: 0;
11 - background: rgba(0, 0, 0, 0.5);
12 - display: flex;
13 - align-items: center;
14 - justify-content: center;
15 - z-index: 2001;
16 - }
17 -
18 - .modal-container {
19 - background: var(--color-panel);
20 - color: var(--color-primary);
21 - border-radius: 0.5rem;
22 - width: 90%;
23 - max-width: 800px;
24 - max-height: 90vh;
25 - display: flex;
26 - flex-direction: column;
27 - }
28 -
2 + transition: none;
3 +}
4 +
5 +.modal-overlay {
6 + position: fixed;
7 + top: 0;
8 + left: 0;
9 + right: 0;
10 + bottom: 0;
11 + background: rgba(0, 0, 0, 0.5);
12 + display: flex;
13 + align-items: center;
14 + justify-content: center;
15 + z-index: 2001;
16 +}
17 +
18 +.modal-container {
19 + background: var(--color-panel);
20 + color: var(--color-primary);
21 + border-radius: 0.5rem;
22 + width: 90%;
23 + max-width: 800px;
24 + max-height: 90vh;
25 + display: flex;
26 + flex-direction: column;
27 +}
28 +
29 +.modal-header {
30 + padding: 1em 1em;
31 + border-bottom: 1px solid var(--color-border);
32 +}
33 +
34 +.modal-header h2 {
35 + font-size: 1.25rem;
36 + margin: 0;
37 +}
38 +
39 +.modal-header ul {
40 + margin-bottom: 0;
41 +}
42 +
43 +.modal-content {
44 + padding: var(--spacing-sm);
45 + overflow-y: auto;
46 + flex-grow: 1;
47 + background-clip: border-box;
48 + border: 6px solid transparent;
49 + transition: all 0.3s ease;
50 + margin-bottom: 0;
51 + padding-bottom: 0;
52 +}
53 +
54 +.modal-content::-webkit-scrollbar {
55 + width: 6px;
56 + height: 6px;
57 +}
58 +
59 +.modal-content::-webkit-scrollbar-track {
60 + background: transparent;
61 + margin: 4px 0;
62 + border-radius: 6px;
63 +}
64 +
65 +.modal-content::-webkit-scrollbar-thumb {
66 + background-color: rgba(155, 155, 155, 0.5);
67 + border-radius: 6px;
68 + transition: background-color 0.2s ease;
69 +}
70 +
71 +.modal-content::-webkit-scrollbar-thumb:hover {
72 + background-color: rgba(155, 155, 155, 0.7);
73 +}
74 +
75 +.modal-footer {
76 + padding: var(--spacing-sm);
77 + border-top: 1px solid var(--color-border);
78 + display: flex;
79 + justify-content: flex-end;
80 + gap: 1rem;
81 + background: var(--color-background);
82 +}
83 +
84 +.section {
85 + margin-bottom: 2rem;
86 + padding: 1rem;
87 + border: 1px solid var(--color-border);
88 + border-radius: 0.5rem;
89 +}
90 +
91 +.section-title {
92 + font-size: 1.25rem;
93 + font-weight: bold;
94 + margin-bottom: 0.5rem;
95 +}
96 +
97 +.section-description {
98 + color: #666;
99 + margin-bottom: 1rem;
100 +}
101 +
102 +.field {
103 + display: grid;
104 + margin-block-start: 1rem;
105 + grid-template-columns: 250px 1fr;
106 + align-items: center;
107 +}
108 +
109 +.field.field-full {
110 + grid-template-columns: 1fr;
111 +}
112 +
113 +.field-label {
114 + display: flex;
115 + flex-direction: column;
116 +}
117 +
118 +.field-title {
119 + font-weight: bold;
120 +}
121 +
122 +.field-description {
123 + color: #666;
124 + font-size: 0.875rem;
125 + margin-top: 0.25rem;
126 + margin-bottom: 0.5rem;
127 +}
128 +
129 +.field-control {
130 + width: 100%;
131 + display: flex;
132 + align-items: center;
133 +}
134 +
135 +input[type="text"] {
136 + width: 100%;
137 + padding: 0.5rem;
138 + border: 1px solid #ddd;
139 + border-radius: 0.25rem;
140 + font-family: "Rubik", Arial, Helvetica, sans-serif;
141 +}
142 +
143 +input[type="password"] {
144 + width: 100%;
145 + padding: 0.5rem;
146 + border: 1px solid #ddd;
147 + border-radius: 0.25rem;
148 + font-family: "Rubik", Arial, Helvetica, sans-serif;
149 +}
150 +
151 +textarea {
152 + width: 100%;
153 + min-height: 100px;
154 + padding: 0.5rem;
155 + border: 1px solid #ddd;
156 + border-radius: 0.25rem;
157 + font-family: Roboto Mono;
158 + scroll-behavior: smooth;
159 + resize: none;
160 + /*-/* scrollbar padding */
161 + background-clip: border-box;
162 + border: 6px solid transparent;
163 + transition: all 0.3s ease;
164 +}
165 +
166 +.toggle {
167 + position: relative;
168 + display: inline-block;
169 + width: 60px;
170 + height: 34px;
171 + margin: 0;
172 +}
173 +
174 +.toggle input {
175 + opacity: 0;
176 + width: 0;
177 + height: 0;
178 +}
179 +
180 +.toggler {
181 + position: absolute;
182 + cursor: pointer;
183 + top: 0;
184 + left: 0;
185 + right: 0;
186 + bottom: 0;
187 + background-color: #ccc;
188 + transition: 0.4s;
189 + border-radius: 34px;
190 +}
191 +
192 +.toggler:before {
193 + position: absolute;
194 + content: "";
195 + height: 26px;
196 + width: 26px;
197 + left: 4px;
198 + bottom: 4px;
199 + background-color: white;
200 + transition: 0.4s;
201 + border-radius: 50%;
202 +}
203 +
204 +input:checked + .toggler {
205 + background-color: #2196f3;
206 +}
207 +
208 +input:checked + .toggler:before {
209 + transform: translateX(26px);
210 +}
211 +
212 +input[type="range"] {
213 + width: 100%;
214 +}
215 +
216 +.range-value {
217 + min-width: 3em;
218 + text-align: right;
219 +}
220 +
221 +#buttons-container {
222 + display: flex;
223 + gap: 0.875rem;
224 +}
225 +
226 +.btn {
227 + padding: 0.7rem 1.5rem;
228 + border-radius: 0.25rem;
229 + cursor: pointer;
230 + border: none;
231 + font-size: 0.875rem;
232 + font-family: "Rubik", Arial, Helvetica, sans-serif;
233 +}
234 +
235 +.btn-ok {
236 + background: #3270e2;
237 + color: white;
238 + transition: background 0.3s ease-in-out;
239 +}
240 +
241 +.btn-ok:hover {
242 + background: #3265c0;
243 +}
244 +
245 +.btn-ok:active {
246 + background: #345693;
247 +}
248 +
249 +.btn-cancel {
250 + background: #ddd;
251 + color: #333;
252 + transition: background 0.3s ease-in-out;
253 +}
254 +
255 +.btn-cancel:hover {
256 + background: #a6a6a6;
257 +}
258 +
259 +.btn-cancel:active {
260 + background: #808080;
261 +}
262 +
263 +.btn-field {
264 + background: #2196f3;
265 + color: white;
266 + width: fit-content;
267 +}
268 +
269 +.btn-field:disabled {
270 + background: #ccc;
271 + cursor: not-allowed;
272 +}
273 +
274 +select {
275 + width: 100%;
276 + padding: 0.5rem;
277 + border: 1px solid #ddd;
278 + border-radius: 0.25rem;
279 + background-color: white;
280 + font-size: inherit;
281 + cursor: pointer;
282 + font-family: "Rubik", Arial, Helvetica, sans-serif;
283 + outline: none;
284 +}
285 +
286 +select:disabled {
287 + background-color: #f5f5f5;
288 + cursor: not-allowed;
289 +}
290 +
291 +/* Style for navigation links */
292 +nav ul {
293 + list-style-type: none;
294 + padding: 0;
295 +}
296 +
297 +nav ul li {
298 + display: inline;
299 + margin-right: 1rem;
300 +}
301 +
302 +nav ul li a {
303 + text-decoration: none;
304 + color: #2196f3;
305 + font-weight: bold;
306 +}
307 +
308 +nav ul li a:hover {
309 + text-decoration: underline;
310 +}
311 +
312 +@media (max-width: 768px) {
313 .modal-header {
30 - padding: 0.875rem 2rem;
31 - border-bottom: 1px solid var(--color-border);
32 - }
33 -
34 - .modal-content {
35 - padding: 1.5rem;
36 - overflow-y: auto;
37 - flex-grow: 1;
38 - background-clip: border-box;
39 - border: 6px solid transparent;
40 - transition: all 0.3s ease;
41 - margin-bottom: 0;
42 - padding-bottom: 0;
314 + padding: var(--spacing-sm);
315 + border-bottom: 1px solid var(--color-border);
316 }
44 -
45 - .modal-content::-webkit-scrollbar {
46 - width: 6px;
47 - height: 6px;
317 +
318 + .modal-header h2 {
319 + font-size: 1.25rem;
320 + margin: 0;
321 }
49 -
50 - .modal-content::-webkit-scrollbar-track {
51 - background: transparent;
52 - margin: 4px 0;
53 - border-radius: 6px;
54 - }
55 -
56 - .modal-content::-webkit-scrollbar-thumb {
57 - background-color: rgba(155, 155, 155, 0.5);
58 - border-radius: 6px;
59 - transition: background-color 0.2s ease;
60 - }
61 -
62 - .modal-content::-webkit-scrollbar-thumb:hover {
63 - background-color: rgba(155, 155, 155, 0.7);
322 +
323 + .modal-content {
324 + padding: 1rem;
325 + overflow-y: auto;
326 + flex-grow: 1;
327 }
65 -
328 +
329 .modal-footer {
67 - padding: 1.5rem 2rem;
68 - border-top: 1px solid var(--color-border);
69 - display: flex;
70 - justify-content: flex-end;
71 - gap: 1rem;
72 - background: var(--color-background);
73 - }
74 -
75 - .section {
76 - margin-bottom: 2rem;
77 - padding: 1rem;
78 - border: 1px solid var(--color-border);
79 - border-radius: 0.5rem;
80 - }
81 -
82 - .section-title {
83 - font-size: 1.25rem;
84 - font-weight: bold;
85 - margin-bottom: 0.5rem;
86 - }
87 -
88 - .section-description {
89 - color: #666;
90 - margin-bottom: 1rem;
91 - }
92 -
93 - .field {
94 - display: grid;
95 - margin-block-start: 1rem;
96 - grid-template-columns: 250px 1fr;
97 - align-items: center;
98 - }
99 -
100 - .field.field-full {
101 - grid-template-columns: 1fr;
102 - }
103 -
104 - .field-label {
105 - display: flex;
106 - flex-direction: column;
107 - }
108 -
109 - .field-title {
110 - font-weight: bold;
111 - }
112 -
113 - .field-description {
114 - color: #666;
115 - font-size: 0.875rem;
116 - margin-top: 0.25rem;
117 - margin-bottom: 0.5rem;
118 - }
119 -
120 - .field-control {
121 - width: 100%;
122 - display: flex;
123 - align-items: center;
124 - }
125 -
126 - input[type="text"] {
127 - width: 100%;
128 - padding: 0.5rem;
129 - border: 1px solid #ddd;
130 - border-radius: 0.25rem;
131 - font-family: "Rubik", Arial, Helvetica, sans-serif;
330 + padding: var(--spacing-sm);
331 }
133 -
134 - textarea {
135 - width: 100%;
136 - min-height: 100px;
137 - padding: 0.5rem;
138 - border: 1px solid #ddd;
139 - border-radius: 0.25rem;
140 - font-family: Roboto Mono;
141 - scroll-behavior: smooth;
142 - resize: none;
143 - /*-/* scrollbar padding */
144 - background-clip: border-box;
145 - border: 6px solid transparent;
146 - transition: all 0.3s ease;
147 - }
148 -
149 - .toggle {
150 - position: relative;
151 - display: inline-block;
152 - width: 60px;
153 - height: 34px;
154 - margin: 0;
155 - }
156 -
157 - .toggle input {
158 - opacity: 0;
159 - width: 0;
160 - height: 0;
161 - }
162 -
163 - .toggler {
164 - position: absolute;
165 - cursor: pointer;
166 - top: 0;
167 - left: 0;
168 - right: 0;
169 - bottom: 0;
170 - background-color: #ccc;
171 - transition: .4s;
172 - border-radius: 34px;
173 - }
174 -
175 - .toggler:before {
176 - position: absolute;
177 - content: "";
178 - height: 26px;
179 - width: 26px;
180 - left: 4px;
181 - bottom: 4px;
182 - background-color: white;
183 - transition: .4s;
184 - border-radius: 50%;
185 - }
186 -
187 - input:checked+.toggler {
188 - background-color: #2196F3;
189 - }
190 -
191 - input:checked+.toggler:before {
192 - transform: translateX(26px);
193 - }
194 -
195 - input[type="range"] {
196 - width: 100%;
197 - }
198 -
199 - .range-value {
200 - min-width: 3em;
201 - text-align: right;
202 - }
203 -
332 +
333 #buttons-container {
205 - display: flex;
206 - gap: 0.875rem;
207 - }
208 -
209 - .btn {
210 - padding: 0.7rem 1.5rem;
211 - border-radius: 0.25rem;
212 - cursor: pointer;
213 - border: none;
214 - font-size: 0.875rem;
215 - font-family: "Rubik", Arial, Helvetica, sans-serif;
216 - }
217 -
218 - .btn-ok {
219 - background: #3270e2;
220 - color: white;
221 - transition: background 0.3s ease-in-out;
222 - }
223 -
224 - .btn-ok:hover{
225 - background: #3265c0;
226 - }
227 -
228 - .btn-ok:active{
229 - background: #345693;
334 + display: flex;
335 + gap: 1rem;
336 + margin: 0 auto;
337 }
231 -
232 - .btn-cancel {
233 - background: #ddd;
234 - color: #333;
235 - transition: background 0.3s ease-in-out;
236 - }
237 -
238 - .btn-cancel:hover {
239 - background: #a6a6a6
240 - }
241 -
242 - .btn-cancel:active {
243 - background: #808080
244 - }
245 -
246 - .btn-field {
247 - background: #2196F3;
248 - color: white;
249 - width: fit-content;
250 - }
251 -
252 - .btn-field:disabled {
253 - background: #ccc;
254 - cursor: not-allowed;
255 - }
256 -
257 - select {
258 - width: 100%;
259 - padding: 0.5rem;
260 - border: 1px solid #ddd;
261 - border-radius: 0.25rem;
262 - background-color: white;
263 - font-size: inherit;
264 - cursor: pointer;
265 - font-family: "Rubik", Arial, Helvetica, sans-serif;
266 - outline: none;
267 - }
268 -
269 - select:disabled {
270 - background-color: #f5f5f5;
271 - cursor: not-allowed;
272 - }
273 -
274 - /* Style for navigation links */
275 - nav ul {
276 - list-style-type: none;
277 - padding: 0;
338 +
339 + .section {
340 + margin-bottom: 1.5rem;
341 + padding: 1rem;
342 + border: 1px solid var(--color-border);
343 + border-radius: 0.5rem;
344 }
279 -
280 - nav ul li {
281 - display: inline;
282 - margin-right: 1rem;
345 +
346 + .field-control {
347 + width: 100%;
348 }
284 -
285 - nav ul li a {
286 - text-decoration: none;
287 - color: #2196F3;
288 - font-weight: bold;
349 + .field-description {
350 + padding-bottom: var(--spacing-sm);
351 }
290 -
291 - nav ul li a:hover {
292 - text-decoration: underline;
352 + .field {
353 + padding-top: var(--spacing-xs);
354 + padding-bottom: var(--spacing-xs);
355 + display: block;
356 + align-items: center;
357 }
294 -
295 - @media (max-width: 768px) {
296 - .modal-header {
297 - padding: 0.7rem 2rem;
298 - border-bottom: 1px solid var(--color-border);
299 - }
300 -
301 - .modal-content {
302 - padding: 1rem;
303 - overflow-y: auto;
304 - flex-grow: 1;
305 - }
306 -
307 - .modal-footer {
308 - padding: 1.5rem;
309 - }
310 -
311 - #buttons-container {
312 - display: flex;
313 - gap: 1rem;
314 - margin: 0 auto;
315 - }
316 -
317 - .section {
318 - margin-bottom: 1.5rem;
319 - padding: 1rem;
320 - border: 1px solid var(--color-border);
321 - border-radius: 0.5rem;
322 - }
323 -
324 - .field-control {
325 - width: 100%;
326 - }
327 - .field-description {
328 - padding-bottom: var(--spacing-sm);
329 - }
330 - .field {
331 - padding-top: var(--spacing-xs);
332 - padding-bottom: var(--spacing-xs);
333 - display: block;
334 - align-items: center;
335 - }
336 - }
\ No newline at end of file
358 +}