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">×</button>
249
+ <button @click="attachments.splice(index, 1); hasAttachments = attachments.length > 0"
250
+ class="remove-attachment">×</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
+}