flock-core 0.4.0b43__py3-none-any.whl → 0.4.0b45__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/cli/manage_agents.py +19 -4
- flock/core/api/__init__.py +1 -2
- flock/core/api/endpoints.py +150 -218
- flock/core/api/main.py +134 -653
- flock/core/api/service.py +214 -0
- flock/core/flock.py +192 -134
- flock/core/flock_agent.py +31 -0
- flock/webapp/app/api/agent_management.py +135 -164
- flock/webapp/app/api/execution.py +76 -85
- flock/webapp/app/api/flock_management.py +60 -33
- flock/webapp/app/chat.py +233 -0
- flock/webapp/app/config.py +6 -3
- flock/webapp/app/dependencies.py +95 -0
- flock/webapp/app/main.py +320 -906
- flock/webapp/app/services/flock_service.py +183 -161
- flock/webapp/run.py +176 -100
- flock/webapp/static/css/chat.css +227 -0
- flock/webapp/static/css/components.css +167 -0
- flock/webapp/static/css/header.css +39 -0
- flock/webapp/static/css/layout.css +46 -0
- flock/webapp/static/css/sidebar.css +127 -0
- flock/webapp/templates/base.html +6 -1
- flock/webapp/templates/chat.html +60 -0
- flock/webapp/templates/chat_settings.html +20 -0
- flock/webapp/templates/flock_editor.html +1 -1
- flock/webapp/templates/partials/_agent_detail_form.html +8 -7
- flock/webapp/templates/partials/_agent_list.html +3 -3
- flock/webapp/templates/partials/_agent_manager_view.html +3 -4
- flock/webapp/templates/partials/_chat_container.html +9 -0
- flock/webapp/templates/partials/_chat_messages.html +13 -0
- flock/webapp/templates/partials/_chat_settings_form.html +65 -0
- flock/webapp/templates/partials/_execution_form.html +2 -2
- flock/webapp/templates/partials/_execution_view_container.html +1 -1
- flock/webapp/templates/partials/_flock_properties_form.html +2 -2
- flock/webapp/templates/partials/_registry_viewer_content.html +3 -3
- flock/webapp/templates/partials/_sidebar.html +17 -1
- flock/webapp/templates/registry_viewer.html +3 -3
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/METADATA +1 -1
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/RECORD +42 -31
- flock/webapp/static/css/custom.css +0 -612
- flock/webapp/templates/partials/_agent_manager_view_old.html +0 -19
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/licenses/LICENSE +0 -0
flock/webapp/app/main.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# src/flock/webapp/app/main.py
|
|
2
2
|
import json
|
|
3
|
-
import os # Needed for environment variable helpers
|
|
4
3
|
import shutil
|
|
5
|
-
import sys # For path
|
|
6
4
|
import urllib.parse
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
from fastapi import FastAPI, File, Form, Query, Request, UploadFile
|
|
@@ -11,1064 +10,479 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
|
|
11
10
|
from fastapi.staticfiles import StaticFiles
|
|
12
11
|
from fastapi.templating import Jinja2Templates
|
|
13
12
|
|
|
13
|
+
from flock.core.api.endpoints import create_api_router
|
|
14
|
+
from flock.core.api.run_store import RunStore
|
|
15
|
+
|
|
16
|
+
# Import core Flock components and API related modules
|
|
17
|
+
from flock.core.flock import Flock # For type hinting
|
|
18
|
+
from flock.core.logging.logging import get_logger # For logging
|
|
19
|
+
|
|
20
|
+
# Import UI-specific routers
|
|
14
21
|
from flock.webapp.app.api import (
|
|
15
22
|
agent_management,
|
|
16
23
|
execution,
|
|
17
24
|
flock_management,
|
|
18
25
|
registry_viewer,
|
|
19
26
|
)
|
|
20
|
-
|
|
21
|
-
# Import config functions
|
|
22
27
|
from flock.webapp.app.config import (
|
|
23
|
-
DEFAULT_THEME_NAME,
|
|
28
|
+
DEFAULT_THEME_NAME,
|
|
24
29
|
FLOCK_FILES_DIR,
|
|
25
|
-
THEMES_DIR,
|
|
30
|
+
THEMES_DIR,
|
|
26
31
|
get_current_theme_name,
|
|
27
|
-
# set_current_theme_name, # Not directly used in main.py, but available
|
|
28
32
|
)
|
|
33
|
+
|
|
34
|
+
# Import dependency management and config
|
|
35
|
+
from flock.webapp.app.dependencies import (
|
|
36
|
+
get_pending_custom_endpoints_and_clear,
|
|
37
|
+
set_global_flock_services,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Import service functions (which now expect app_state)
|
|
29
41
|
from flock.webapp.app.services.flock_service import (
|
|
30
|
-
|
|
42
|
+
clear_current_flock_service,
|
|
31
43
|
create_new_flock_service,
|
|
32
44
|
get_available_flock_files,
|
|
33
|
-
get_current_flock_filename,
|
|
34
|
-
get_current_flock_instance,
|
|
35
45
|
get_flock_preview_service,
|
|
36
46
|
load_flock_from_file_service,
|
|
47
|
+
# Note: get_current_flock_instance/filename are removed from service,
|
|
48
|
+
# as main.py will use request.app.state for this.
|
|
37
49
|
)
|
|
38
50
|
from flock.webapp.app.theme_mapper import alacritty_to_pico
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Find the 'src/flock' directory - This can be removed if THEMES_DIR from config is sufficient
|
|
43
|
-
# flock_base_dir = (
|
|
44
|
-
# Path(__file__).resolve().parent.parent.parent
|
|
45
|
-
# ) # src/flock/webapp/app -> src/flock
|
|
46
|
-
|
|
47
|
-
# Calculate themes directory relative to the flock base dir - This can be removed
|
|
48
|
-
# themes_dir = flock_base_dir / "themes"
|
|
52
|
+
logger = get_logger("webapp.main")
|
|
49
53
|
|
|
50
|
-
# Ensure the parent ('src') is in the path for core imports
|
|
51
|
-
# This path manipulation might still be needed if core imports are relative in a specific way
|
|
52
|
-
flock_webapp_dir = Path(__file__).resolve().parent.parent # src/flock/webapp/
|
|
53
|
-
flock_base_dir = flock_webapp_dir.parent # src/flock/
|
|
54
|
-
src_dir = flock_base_dir.parent # src/
|
|
55
|
-
if str(src_dir) not in sys.path:
|
|
56
|
-
sys.path.insert(0, str(src_dir))
|
|
57
54
|
|
|
58
55
|
try:
|
|
59
56
|
from flock.core.logging.formatters.themed_formatter import (
|
|
60
57
|
load_theme_from_file,
|
|
61
58
|
)
|
|
62
|
-
|
|
63
59
|
THEME_LOADER_AVAILABLE = True
|
|
64
|
-
# themes_dir is now imported from config
|
|
65
60
|
except ImportError:
|
|
66
|
-
|
|
67
|
-
"Warning: Could not import flock.core theme loading utilities.",
|
|
68
|
-
file=sys.stderr,
|
|
69
|
-
)
|
|
61
|
+
logger.warning("Could not import flock.core theme loading utilities.")
|
|
70
62
|
THEME_LOADER_AVAILABLE = False
|
|
71
|
-
# THEMES_DIR will be None if not imported, or its value from config
|
|
72
63
|
|
|
73
|
-
# ---
|
|
74
|
-
|
|
64
|
+
# --- .env helpers (copied from original main.py for self-containment) ---
|
|
65
|
+
ENV_FILE_PATH = Path(".env") #Path(os.getenv("FLOCK_WEB_ENV_FILE", Path.home() / ".flock" / ".env"))
|
|
66
|
+
#ENV_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
75
67
|
SHOW_SECRETS_KEY = "SHOW_SECRETS"
|
|
76
68
|
|
|
77
|
-
def
|
|
69
|
+
def load_env_file_web() -> dict[str, str]:
|
|
78
70
|
env_vars: dict[str, str] = {}
|
|
79
|
-
if not
|
|
80
|
-
|
|
81
|
-
with open(ENV_FILE) as f:
|
|
82
|
-
lines = f.readlines()
|
|
71
|
+
if not ENV_FILE_PATH.exists(): return env_vars
|
|
72
|
+
with open(ENV_FILE_PATH) as f: lines = f.readlines()
|
|
83
73
|
for line in lines:
|
|
84
74
|
line = line.strip()
|
|
85
|
-
if not line:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
env_vars[line] = ""
|
|
90
|
-
continue
|
|
91
|
-
if "=" in line:
|
|
92
|
-
k, v = line.split("=", 1)
|
|
93
|
-
env_vars[k] = v
|
|
94
|
-
else:
|
|
95
|
-
env_vars[line] = ""
|
|
75
|
+
if not line: env_vars[""] = ""; continue
|
|
76
|
+
if line.startswith("#"): env_vars[line] = ""; continue
|
|
77
|
+
if "=" in line: k, v = line.split("=", 1); env_vars[k] = v
|
|
78
|
+
else: env_vars[line] = ""
|
|
96
79
|
return env_vars
|
|
97
80
|
|
|
98
|
-
def
|
|
81
|
+
def save_env_file_web(env_vars: dict[str, str]):
|
|
99
82
|
try:
|
|
100
|
-
with open(
|
|
83
|
+
with open(ENV_FILE_PATH, "w") as f:
|
|
101
84
|
for k, v in env_vars.items():
|
|
102
|
-
if k.startswith("#"):
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
print(f"[Settings] Failed to save .env: {e}")
|
|
110
|
-
|
|
111
|
-
def is_sensitive(key: str) -> bool:
|
|
112
|
-
patterns = ["key", "token", "secret", "password", "api", "pat"]
|
|
113
|
-
low = key.lower()
|
|
85
|
+
if k.startswith("#"): f.write(f"{k}\n")
|
|
86
|
+
elif not k: f.write("\n")
|
|
87
|
+
else: f.write(f"{k}={v}\n")
|
|
88
|
+
except Exception as e: logger.error(f"[Settings] Failed to save .env: {e}")
|
|
89
|
+
|
|
90
|
+
def is_sensitive_web(key: str) -> bool:
|
|
91
|
+
patterns = ["key", "token", "secret", "password", "api", "pat"]; low = key.lower()
|
|
114
92
|
return any(p in low for p in patterns)
|
|
115
93
|
|
|
116
|
-
def
|
|
117
|
-
if not value:
|
|
118
|
-
|
|
119
|
-
if len(value) <= 4:
|
|
120
|
-
return "••••"
|
|
94
|
+
def mask_sensitive_value_web(value: str) -> str:
|
|
95
|
+
if not value: return value
|
|
96
|
+
if len(value) <= 4: return "••••"
|
|
121
97
|
return value[:2] + "•" * (len(value) - 4) + value[-2:]
|
|
122
98
|
|
|
123
|
-
def
|
|
99
|
+
def get_show_secrets_setting_web(env_vars: dict[str, str]) -> bool:
|
|
124
100
|
return env_vars.get(SHOW_SECRETS_KEY, "false").lower() == "true"
|
|
125
101
|
|
|
126
|
-
def
|
|
127
|
-
env_vars =
|
|
102
|
+
def set_show_secrets_setting_web(show: bool):
|
|
103
|
+
env_vars = load_env_file_web()
|
|
128
104
|
env_vars[SHOW_SECRETS_KEY] = str(show)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
105
|
+
save_env_file_web(env_vars)
|
|
106
|
+
# --- End .env helpers ---
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@asynccontextmanager
|
|
110
|
+
async def lifespan(app: FastAPI):
|
|
111
|
+
logger.info("FastAPI application starting up...")
|
|
112
|
+
# Flock instance and RunStore are expected to be set on app.state
|
|
113
|
+
# by `start_unified_server` in `webapp/run.py` *before* uvicorn starts the app.
|
|
114
|
+
# The call to `set_global_flock_services` also happens there.
|
|
115
|
+
|
|
116
|
+
# Add custom routes if any were passed during server startup
|
|
117
|
+
# These are retrieved from the dependency module where `start_unified_server` stored them.
|
|
118
|
+
pending_endpoints = get_pending_custom_endpoints_and_clear()
|
|
119
|
+
if pending_endpoints:
|
|
120
|
+
flock_instance_from_state: Flock | None = getattr(app.state, "flock_instance", None)
|
|
121
|
+
if flock_instance_from_state:
|
|
122
|
+
from flock.core.api.main import (
|
|
123
|
+
FlockAPI, # Local import for this specific task
|
|
124
|
+
)
|
|
125
|
+
# Create a temporary FlockAPI service object just for adding routes
|
|
126
|
+
temp_flock_api_service = FlockAPI(
|
|
127
|
+
flock_instance_from_state,
|
|
128
|
+
custom_endpoints=pending_endpoints
|
|
129
|
+
)
|
|
130
|
+
temp_flock_api_service.add_custom_routes_to_app(app)
|
|
131
|
+
logger.info(f"Lifespan: Added {len(pending_endpoints)} custom API routes to main app.")
|
|
132
|
+
else:
|
|
133
|
+
logger.warning("Lifespan: Pending custom endpoints found, but no Flock instance in app.state. Cannot add custom routes.")
|
|
134
|
+
yield
|
|
135
|
+
logger.info("FastAPI application shutting down...")
|
|
132
136
|
|
|
133
|
-
app = FastAPI(title="Flock UI")
|
|
137
|
+
app = FastAPI(title="Flock Web UI & API", lifespan=lifespan)
|
|
134
138
|
|
|
135
139
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
136
|
-
app.mount(
|
|
137
|
-
"/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static"
|
|
138
|
-
)
|
|
140
|
+
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
|
139
141
|
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
app.include_router(
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
# Ensure execution router is imported and included BEFORE it's referenced by the renamed route
|
|
148
|
-
app.include_router(
|
|
149
|
-
execution.router, prefix="/api/flocks", tags=["Execution API"]
|
|
150
|
-
)
|
|
151
|
-
app.include_router(
|
|
152
|
-
registry_viewer.router, prefix="/api/registry", tags=["Registry API"]
|
|
153
|
-
)
|
|
154
|
-
|
|
143
|
+
core_api_router = create_api_router()
|
|
144
|
+
app.include_router(core_api_router, prefix="/api", tags=["Flock API Core"])
|
|
145
|
+
app.include_router(flock_management.router, prefix="/ui/api/flock", tags=["UI Flock Management"])
|
|
146
|
+
app.include_router(agent_management.router, prefix="/ui/api/flock", tags=["UI Agent Management"])
|
|
147
|
+
app.include_router(execution.router, prefix="/ui/api/flock", tags=["UI Execution"])
|
|
148
|
+
app.include_router(registry_viewer.router, prefix="/ui/api/registry", tags=["UI Registry"])
|
|
155
149
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return "" # Return empty if theme loading isn't possible
|
|
160
|
-
|
|
161
|
-
active_theme_name = theme_name or DEFAULT_THEME_NAME
|
|
150
|
+
def generate_theme_css_web(theme_name: str | None) -> str:
|
|
151
|
+
if not THEME_LOADER_AVAILABLE or THEMES_DIR is None: return ""
|
|
152
|
+
active_theme_name = theme_name or get_current_theme_name() or DEFAULT_THEME_NAME
|
|
162
153
|
theme_filename = f"{active_theme_name}.toml"
|
|
163
|
-
theme_path = THEMES_DIR / theme_filename
|
|
164
|
-
|
|
154
|
+
theme_path = THEMES_DIR / theme_filename
|
|
165
155
|
if not theme_path.exists():
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
170
|
-
# Optionally load the default theme file if the requested one isn't found
|
|
171
|
-
theme_filename = f"{DEFAULT_THEME_NAME}.toml"
|
|
172
|
-
theme_path = THEMES_DIR / theme_filename
|
|
156
|
+
logger.warning(f"Theme file not found: {theme_path}. Using default: {DEFAULT_THEME_NAME}.toml")
|
|
157
|
+
theme_path = THEMES_DIR / f"{DEFAULT_THEME_NAME}.toml"
|
|
158
|
+
active_theme_name = DEFAULT_THEME_NAME
|
|
173
159
|
if not theme_path.exists():
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return "" # Return empty if even default isn't found
|
|
179
|
-
active_theme_name = (
|
|
180
|
-
DEFAULT_THEME_NAME # Update active name if defaulted
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
try:
|
|
184
|
-
theme_dict = load_theme_from_file(str(theme_path))
|
|
185
|
-
except Exception as e:
|
|
186
|
-
print(f"Error loading theme file {theme_path}: {e}", file=sys.stderr)
|
|
187
|
-
return "" # Return empty on error
|
|
188
|
-
|
|
189
|
-
# --- Define TOML Color -> CSS Variable Mapping ---
|
|
190
|
-
# This mapping is crucial and may need adjustment based on theme intent & Pico usage
|
|
191
|
-
css_vars = {}
|
|
192
|
-
try:
|
|
193
|
-
# Basic Colors
|
|
194
|
-
# Base colors
|
|
195
|
-
css_vars["--pico-background-color"] = theme_dict["colors"]["primary"].get("background") # Main background
|
|
196
|
-
css_vars["--pico-color"] = theme_dict["colors"]["primary"].get("foreground") # Main text
|
|
197
|
-
|
|
198
|
-
# Headings
|
|
199
|
-
css_vars["--pico-h1-color"] = theme_dict["colors"]["selection"].get("text") # Primary heading
|
|
200
|
-
css_vars["--pico-h2-color"] = theme_dict["colors"]["selection"].get("text") # Secondary heading
|
|
201
|
-
css_vars["--pico-h3-color"] = theme_dict["colors"]["primary"].get("foreground") # Body heading
|
|
202
|
-
css_vars["--pico-muted-color"] = theme_dict["colors"]["selection"].get("text") # Muted/subtext
|
|
203
|
-
css_vars["--pico-primary-inverse"] = theme_dict["colors"]["cursor"].get("text") # Contrast on primary
|
|
204
|
-
css_vars["--pico-contrast"] = theme_dict["colors"]["primary"].get("background") # Contrast text on dark
|
|
205
|
-
css_vars["--pico-contrast-inverse"] = theme_dict["colors"]["primary"].get("foreground") # Contrast text on dark
|
|
206
|
-
|
|
207
|
-
# Primary interaction
|
|
208
|
-
css_vars["--pico-primary"] = theme_dict["colors"]["normal"].get("blue")
|
|
209
|
-
css_vars["--pico-primary-hover"] = theme_dict["colors"]["bright"].get("blue")
|
|
210
|
-
css_vars["--pico-primary-focus"] = f"rgba({theme_dict['colors']['bright'].get('blue')}, 0.25)"
|
|
211
|
-
css_vars["--pico-primary-active"] = theme_dict["colors"]["normal"].get("blue")
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
# Secondary interaction
|
|
216
|
-
css_vars["--pico-secondary"] = theme_dict["colors"]["normal"].get("magenta")
|
|
217
|
-
css_vars["--pico-secondary-hover"] = theme_dict["colors"]["bright"].get("magenta")
|
|
218
|
-
css_vars["--pico-secondary-focus"] = f"rgba({theme_dict['colors']['bright'].get('magenta')}, 0.25)"
|
|
219
|
-
css_vars["--pico-secondary-active"] = theme_dict["colors"]["normal"].get("magenta")
|
|
220
|
-
|
|
221
|
-
# Cards and containers
|
|
222
|
-
css_vars["--pico-card-background-color"] = theme_dict["colors"]["primary"].get("background")
|
|
223
|
-
css_vars["--pico-card-border-color"] = theme_dict["colors"]["bright"].get("black") # Mid-tone, visible on bright bg
|
|
224
|
-
css_vars["--pico-card-sectioning-background-color"] = theme_dict["colors"]["selection"].get("background") # Subtle contrast
|
|
225
|
-
css_vars["--pico-border-color"] = theme_dict["colors"]["bright"].get("black")
|
|
226
|
-
css_vars["--pico-muted-border-color"] = theme_dict["colors"]["normal"].get("black") # More subtle than main border
|
|
227
|
-
|
|
228
|
-
# Forms
|
|
229
|
-
css_vars["--pico-form-element-background-color"] = theme_dict["colors"]["primary"].get("background")
|
|
230
|
-
css_vars["--pico-form-element-border-color"] = theme_dict["colors"]["bright"].get("black")
|
|
231
|
-
css_vars["--pico-form-element-color"] = theme_dict["colors"]["primary"].get("foreground")
|
|
232
|
-
css_vars["--pico-form-element-focus-color"] = theme_dict["colors"]["bright"].get("blue")
|
|
233
|
-
css_vars["--pico-form-element-placeholder-color"] = theme_dict["colors"]["bright"].get("black")
|
|
234
|
-
css_vars["--pico-form-element-active-border-color"] = theme_dict["colors"]["bright"].get("blue")
|
|
235
|
-
css_vars["--pico-form-element-active-background-color"] = theme_dict["colors"]["selection"].get("background")
|
|
236
|
-
css_vars["--pico-form-element-disabled-background-color"] = theme_dict["colors"]["normal"].get("black")
|
|
237
|
-
css_vars["--pico-form-element-disabled-border-color"] = theme_dict["colors"]["bright"].get("black")
|
|
238
|
-
css_vars["--pico-form-element-invalid-border-color"] = theme_dict["colors"]["normal"].get("red")
|
|
239
|
-
css_vars["--pico-form-element-invalid-focus-color"] = theme_dict["colors"]["bright"].get("red")
|
|
240
|
-
|
|
241
|
-
# Buttons
|
|
242
|
-
css_vars["--pico-button-base-background-color"] = theme_dict["colors"]["primary"].get("background")
|
|
243
|
-
css_vars["--pico-button-base-color"] = theme_dict["colors"]["primary"].get("foreground")
|
|
244
|
-
css_vars["--pico-button-hover-background-color"] = theme_dict["colors"]["selection"].get("background")
|
|
245
|
-
css_vars["--pico-button-hover-color"] = theme_dict["colors"]["selection"].get("text")
|
|
246
|
-
|
|
247
|
-
# Code blocks
|
|
248
|
-
css_vars["--pico-code-background-color"] = theme_dict["colors"]["cursor"].get("text") # Background behind code
|
|
249
|
-
css_vars["--pico-code-color"] = theme_dict["colors"]["primary"].get("foreground") # Code text
|
|
250
|
-
css_vars["--pico-code-kbd-background-color"] = theme_dict["colors"]["selection"].get("background")
|
|
251
|
-
css_vars["--pico-code-kbd-color"] = theme_dict["colors"]["selection"].get("text")
|
|
252
|
-
css_vars["--pico-code-tag-color"] = theme_dict["colors"]["normal"].get("blue") # Tag elements
|
|
253
|
-
css_vars["--pico-code-property-color"] = theme_dict["colors"]["normal"].get("green") # CSS property names
|
|
254
|
-
css_vars["--pico-code-value-color"] = theme_dict["colors"]["normal"].get("red") # Values and literals
|
|
255
|
-
css_vars["--pico-code-comment-color"] = theme_dict["colors"]["bright"].get("black") # Dim comment color
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
# Semantic markup
|
|
259
|
-
css_vars["--pico-mark-background-color"] = theme_dict["colors"]["normal"].get("yellow") + "33"
|
|
260
|
-
css_vars["--pico-mark-color"] = theme_dict["colors"]["primary"].get("foreground")
|
|
261
|
-
css_vars["--pico-ins-color"] = theme_dict["colors"]["normal"].get("green")
|
|
262
|
-
css_vars["--pico-del-color"] = theme_dict["colors"]["normal"].get("red")
|
|
263
|
-
# Deleted content - red
|
|
264
|
-
# Custom flock vars (mapped previously, ensure they are kept)
|
|
265
|
-
css_vars["--flock-sidebar-background"] = theme_dict["colors"]["primary"].get("background")# css_vars["--pico-card-background-color"] # Example: Link sidebar to card background
|
|
266
|
-
css_vars["--flock-header-background"] = theme_dict["colors"]["selection"].get("background") # Example: Link header to card background
|
|
267
|
-
css_vars["--flock-error-color"] = theme_dict["colors"]["normal"].get("red", "#dc3545")
|
|
268
|
-
css_vars["--flock-success-color"] = theme_dict["colors"]["normal"].get("green", "#28a745")
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
#css_vars.update(pico_vars)
|
|
273
|
-
|
|
274
|
-
except KeyError as e:
|
|
275
|
-
print(
|
|
276
|
-
f"Warning: Missing expected key in theme '{active_theme_name}': {e}. CSS may be incomplete.",
|
|
277
|
-
file=sys.stderr,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
if not css_vars:
|
|
281
|
-
return "" # Return empty if no variables were mapped
|
|
160
|
+
logger.warning(f"Default theme file not found: {theme_path}. No theme CSS.")
|
|
161
|
+
return ""
|
|
162
|
+
try: theme_dict = load_theme_from_file(str(theme_path))
|
|
163
|
+
except Exception as e: logger.error(f"Error loading theme {theme_path}: {e}"); return ""
|
|
282
164
|
|
|
283
165
|
pico_vars = alacritty_to_pico(theme_dict)
|
|
166
|
+
if not pico_vars: return ""
|
|
284
167
|
css_rules = [f" {name}: {value};" for name, value in pico_vars.items()]
|
|
285
|
-
|
|
286
|
-
# Apply overrides within the currently active theme selector for better specificity
|
|
287
|
-
# We could get the theme name passed in, or maybe check the <html> tag attribute?
|
|
288
|
-
# For simplicity, let's assume we want to override Pico's dark theme vars when a theme is loaded.
|
|
289
|
-
# A better approach might involve removing data-theme="dark" and applying theme to :root
|
|
290
|
-
# or having specific data-theme selectors for each flock theme.
|
|
291
|
-
# Let's try applying to [data-theme="dark"] first.
|
|
292
|
-
selector = '[data-theme="dark"]'
|
|
293
168
|
css_string = ":root {\n" + "\n".join(css_rules) + "\n}"
|
|
294
|
-
|
|
295
|
-
print(
|
|
296
|
-
f"--- Generated CSS for theme '{active_theme_name}' ---"
|
|
297
|
-
) # Debugging print
|
|
298
|
-
print(css_string) # Debugging print
|
|
299
|
-
print(
|
|
300
|
-
"----------------------------------------------------"
|
|
301
|
-
) # Debugging print
|
|
302
169
|
return css_string
|
|
303
170
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
request: Request,
|
|
307
|
-
error: str = None,
|
|
308
|
-
success: str = None,
|
|
309
|
-
ui_mode: str = "standalone",
|
|
171
|
+
def get_base_context_web(
|
|
172
|
+
request: Request, error: str = None, success: str = None, ui_mode: str = "standalone"
|
|
310
173
|
) -> dict:
|
|
311
|
-
|
|
312
|
-
|
|
174
|
+
flock_instance_from_state: Flock | None = getattr(request.app.state, "flock_instance", None)
|
|
175
|
+
current_flock_filename_from_state: str | None = getattr(request.app.state, "flock_filename", None)
|
|
176
|
+
theme_name = get_current_theme_name()
|
|
177
|
+
theme_css = generate_theme_css_web(theme_name)
|
|
313
178
|
return {
|
|
314
179
|
"request": request,
|
|
315
|
-
"current_flock":
|
|
316
|
-
"current_filename":
|
|
180
|
+
"current_flock": flock_instance_from_state,
|
|
181
|
+
"current_filename": current_flock_filename_from_state,
|
|
317
182
|
"error_message": error,
|
|
318
183
|
"success_message": success,
|
|
319
184
|
"ui_mode": ui_mode,
|
|
320
|
-
"theme_css": theme_css,
|
|
321
|
-
"active_theme_name": theme_name,
|
|
185
|
+
"theme_css": theme_css,
|
|
186
|
+
"active_theme_name": theme_name,
|
|
187
|
+
"chat_enabled": getattr(request.app.state, "chat_enabled", False),
|
|
322
188
|
}
|
|
323
189
|
|
|
324
|
-
|
|
325
|
-
# --- Main Page Routes ---
|
|
326
|
-
@app.get("/", response_class=HTMLResponse)
|
|
190
|
+
@app.get("/", response_class=HTMLResponse, tags=["UI Pages"])
|
|
327
191
|
async def page_dashboard(
|
|
328
|
-
request: Request,
|
|
329
|
-
error: str = None,
|
|
330
|
-
success: str = None,
|
|
331
|
-
ui_mode: str = Query(
|
|
332
|
-
None
|
|
333
|
-
), # Default to None to detect if it was explicitly passed
|
|
192
|
+
request: Request, error: str = None, success: str = None, ui_mode: str = Query(None)
|
|
334
193
|
):
|
|
335
|
-
# Determine effective ui_mode
|
|
336
194
|
effective_ui_mode = ui_mode
|
|
337
|
-
flock_is_preloaded =
|
|
195
|
+
flock_is_preloaded = hasattr(request.app.state, "flock_instance") and request.app.state.flock_instance is not None
|
|
338
196
|
|
|
339
|
-
if effective_ui_mode is None:
|
|
340
|
-
if flock_is_preloaded
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
pass
|
|
351
|
-
|
|
352
|
-
# Conditional flock clearing based on the *effective* ui_mode
|
|
353
|
-
if effective_ui_mode != "scoped":
|
|
354
|
-
# If we are about to enter standalone mode, and a flock might have been
|
|
355
|
-
# preloaded (e.g. user navigated from /?ui_mode=scoped to /?ui_mode=standalone),
|
|
356
|
-
# ensure it's cleared for a true standalone experience.
|
|
357
|
-
if flock_is_preloaded: # Clear only if one was there
|
|
358
|
-
clear_current_flock()
|
|
359
|
-
|
|
360
|
-
context = get_base_context(request, error, success, effective_ui_mode)
|
|
197
|
+
if effective_ui_mode is None:
|
|
198
|
+
effective_ui_mode = "scoped" if flock_is_preloaded else "standalone"
|
|
199
|
+
if effective_ui_mode == "scoped":
|
|
200
|
+
return RedirectResponse(url=f"/?ui_mode=scoped&initial_load=true", status_code=307)
|
|
201
|
+
|
|
202
|
+
if effective_ui_mode == "standalone" and flock_is_preloaded:
|
|
203
|
+
clear_current_flock_service(request.app.state) # Pass app.state
|
|
204
|
+
logger.info("Switched to standalone mode, cleared preloaded Flock instance from app.state.")
|
|
205
|
+
|
|
206
|
+
context = get_base_context_web(request, error, success, effective_ui_mode)
|
|
207
|
+
flock_in_state = hasattr(request.app.state, "flock_instance") and request.app.state.flock_instance is not None
|
|
361
208
|
|
|
362
209
|
if effective_ui_mode == "scoped":
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
"/ui/htmx/execution-view-container"
|
|
366
|
-
)
|
|
367
|
-
else:
|
|
368
|
-
context["initial_content_url"] = "/ui/htmx/scoped-no-flock-view"
|
|
369
|
-
else: # Standalone mode
|
|
210
|
+
context["initial_content_url"] = "/ui/htmx/execution-view-container" if flock_in_state else "/ui/htmx/scoped-no-flock-view"
|
|
211
|
+
else:
|
|
370
212
|
context["initial_content_url"] = "/ui/htmx/load-flock-view"
|
|
371
|
-
|
|
372
213
|
return templates.TemplateResponse("base.html", context)
|
|
373
214
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
request: Request,
|
|
378
|
-
success: str = None,
|
|
379
|
-
error: str = None,
|
|
380
|
-
ui_mode: str = Query("standalone"),
|
|
215
|
+
@app.get("/ui/editor/{section:path}", response_class=HTMLResponse, tags=["UI Pages"])
|
|
216
|
+
async def page_editor_section(
|
|
217
|
+
request: Request, section: str, success: str = None, error: str = None, ui_mode: str = Query("standalone")
|
|
381
218
|
):
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if not flock:
|
|
219
|
+
flock_instance_from_state: Flock | None = getattr(request.app.state, "flock_instance", None)
|
|
220
|
+
if not flock_instance_from_state:
|
|
385
221
|
err_msg = "No flock loaded. Please load or create a flock first."
|
|
386
|
-
# Preserve ui_mode on redirect if it was passed
|
|
387
222
|
redirect_url = f"/?error={urllib.parse.quote(err_msg)}"
|
|
388
|
-
if ui_mode == "scoped":
|
|
389
|
-
redirect_url += "&ui_mode=scoped"
|
|
390
|
-
return RedirectResponse(url=redirect_url, status_code=303)
|
|
391
|
-
context = get_base_context(request, error, success, ui_mode)
|
|
392
|
-
context["initial_content_url"] = "/api/flocks/htmx/flock-properties-form"
|
|
393
|
-
return templates.TemplateResponse("base.html", context)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
@app.get("/ui/editor/agents", response_class=HTMLResponse)
|
|
397
|
-
async def page_editor_agents(
|
|
398
|
-
request: Request,
|
|
399
|
-
success: str = None,
|
|
400
|
-
error: str = None,
|
|
401
|
-
ui_mode: str = Query("standalone"),
|
|
402
|
-
):
|
|
403
|
-
# ... (same as before) ...
|
|
404
|
-
flock = get_current_flock_instance()
|
|
405
|
-
if not flock:
|
|
406
|
-
# Preserve ui_mode on redirect
|
|
407
|
-
redirect_url = (
|
|
408
|
-
f"/?error={urllib.parse.quote('No flock loaded for agent view.')}"
|
|
409
|
-
)
|
|
410
|
-
if ui_mode == "scoped":
|
|
411
|
-
redirect_url += "&ui_mode=scoped"
|
|
223
|
+
if ui_mode == "scoped": redirect_url += "&ui_mode=scoped"
|
|
412
224
|
return RedirectResponse(url=redirect_url, status_code=303)
|
|
413
|
-
context = get_base_context(request, error, success, ui_mode)
|
|
414
|
-
context["initial_content_url"] = "/ui/htmx/agent-manager-view"
|
|
415
|
-
return templates.TemplateResponse("base.html", context)
|
|
416
|
-
|
|
417
225
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
if not flock:
|
|
427
|
-
# Preserve ui_mode on redirect
|
|
428
|
-
redirect_url = (
|
|
429
|
-
f"/?error={urllib.parse.quote('No flock loaded to execute.')}"
|
|
430
|
-
)
|
|
431
|
-
if ui_mode == "scoped":
|
|
432
|
-
redirect_url += "&ui_mode=scoped"
|
|
433
|
-
return RedirectResponse(url=redirect_url, status_code=303)
|
|
434
|
-
context = get_base_context(request, error, success, ui_mode)
|
|
435
|
-
# UPDATED initial_content_url
|
|
436
|
-
context["initial_content_url"] = "/ui/htmx/execution-view-container"
|
|
226
|
+
context = get_base_context_web(request, error, success, ui_mode)
|
|
227
|
+
content_map = {
|
|
228
|
+
"properties": "/ui/api/flock/htmx/flock-properties-form",
|
|
229
|
+
"agents": "/ui/htmx/agent-manager-view",
|
|
230
|
+
"execute": "/ui/htmx/execution-view-container"
|
|
231
|
+
}
|
|
232
|
+
context["initial_content_url"] = content_map.get(section, "/ui/htmx/load-flock-view")
|
|
233
|
+
if section not in content_map: context["error_message"] = "Invalid editor section."
|
|
437
234
|
return templates.TemplateResponse("base.html", context)
|
|
438
235
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
async def page_registry(
|
|
443
|
-
request: Request,
|
|
444
|
-
error: str = None,
|
|
445
|
-
success: str = None,
|
|
446
|
-
ui_mode: str = Query("standalone"),
|
|
447
|
-
):
|
|
448
|
-
context = get_base_context(request, error, success, ui_mode)
|
|
236
|
+
@app.get("/ui/registry", response_class=HTMLResponse, tags=["UI Pages"])
|
|
237
|
+
async def page_registry(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
|
|
238
|
+
context = get_base_context_web(request, error, success, ui_mode)
|
|
449
239
|
context["initial_content_url"] = "/ui/htmx/registry-viewer"
|
|
450
240
|
return templates.TemplateResponse("base.html", context)
|
|
451
241
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
request
|
|
456
|
-
error: str = None,
|
|
457
|
-
success: str = None,
|
|
458
|
-
ui_mode: str = Query("standalone"),
|
|
459
|
-
):
|
|
460
|
-
clear_current_flock()
|
|
461
|
-
# Create page should arguably not be accessible in scoped mode directly via URL,
|
|
462
|
-
# as the sidebar link will be hidden. If accessed, treat as standalone.
|
|
463
|
-
context = get_base_context(
|
|
464
|
-
request, error, success, "standalone"
|
|
465
|
-
) # Force standalone for direct access
|
|
242
|
+
@app.get("/ui/create", response_class=HTMLResponse, tags=["UI Pages"])
|
|
243
|
+
async def page_create(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
|
|
244
|
+
clear_current_flock_service(request.app.state) # Pass app.state
|
|
245
|
+
context = get_base_context_web(request, error, success, "standalone")
|
|
466
246
|
context["initial_content_url"] = "/ui/htmx/create-flock-form"
|
|
467
247
|
return templates.TemplateResponse("base.html", context)
|
|
468
248
|
|
|
249
|
+
@app.get("/ui/htmx/sidebar", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
250
|
+
async def htmx_get_sidebar(request: Request, ui_mode: str = Query("standalone")):
|
|
251
|
+
return templates.TemplateResponse("partials/_sidebar.html", get_base_context_web(request, ui_mode=ui_mode))
|
|
469
252
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
request: Request, ui_mode: str = Query("standalone")
|
|
474
|
-
):
|
|
475
|
-
# ... (same as before) ...
|
|
476
|
-
return templates.TemplateResponse(
|
|
477
|
-
"partials/_sidebar.html",
|
|
478
|
-
{
|
|
479
|
-
"request": request,
|
|
480
|
-
"current_flock": get_current_flock_instance(),
|
|
481
|
-
"ui_mode": ui_mode,
|
|
482
|
-
},
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
@app.get("/ui/htmx/header-flock-status", response_class=HTMLResponse)
|
|
487
|
-
async def htmx_get_header_flock_status(
|
|
488
|
-
request: Request, ui_mode: str = Query("standalone")
|
|
489
|
-
):
|
|
490
|
-
# ui_mode isn't strictly needed for this partial's content, but good to accept if passed by hx-get
|
|
491
|
-
return templates.TemplateResponse(
|
|
492
|
-
"partials/_header_flock_status.html",
|
|
493
|
-
{
|
|
494
|
-
"request": request,
|
|
495
|
-
"current_flock": get_current_flock_instance(),
|
|
496
|
-
"current_filename": get_current_flock_filename(),
|
|
497
|
-
},
|
|
498
|
-
)
|
|
499
|
-
|
|
253
|
+
@app.get("/ui/htmx/header-flock-status", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
254
|
+
async def htmx_get_header_flock_status(request: Request, ui_mode: str = Query("standalone")):
|
|
255
|
+
return templates.TemplateResponse("partials/_header_flock_status.html", get_base_context_web(request, ui_mode=ui_mode))
|
|
500
256
|
|
|
501
|
-
@app.get("/ui/htmx/load-flock-view", response_class=HTMLResponse)
|
|
502
|
-
async def htmx_get_load_flock_view(
|
|
503
|
-
request
|
|
504
|
-
error: str = None,
|
|
505
|
-
success: str = None,
|
|
506
|
-
ui_mode: str = Query("standalone"),
|
|
507
|
-
):
|
|
508
|
-
# ... (same as before) ...
|
|
509
|
-
# This view is part of the "standalone" functionality.
|
|
510
|
-
# If somehow accessed in scoped mode, it might be confusing, but let it render.
|
|
511
|
-
return templates.TemplateResponse(
|
|
512
|
-
"partials/_load_manager_view.html",
|
|
513
|
-
{
|
|
514
|
-
"request": request,
|
|
515
|
-
"error_message": error,
|
|
516
|
-
"success_message": success,
|
|
517
|
-
"ui_mode": ui_mode, # Pass for consistency, though not directly used in this partial
|
|
518
|
-
},
|
|
519
|
-
)
|
|
257
|
+
@app.get("/ui/htmx/load-flock-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
258
|
+
async def htmx_get_load_flock_view(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
|
|
259
|
+
return templates.TemplateResponse("partials/_load_manager_view.html", get_base_context_web(request, error, success, ui_mode))
|
|
520
260
|
|
|
521
|
-
|
|
522
|
-
@app.get("/ui/htmx/dashboard-flock-file-list", response_class=HTMLResponse)
|
|
261
|
+
@app.get("/ui/htmx/dashboard-flock-file-list", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
523
262
|
async def htmx_get_dashboard_flock_file_list_partial(request: Request):
|
|
524
|
-
|
|
525
|
-
return templates.TemplateResponse(
|
|
526
|
-
"partials/_dashboard_flock_file_list.html",
|
|
527
|
-
{"request": request, "flock_files": get_available_flock_files()},
|
|
528
|
-
)
|
|
263
|
+
return templates.TemplateResponse("partials/_dashboard_flock_file_list.html", {"request": request, "flock_files": get_available_flock_files()})
|
|
529
264
|
|
|
530
|
-
|
|
531
|
-
@app.get("/ui/htmx/dashboard-default-action-pane", response_class=HTMLResponse)
|
|
265
|
+
@app.get("/ui/htmx/dashboard-default-action-pane", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
532
266
|
async def htmx_get_dashboard_default_action_pane(request: Request):
|
|
533
|
-
|
|
534
|
-
return HTMLResponse("""
|
|
535
|
-
<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;">
|
|
536
|
-
<p>Select a Flock from the list to view its details and load it into the editor.</p>
|
|
537
|
-
<hr>
|
|
538
|
-
<p>Or, create a new Flock or upload an existing one using the "Create New Flock" option in the sidebar.</p>
|
|
539
|
-
</article>
|
|
540
|
-
""")
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
@app.get(
|
|
544
|
-
"/ui/htmx/dashboard-flock-properties-preview/{filename}",
|
|
545
|
-
response_class=HTMLResponse,
|
|
546
|
-
)
|
|
547
|
-
async def htmx_get_dashboard_flock_properties_preview(
|
|
548
|
-
request: Request, filename: str
|
|
549
|
-
):
|
|
550
|
-
# ... (same as before) ...
|
|
551
|
-
preview_flock_data = get_flock_preview_service(filename)
|
|
552
|
-
return templates.TemplateResponse(
|
|
553
|
-
"partials/_dashboard_flock_properties_preview.html",
|
|
554
|
-
{
|
|
555
|
-
"request": request,
|
|
556
|
-
"selected_filename": filename,
|
|
557
|
-
"preview_flock": preview_flock_data,
|
|
558
|
-
},
|
|
559
|
-
)
|
|
267
|
+
return HTMLResponse("""<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;"><p>Select a Flock from the list to view its details and load it into the editor.</p><hr><p>Or, create a new Flock or upload an existing one using the "Create New Flock" option in the sidebar.</p></article>""")
|
|
560
268
|
|
|
269
|
+
@app.get("/ui/htmx/dashboard-flock-properties-preview/{filename}", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
270
|
+
async def htmx_get_dashboard_flock_properties_preview(request: Request, filename: str):
|
|
271
|
+
preview_flock_data = get_flock_preview_service(filename)
|
|
272
|
+
return templates.TemplateResponse("partials/_dashboard_flock_properties_preview.html", {"request": request, "selected_filename": filename, "preview_flock": preview_flock_data})
|
|
561
273
|
|
|
562
|
-
@app.get("/ui/htmx/create-flock-form", response_class=HTMLResponse)
|
|
563
|
-
async def htmx_get_create_flock_form(
|
|
564
|
-
request
|
|
565
|
-
error: str = None,
|
|
566
|
-
success: str = None,
|
|
567
|
-
ui_mode: str = Query("standalone"),
|
|
568
|
-
):
|
|
569
|
-
# ... (same as before) ...
|
|
570
|
-
# This view is part of the "standalone" functionality.
|
|
571
|
-
return templates.TemplateResponse(
|
|
572
|
-
"partials/_create_flock_form.html",
|
|
573
|
-
{
|
|
574
|
-
"request": request,
|
|
575
|
-
"error_message": error,
|
|
576
|
-
"success_message": success,
|
|
577
|
-
"ui_mode": ui_mode, # Pass for consistency
|
|
578
|
-
},
|
|
579
|
-
)
|
|
580
|
-
|
|
274
|
+
@app.get("/ui/htmx/create-flock-form", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
275
|
+
async def htmx_get_create_flock_form(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
|
|
276
|
+
return templates.TemplateResponse("partials/_create_flock_form.html", get_base_context_web(request, error, success, ui_mode))
|
|
581
277
|
|
|
582
|
-
@app.get("/ui/htmx/agent-manager-view", response_class=HTMLResponse)
|
|
278
|
+
@app.get("/ui/htmx/agent-manager-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
583
279
|
async def htmx_get_agent_manager_view(request: Request):
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
"<article class='error'><p>No flock loaded. Cannot manage agents.</p></article>"
|
|
589
|
-
)
|
|
280
|
+
context = get_base_context_web(request) # This gets flock from app.state
|
|
281
|
+
if not context.get("current_flock"): # Check if flock exists in the context
|
|
282
|
+
return HTMLResponse("<article class='error'><p>No flock loaded. Cannot manage agents.</p></article>")
|
|
283
|
+
# Pass the 'current_flock' from the context to the template as 'flock'
|
|
590
284
|
return templates.TemplateResponse(
|
|
591
285
|
"partials/_agent_manager_view.html",
|
|
592
|
-
{"request": request, "flock":
|
|
286
|
+
{"request": request, "flock": context.get("current_flock")}
|
|
593
287
|
)
|
|
594
288
|
|
|
595
|
-
|
|
596
|
-
@app.get("/ui/htmx/registry-viewer", response_class=HTMLResponse)
|
|
289
|
+
@app.get("/ui/htmx/registry-viewer", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
597
290
|
async def htmx_get_registry_viewer(request: Request):
|
|
598
|
-
|
|
599
|
-
return templates.TemplateResponse(
|
|
600
|
-
"partials/_registry_viewer_content.html", {"request": request}
|
|
601
|
-
)
|
|
291
|
+
return templates.TemplateResponse("partials/_registry_viewer_content.html", get_base_context_web(request))
|
|
602
292
|
|
|
603
|
-
|
|
604
|
-
# --- NEW HTMX ROUTE FOR THE EXECUTION VIEW CONTAINER ---
|
|
605
|
-
@app.get("/ui/htmx/execution-view-container", response_class=HTMLResponse)
|
|
293
|
+
@app.get("/ui/htmx/execution-view-container", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
606
294
|
async def htmx_get_execution_view_container(request: Request):
|
|
607
|
-
|
|
608
|
-
if not
|
|
609
|
-
|
|
610
|
-
"<article class='error'><p>No Flock loaded. Cannot execute.</p></article>"
|
|
611
|
-
)
|
|
612
|
-
return templates.TemplateResponse(
|
|
613
|
-
"partials/_execution_view_container.html", {"request": request}
|
|
614
|
-
)
|
|
615
|
-
|
|
295
|
+
context = get_base_context_web(request)
|
|
296
|
+
if not context.get("current_flock"): return HTMLResponse("<article class='error'><p>No Flock loaded. Cannot execute.</p></article>")
|
|
297
|
+
return templates.TemplateResponse("partials/_execution_view_container.html", context)
|
|
616
298
|
|
|
617
|
-
|
|
618
|
-
@app.get("/ui/htmx/scoped-no-flock-view", response_class=HTMLResponse)
|
|
299
|
+
@app.get("/ui/htmx/scoped-no-flock-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
619
300
|
async def htmx_scoped_no_flock_view(request: Request):
|
|
620
|
-
return HTMLResponse("""
|
|
621
|
-
<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;">
|
|
622
|
-
<hgroup>
|
|
623
|
-
<h2>Scoped Flock Mode</h2>
|
|
624
|
-
<h3>No Flock Loaded</h3>
|
|
625
|
-
</hgroup>
|
|
626
|
-
<p>This UI is in a scoped mode, expecting a Flock to be pre-loaded.</p>
|
|
627
|
-
<p>Please ensure the calling application provides a Flock instance.</p>
|
|
628
|
-
</article>
|
|
629
|
-
""")
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
# Endpoint to launch the UI in scoped mode with a preloaded flock
|
|
633
|
-
@app.post("/ui/launch-scoped", response_class=RedirectResponse)
|
|
634
|
-
async def launch_scoped_ui(
|
|
635
|
-
request: Request,
|
|
636
|
-
flock_data: dict, # This would be the flock's JSON data
|
|
637
|
-
# Potentially also receive filename if it's from a saved file
|
|
638
|
-
):
|
|
639
|
-
# Here, you would parse flock_data, create a Flock instance,
|
|
640
|
-
# and set it as the current flock using your flock_service methods.
|
|
641
|
-
# For now, let's assume flock_service has a method like:
|
|
642
|
-
# set_current_flock_from_data(data) -> bool (returns True if successful)
|
|
643
|
-
|
|
644
|
-
# This is a placeholder for actual flock loading logic
|
|
645
|
-
# from flock.core.entities.flock import Flock # Assuming Flock can be instantiated from dict
|
|
646
|
-
# from flock.webapp.app.services.flock_service import set_current_flock_instance, set_current_flock_filename
|
|
647
|
-
|
|
648
|
-
# try:
|
|
649
|
-
# # Assuming flock_data is a dict that can initialize a Flock object
|
|
650
|
-
# # You might need a more robust way to deserialize, e.g., using Pydantic models
|
|
651
|
-
# loaded_flock = Flock(**flock_data) # This is a simplistic example
|
|
652
|
-
# set_current_flock_instance(loaded_flock)
|
|
653
|
-
# # If the flock has a name or identifier, you might set it as well
|
|
654
|
-
# # set_current_flock_filename(flock_data.get("name", "scoped_flock")) # Example
|
|
655
|
-
#
|
|
656
|
-
# # Redirect to the agent editor or properties page in scoped mode
|
|
657
|
-
# # The page_dashboard will handle ui_mode=scoped and redirect/set initial content appropriately
|
|
658
|
-
# return RedirectResponse(url="/?ui_mode=scoped", status_code=303)
|
|
659
|
-
# except Exception as e:
|
|
660
|
-
# # Log error e
|
|
661
|
-
# # Redirect to an error page or the standalone dashboard with an error message
|
|
662
|
-
# error_msg = f"Failed to load flock for scoped view: {e}"
|
|
663
|
-
# return RedirectResponse(url=f"/?error={urllib.parse.quote(error_msg)}&ui_mode=standalone", status_code=303)
|
|
664
|
-
|
|
665
|
-
# For now, since we don't have the flock loading logic here,
|
|
666
|
-
# we'll just redirect. The calling service (`src/flock/core/api`)
|
|
667
|
-
# will need to ensure the flock is loaded into the webapp's session/state
|
|
668
|
-
# *before* redirecting to this UI.
|
|
669
|
-
|
|
670
|
-
# A more direct way if `load_flock_from_data_service` exists and sets it globally for the session:
|
|
671
|
-
# success = load_flock_from_data_service(flock_data, "scoped_runtime_flock") # example filename
|
|
672
|
-
# if success:
|
|
673
|
-
# return RedirectResponse(url="/ui/editor/agents?ui_mode=scoped", status_code=303) # or properties
|
|
674
|
-
# else:
|
|
675
|
-
# return RedirectResponse(url="/?error=Failed+to+load+scoped+flock&ui_mode=standalone", status_code=303)
|
|
676
|
-
|
|
677
|
-
# Given the current structure, the simplest way for an external service to "preload" a flock
|
|
678
|
-
# is to use the existing `load_flock_from_file_service` if the flock can be temporarily saved,
|
|
679
|
-
# or by enhancing `flock_service` to allow setting a Flock instance directly.
|
|
680
|
-
# Let's assume the flock is already loaded into the session by the calling API for now.
|
|
681
|
-
# The calling API will be responsible for calling a service function within the webapp's context.
|
|
682
|
-
|
|
683
|
-
# This endpoint's primary job is now to redirect to the UI in the correct mode.
|
|
684
|
-
# The actual loading of the flock should happen *before* this redirect,
|
|
685
|
-
# by the API server calling a service function within the webapp's context.
|
|
686
|
-
|
|
687
|
-
# For demonstration, let's imagine the calling API has already used a service
|
|
688
|
-
# to set the flock. We just redirect.
|
|
689
|
-
if get_current_flock_instance():
|
|
690
|
-
return RedirectResponse(
|
|
691
|
-
url="/ui/editor/agents?ui_mode=scoped", status_code=303
|
|
692
|
-
)
|
|
693
|
-
else:
|
|
694
|
-
# If no flock is loaded, go to the main page in scoped mode, which will show the "no flock" message.
|
|
695
|
-
return RedirectResponse(url="/?ui_mode=scoped", status_code=303)
|
|
696
|
-
|
|
301
|
+
return HTMLResponse("""<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;"><hgroup><h2>Scoped Flock Mode</h2><h3>No Flock Loaded</h3></hgroup><p>This UI is in a scoped mode, expecting a Flock to be pre-loaded.</p><p>Please ensure the calling application provides a Flock instance.</p></article>""")
|
|
697
302
|
|
|
698
|
-
# --- Action Routes
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
@app.post("/ui/load-flock-action/by-name", response_class=HTMLResponse)
|
|
703
|
-
async def ui_load_flock_by_name_action(
|
|
704
|
-
request: Request, selected_flock_filename: str = Form(...)
|
|
705
|
-
):
|
|
706
|
-
loaded_flock = load_flock_from_file_service(selected_flock_filename)
|
|
303
|
+
# --- Action Routes (POST requests for UI interactions) ---
|
|
304
|
+
@app.post("/ui/load-flock-action/by-name", response_class=HTMLResponse, tags=["UI Actions"])
|
|
305
|
+
async def ui_load_flock_by_name_action(request: Request, selected_flock_filename: str = Form(...)):
|
|
306
|
+
loaded_flock = load_flock_from_file_service(selected_flock_filename, request.app.state)
|
|
707
307
|
response_headers = {}
|
|
308
|
+
ui_mode_query = request.query_params.get("ui_mode", "standalone")
|
|
708
309
|
if loaded_flock:
|
|
709
|
-
|
|
710
|
-
response_headers["HX-Push-Url"] = "/ui/editor/properties"
|
|
711
|
-
response_headers["HX-Trigger"] = json.dumps(
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
)
|
|
717
|
-
return templates.TemplateResponse(
|
|
718
|
-
"partials/_flock_properties_form.html",
|
|
719
|
-
{
|
|
720
|
-
"request": request,
|
|
721
|
-
"flock": loaded_flock,
|
|
722
|
-
"current_filename": get_current_flock_filename(),
|
|
723
|
-
},
|
|
724
|
-
headers=response_headers,
|
|
725
|
-
)
|
|
310
|
+
success_message_text = f"Flock '{loaded_flock.name}' loaded from '{selected_flock_filename}'."
|
|
311
|
+
response_headers["HX-Push-Url"] = "/ui/editor/properties?ui_mode=" + request.query_params.get("ui_mode", "standalone")
|
|
312
|
+
response_headers["HX-Trigger"] = json.dumps({"flockLoaded": None, "notify": {"type": "success", "message": success_message_text}})
|
|
313
|
+
# Use get_base_context_web to ensure all necessary context vars are present for the partial
|
|
314
|
+
context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
|
|
315
|
+
return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
|
|
726
316
|
else:
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
)
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
@app.post("/ui/load-flock-action/by-upload", response_class=HTMLResponse)
|
|
741
|
-
async def ui_load_flock_by_upload_action(
|
|
742
|
-
request: Request, flock_file_upload: UploadFile = File(...)
|
|
743
|
-
):
|
|
744
|
-
error_message = None
|
|
745
|
-
filename_to_load = None
|
|
746
|
-
response_headers = {}
|
|
317
|
+
error_message_text = f"Failed to load flock file '{selected_flock_filename}'."
|
|
318
|
+
response_headers["HX-Trigger"] = json.dumps({"notify": {"type": "error", "message": error_message_text}})
|
|
319
|
+
context = get_base_context_web(request, error=error_message_text, ui_mode=ui_mode_query)
|
|
320
|
+
context["error_message_inline"] = error_message_text # For direct display in partial
|
|
321
|
+
return templates.TemplateResponse("partials/_load_manager_view.html", context, headers=response_headers)
|
|
322
|
+
|
|
323
|
+
@app.post("/ui/load-flock-action/by-upload", response_class=HTMLResponse, tags=["UI Actions"])
|
|
324
|
+
async def ui_load_flock_by_upload_action(request: Request, flock_file_upload: UploadFile = File(...)):
|
|
325
|
+
error_message_text, filename_to_load, response_headers = None, None, {}
|
|
326
|
+
ui_mode_query = request.query_params.get("ui_mode", "standalone")
|
|
327
|
+
|
|
747
328
|
if flock_file_upload and flock_file_upload.filename:
|
|
748
|
-
if not flock_file_upload.filename.endswith((".yaml", ".yml", ".flock")):
|
|
749
|
-
error_message = "Invalid file type."
|
|
329
|
+
if not flock_file_upload.filename.endswith((".yaml", ".yml", ".flock")): error_message_text = "Invalid file type."
|
|
750
330
|
else:
|
|
751
331
|
upload_path = FLOCK_FILES_DIR / flock_file_upload.filename
|
|
752
332
|
try:
|
|
753
|
-
with upload_path.open("wb") as buffer:
|
|
754
|
-
shutil.copyfileobj(flock_file_upload.file, buffer)
|
|
333
|
+
with upload_path.open("wb") as buffer: shutil.copyfileobj(flock_file_upload.file, buffer)
|
|
755
334
|
filename_to_load = flock_file_upload.filename
|
|
756
|
-
except Exception as e:
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
await flock_file_upload.close()
|
|
760
|
-
else:
|
|
761
|
-
error_message = "No file uploaded."
|
|
335
|
+
except Exception as e: error_message_text = f"Upload failed: {e}"
|
|
336
|
+
finally: await flock_file_upload.close()
|
|
337
|
+
else: error_message_text = "No file uploaded."
|
|
762
338
|
|
|
763
|
-
if filename_to_load and not
|
|
764
|
-
loaded_flock = load_flock_from_file_service(filename_to_load)
|
|
339
|
+
if filename_to_load and not error_message_text:
|
|
340
|
+
loaded_flock = load_flock_from_file_service(filename_to_load, request.app.state)
|
|
765
341
|
if loaded_flock:
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
)
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
},
|
|
784
|
-
headers=response_headers,
|
|
785
|
-
)
|
|
786
|
-
else:
|
|
787
|
-
error_message = f"Failed to process uploaded '{filename_to_load}'."
|
|
788
|
-
|
|
789
|
-
response_headers["HX-Trigger"] = json.dumps(
|
|
790
|
-
{
|
|
791
|
-
"notify": {
|
|
792
|
-
"type": "error",
|
|
793
|
-
"message": error_message or "Upload failed.",
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
)
|
|
797
|
-
return templates.TemplateResponse(
|
|
798
|
-
"partials/_create_flock_form.html",
|
|
799
|
-
{ # Changed target to create form on upload error
|
|
800
|
-
"request": request,
|
|
801
|
-
"error_message": error_message or "Upload action failed.",
|
|
802
|
-
},
|
|
803
|
-
headers=response_headers,
|
|
804
|
-
)
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
@app.post("/ui/create-flock", response_class=HTMLResponse)
|
|
808
|
-
async def ui_create_flock_action(
|
|
809
|
-
request: Request,
|
|
810
|
-
flock_name: str = Form(...),
|
|
811
|
-
default_model: str = Form(None),
|
|
812
|
-
description: str = Form(None),
|
|
813
|
-
):
|
|
342
|
+
success_message_text = f"Flock '{loaded_flock.name}' loaded from '{filename_to_load}'."
|
|
343
|
+
response_headers["HX-Push-Url"] = f"/ui/editor/properties?ui_mode={ui_mode_query}"
|
|
344
|
+
response_headers["HX-Trigger"] = json.dumps({"flockLoaded": None, "flockFileListChanged": None, "notify": {"type": "success", "message": success_message_text}})
|
|
345
|
+
# CORRECTED CALL:
|
|
346
|
+
context = get_base_context_web(request, success=success_message_text, ui_mode=ui_mode_query)
|
|
347
|
+
return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
|
|
348
|
+
else: error_message_text = f"Failed to process uploaded '{filename_to_load}'."
|
|
349
|
+
|
|
350
|
+
final_error_msg = error_message_text or "Upload failed."
|
|
351
|
+
response_headers["HX-Trigger"] = json.dumps({"notify": {"type": "error", "message": final_error_msg}})
|
|
352
|
+
# CORRECTED CALL:
|
|
353
|
+
context = get_base_context_web(request, error=final_error_msg, ui_mode=ui_mode_query)
|
|
354
|
+
return templates.TemplateResponse("partials/_create_flock_form.html", context, headers=response_headers)
|
|
355
|
+
|
|
356
|
+
@app.post("/ui/create-flock", response_class=HTMLResponse, tags=["UI Actions"])
|
|
357
|
+
async def ui_create_flock_action(request: Request, flock_name: str = Form(...), default_model: str = Form(None), description: str = Form(None)):
|
|
358
|
+
ui_mode_query = request.query_params.get("ui_mode", "standalone")
|
|
814
359
|
if not flock_name.strip():
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
"flockLoaded": None,
|
|
831
|
-
"notify": {"type": "success", "message": success_msg},
|
|
832
|
-
}
|
|
833
|
-
),
|
|
834
|
-
}
|
|
835
|
-
return templates.TemplateResponse(
|
|
836
|
-
"partials/_flock_properties_form.html",
|
|
837
|
-
{
|
|
838
|
-
"request": request,
|
|
839
|
-
"flock": new_flock,
|
|
840
|
-
"current_filename": get_current_flock_filename(),
|
|
841
|
-
},
|
|
842
|
-
headers=response_headers,
|
|
843
|
-
)
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
# --- Settings Page ---
|
|
847
|
-
@app.get("/ui/settings", response_class=HTMLResponse)
|
|
848
|
-
async def page_settings(
|
|
849
|
-
request: Request,
|
|
850
|
-
error: str = None,
|
|
851
|
-
success: str = None,
|
|
852
|
-
ui_mode: str = Query("standalone"),
|
|
853
|
-
):
|
|
854
|
-
"""Render the Settings top-level page which in turn loads the HTMX settings view."""
|
|
855
|
-
context = get_base_context(request, error, success, ui_mode)
|
|
360
|
+
# CORRECTED CALL:
|
|
361
|
+
context = get_base_context_web(request, error="Flock name cannot be empty.", ui_mode=ui_mode_query)
|
|
362
|
+
return templates.TemplateResponse("partials/_create_flock_form.html", context)
|
|
363
|
+
|
|
364
|
+
new_flock = create_new_flock_service(flock_name, default_model, description, request.app.state)
|
|
365
|
+
success_msg_text = f"New flock '{new_flock.name}' created. Configure properties and save."
|
|
366
|
+
response_headers = {"HX-Push-Url": f"/ui/editor/properties?ui_mode={ui_mode_query}", "HX-Trigger": json.dumps({"flockLoaded": None, "notify": {"type": "success", "message": success_msg_text}})}
|
|
367
|
+
# CORRECTED CALL:
|
|
368
|
+
context = get_base_context_web(request, success=success_msg_text, ui_mode=ui_mode_query)
|
|
369
|
+
return templates.TemplateResponse("partials/_flock_properties_form.html", context, headers=response_headers)
|
|
370
|
+
|
|
371
|
+
# --- Settings Page & Endpoints ---
|
|
372
|
+
@app.get("/ui/settings", response_class=HTMLResponse, tags=["UI Pages"])
|
|
373
|
+
async def page_settings(request: Request, error: str = None, success: str = None, ui_mode: str = Query("standalone")):
|
|
374
|
+
context = get_base_context_web(request, error, success, ui_mode)
|
|
856
375
|
context["initial_content_url"] = "/ui/htmx/settings-view"
|
|
857
376
|
return templates.TemplateResponse("base.html", context)
|
|
858
377
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
def _prepare_env_vars_for_template():
|
|
863
|
-
env_vars_raw = load_env_file()
|
|
864
|
-
show_secrets = get_show_secrets_setting(env_vars_raw)
|
|
378
|
+
def _prepare_env_vars_for_template_web():
|
|
379
|
+
env_vars_raw = load_env_file_web(); show_secrets = get_show_secrets_setting_web(env_vars_raw)
|
|
865
380
|
env_vars_list = []
|
|
866
381
|
for name, value in env_vars_raw.items():
|
|
867
|
-
if name.startswith("#") or name == "":
|
|
868
|
-
|
|
869
|
-
continue
|
|
870
|
-
display_value = (
|
|
871
|
-
value
|
|
872
|
-
if (not is_sensitive(name) or show_secrets)
|
|
873
|
-
else mask_sensitive_value(value)
|
|
874
|
-
)
|
|
382
|
+
if name.startswith("#") or name == "": continue
|
|
383
|
+
display_value = value if (not is_sensitive_web(name) or show_secrets) else mask_sensitive_value_web(value)
|
|
875
384
|
env_vars_list.append({"name": name, "value": display_value})
|
|
876
385
|
return env_vars_list, show_secrets
|
|
877
386
|
|
|
878
|
-
|
|
879
|
-
@app.get("/ui/htmx/settings-view", response_class=HTMLResponse)
|
|
387
|
+
@app.get("/ui/htmx/settings-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
880
388
|
async def htmx_get_settings_view(request: Request):
|
|
881
|
-
|
|
882
|
-
env_vars_list, show_secrets = _prepare_env_vars_for_template()
|
|
389
|
+
env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
|
|
883
390
|
theme_name = get_current_theme_name()
|
|
884
|
-
themes_available = []
|
|
885
|
-
|
|
886
|
-
themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")]
|
|
887
|
-
return templates.TemplateResponse(
|
|
888
|
-
"partials/_settings_view.html",
|
|
889
|
-
{
|
|
890
|
-
"request": request,
|
|
891
|
-
"env_vars": env_vars_list,
|
|
892
|
-
"show_secrets": show_secrets,
|
|
893
|
-
"themes": themes_available,
|
|
894
|
-
"current_theme": theme_name,
|
|
895
|
-
},
|
|
896
|
-
)
|
|
391
|
+
themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")] if THEMES_DIR and THEMES_DIR.exists() else []
|
|
392
|
+
return templates.TemplateResponse("partials/_settings_view.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets, "themes": themes_available, "current_theme": theme_name})
|
|
897
393
|
|
|
898
|
-
|
|
899
|
-
# --- Env Var Manager Endpoints ---
|
|
900
|
-
@app.post("/ui/htmx/toggle-show-secrets", response_class=HTMLResponse)
|
|
394
|
+
@app.post("/ui/htmx/toggle-show-secrets", response_class=HTMLResponse, tags=["UI Actions"])
|
|
901
395
|
async def htmx_toggle_show_secrets(request: Request):
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
env_vars_list, show_secrets = _prepare_env_vars_for_template()
|
|
907
|
-
return templates.TemplateResponse(
|
|
908
|
-
"partials/_env_vars_table.html",
|
|
909
|
-
{
|
|
910
|
-
"request": request,
|
|
911
|
-
"env_vars": env_vars_list,
|
|
912
|
-
"show_secrets": show_secrets,
|
|
913
|
-
},
|
|
914
|
-
)
|
|
396
|
+
env_vars_raw = load_env_file_web(); current = get_show_secrets_setting_web(env_vars_raw)
|
|
397
|
+
set_show_secrets_setting_web(not current)
|
|
398
|
+
env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
|
|
399
|
+
return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
|
|
915
400
|
|
|
916
|
-
|
|
917
|
-
@app.post("/ui/htmx/env-delete", response_class=HTMLResponse)
|
|
401
|
+
@app.post("/ui/htmx/env-delete", response_class=HTMLResponse, tags=["UI Actions"])
|
|
918
402
|
async def htmx_env_delete(request: Request, var_name: str = Form(...)):
|
|
919
|
-
env_vars_raw =
|
|
920
|
-
if var_name in env_vars_raw:
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
env_vars_list, show_secrets = _prepare_env_vars_for_template()
|
|
924
|
-
return templates.TemplateResponse(
|
|
925
|
-
"partials/_env_vars_table.html",
|
|
926
|
-
{
|
|
927
|
-
"request": request,
|
|
928
|
-
"env_vars": env_vars_list,
|
|
929
|
-
"show_secrets": show_secrets,
|
|
930
|
-
},
|
|
931
|
-
)
|
|
932
|
-
|
|
403
|
+
env_vars_raw = load_env_file_web()
|
|
404
|
+
if var_name in env_vars_raw: del env_vars_raw[var_name]; save_env_file_web(env_vars_raw)
|
|
405
|
+
env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
|
|
406
|
+
return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
|
|
933
407
|
|
|
934
|
-
@app.post("/ui/htmx/env-edit", response_class=HTMLResponse)
|
|
935
|
-
async def htmx_env_edit(
|
|
936
|
-
request: Request,
|
|
937
|
-
var_name: str = Form(...),
|
|
938
|
-
):
|
|
939
|
-
# New value is provided via HX-Prompt header
|
|
408
|
+
@app.post("/ui/htmx/env-edit", response_class=HTMLResponse, tags=["UI Actions"])
|
|
409
|
+
async def htmx_env_edit(request: Request, var_name: str = Form(...)):
|
|
940
410
|
new_value = request.headers.get("HX-Prompt")
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
},
|
|
951
|
-
)
|
|
952
|
-
env_vars_raw = load_env_file()
|
|
953
|
-
env_vars_raw[var_name] = new_value
|
|
954
|
-
save_env_file(env_vars_raw)
|
|
955
|
-
env_vars_list, show_secrets = _prepare_env_vars_for_template()
|
|
956
|
-
return templates.TemplateResponse(
|
|
957
|
-
"partials/_env_vars_table.html",
|
|
958
|
-
{
|
|
959
|
-
"request": request,
|
|
960
|
-
"env_vars": env_vars_list,
|
|
961
|
-
"show_secrets": show_secrets,
|
|
962
|
-
},
|
|
963
|
-
)
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
@app.get("/ui/htmx/env-add-form", response_class=HTMLResponse)
|
|
411
|
+
env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
|
|
412
|
+
if new_value is not None:
|
|
413
|
+
env_vars_raw = load_env_file_web()
|
|
414
|
+
env_vars_raw[var_name] = new_value
|
|
415
|
+
save_env_file_web(env_vars_raw)
|
|
416
|
+
env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
|
|
417
|
+
return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
|
|
418
|
+
|
|
419
|
+
@app.get("/ui/htmx/env-add-form", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
967
420
|
async def htmx_env_add_form(request: Request):
|
|
968
|
-
#
|
|
969
|
-
return HTMLResponse(
|
|
970
|
-
"""
|
|
971
|
-
<form hx-post='/ui/htmx/env-add' hx-target='#env-vars-container' hx-swap='outerHTML' style='display:flex; gap:0.5rem; margin-bottom:0.5rem;'>
|
|
972
|
-
<input name='var_name' placeholder='NAME' required style='flex:2;'>
|
|
973
|
-
<input name='var_value' placeholder='VALUE' style='flex:3;'>
|
|
974
|
-
<button type='submit'>Add</button>
|
|
975
|
-
</form>
|
|
976
|
-
"""
|
|
977
|
-
)
|
|
978
|
-
|
|
421
|
+
return HTMLResponse("""<form hx-post='/ui/htmx/env-add' hx-target='#env-vars-container' hx-swap='outerHTML' style='display:flex; gap:0.5rem; margin-bottom:0.5rem;'><input name='var_name' placeholder='NAME' required style='flex:2;'><input name='var_value' placeholder='VALUE' style='flex:3;'><button type='submit'>Add</button></form>""")
|
|
979
422
|
|
|
980
|
-
@app.post("/ui/htmx/env-add", response_class=HTMLResponse)
|
|
423
|
+
@app.post("/ui/htmx/env-add", response_class=HTMLResponse, tags=["UI Actions"])
|
|
981
424
|
async def htmx_env_add(request: Request, var_name: str = Form(...), var_value: str = Form("")):
|
|
982
|
-
env_vars_raw =
|
|
983
|
-
env_vars_raw[var_name] = var_value
|
|
984
|
-
|
|
985
|
-
env_vars_list, show_secrets
|
|
986
|
-
return templates.TemplateResponse(
|
|
987
|
-
"partials/_env_vars_table.html",
|
|
988
|
-
{
|
|
989
|
-
"request": request,
|
|
990
|
-
"env_vars": env_vars_list,
|
|
991
|
-
"show_secrets": show_secrets,
|
|
992
|
-
},
|
|
993
|
-
)
|
|
994
|
-
|
|
425
|
+
env_vars_raw = load_env_file_web()
|
|
426
|
+
env_vars_raw[var_name] = var_value; save_env_file_web(env_vars_raw)
|
|
427
|
+
env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
|
|
428
|
+
return templates.TemplateResponse("partials/_env_vars_table.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
|
|
995
429
|
|
|
996
|
-
|
|
997
|
-
@app.get("/ui/htmx/theme-preview", response_class=HTMLResponse)
|
|
430
|
+
@app.get("/ui/htmx/theme-preview", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
998
431
|
async def htmx_theme_preview(request: Request, theme: str = Query(None)):
|
|
999
|
-
theme_name = theme or get_current_theme_name()
|
|
1000
|
-
# Load theme data
|
|
432
|
+
theme_name = theme or get_current_theme_name() or DEFAULT_THEME_NAME
|
|
1001
433
|
try:
|
|
1002
434
|
theme_path = THEMES_DIR / f"{theme_name}.toml" if THEMES_DIR else None
|
|
1003
|
-
if not (theme_path and theme_path.exists()):
|
|
1004
|
-
return HTMLResponse("<p>Theme not found.</p>")
|
|
1005
|
-
from flock.core.logging.formatters.themed_formatter import (
|
|
1006
|
-
load_theme_from_file,
|
|
1007
|
-
)
|
|
435
|
+
if not (theme_path and theme_path.exists()): return HTMLResponse("<p>Theme not found.</p>")
|
|
1008
436
|
theme_data = load_theme_from_file(str(theme_path))
|
|
1009
|
-
except Exception as e:
|
|
1010
|
-
return HTMLResponse(f"<p>Error loading theme: {e}</p>")
|
|
1011
|
-
|
|
437
|
+
except Exception as e: return HTMLResponse(f"<p>Error loading theme: {e}</p>")
|
|
1012
438
|
css_vars = alacritty_to_pico(theme_data)
|
|
1013
439
|
css_vars_str = ":root {\n" + "\n".join([f" {k}: {v};" for k, v in css_vars.items()]) + "\n}"
|
|
440
|
+
main_colors = [("Background", css_vars.get("--pico-background-color")), ("Text", css_vars.get("--pico-color")), ("Primary", css_vars.get("--pico-primary")), ("Secondary", css_vars.get("--pico-secondary")), ("Muted", css_vars.get("--pico-muted-color"))]
|
|
441
|
+
return templates.TemplateResponse("partials/_theme_preview.html", {"request": request, "theme_name": theme_name, "css_vars_str": css_vars_str, "main_colors": main_colors})
|
|
1014
442
|
|
|
1015
|
-
|
|
1016
|
-
("Background", css_vars["--pico-background-color"]),
|
|
1017
|
-
("Text", css_vars["--pico-color"]),
|
|
1018
|
-
("Primary", css_vars["--pico-primary"]),
|
|
1019
|
-
("Secondary", css_vars["--pico-secondary"]),
|
|
1020
|
-
("Muted", css_vars["--pico-muted-color"]),
|
|
1021
|
-
]
|
|
1022
|
-
|
|
1023
|
-
return templates.TemplateResponse(
|
|
1024
|
-
"partials/_theme_preview.html",
|
|
1025
|
-
{
|
|
1026
|
-
"request": request,
|
|
1027
|
-
"theme_name": theme_name,
|
|
1028
|
-
"css_vars_str": css_vars_str,
|
|
1029
|
-
"main_colors": main_colors,
|
|
1030
|
-
},
|
|
1031
|
-
)
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
@app.post("/ui/apply-theme")
|
|
443
|
+
@app.post("/ui/apply-theme", tags=["UI Actions"])
|
|
1035
444
|
async def apply_theme(request: Request, theme: str = Form(...)):
|
|
1036
445
|
try:
|
|
1037
446
|
from flock.webapp.app.config import set_current_theme_name
|
|
1038
|
-
|
|
1039
447
|
set_current_theme_name(theme)
|
|
1040
|
-
# Trigger full refresh via HTMX
|
|
1041
448
|
headers = {"HX-Refresh": "true"}
|
|
1042
449
|
return HTMLResponse("", headers=headers)
|
|
1043
|
-
except Exception as e:
|
|
1044
|
-
return HTMLResponse(f"Failed to apply theme: {e}", status_code=500)
|
|
450
|
+
except Exception as e: return HTMLResponse(f"Failed to apply theme: {e}", status_code=500)
|
|
1045
451
|
|
|
1046
|
-
|
|
1047
|
-
# --- Settings Content Endpoints (for tab navigation) ---
|
|
1048
|
-
@app.get("/ui/htmx/settings/env-vars", response_class=HTMLResponse)
|
|
452
|
+
@app.get("/ui/htmx/settings/env-vars", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
1049
453
|
async def htmx_settings_env_vars(request: Request):
|
|
1050
|
-
env_vars_list, show_secrets =
|
|
1051
|
-
return templates.TemplateResponse(
|
|
1052
|
-
"partials/_settings_env_content.html",
|
|
1053
|
-
{
|
|
1054
|
-
"request": request,
|
|
1055
|
-
"env_vars": env_vars_list,
|
|
1056
|
-
"show_secrets": show_secrets,
|
|
1057
|
-
},
|
|
1058
|
-
)
|
|
454
|
+
env_vars_list, show_secrets = _prepare_env_vars_for_template_web()
|
|
455
|
+
return templates.TemplateResponse("partials/_settings_env_content.html", {"request": request, "env_vars": env_vars_list, "show_secrets": show_secrets})
|
|
1059
456
|
|
|
1060
|
-
|
|
1061
|
-
@app.get("/ui/htmx/settings/theme", response_class=HTMLResponse)
|
|
457
|
+
@app.get("/ui/htmx/settings/theme", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
1062
458
|
async def htmx_settings_theme(request: Request):
|
|
1063
459
|
theme_name = get_current_theme_name()
|
|
1064
|
-
themes_available = []
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
460
|
+
themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")] if THEMES_DIR and THEMES_DIR.exists() else []
|
|
461
|
+
return templates.TemplateResponse("partials/_settings_theme_content.html", {"request": request, "themes": themes_available, "current_theme": theme_name})
|
|
462
|
+
|
|
463
|
+
@app.get("/ui/chat", response_class=HTMLResponse, tags=["UI Pages"])
|
|
464
|
+
async def page_chat(request: Request, ui_mode: str = Query("standalone")):
|
|
465
|
+
context = get_base_context_web(request, ui_mode=ui_mode)
|
|
466
|
+
context["initial_content_url"] = "/ui/htmx/chat-view"
|
|
467
|
+
return templates.TemplateResponse("base.html", context)
|
|
468
|
+
|
|
469
|
+
@app.get("/ui/htmx/chat-view", response_class=HTMLResponse, tags=["UI HTMX Partials"])
|
|
470
|
+
async def htmx_get_chat_view(request: Request):
|
|
471
|
+
# Render container partial; session handled in chat router
|
|
472
|
+
return templates.TemplateResponse("partials/_chat_container.html", get_base_context_web(request))
|
|
473
|
+
|
|
474
|
+
if __name__ == "__main__":
|
|
475
|
+
import uvicorn
|
|
476
|
+
# Ensure the dependency injection system is initialized for standalone run
|
|
477
|
+
temp_run_store = RunStore()
|
|
478
|
+
# Create a default/dummy Flock instance for standalone UI testing
|
|
479
|
+
# This allows the UI to function without being started by `Flock.start_api()`
|
|
480
|
+
dev_flock_instance = Flock(name="DevStandaloneFlock", model="test/dummy", enable_logging=True, show_flock_banner=False)
|
|
481
|
+
|
|
482
|
+
set_global_flock_services(dev_flock_instance, temp_run_store)
|
|
483
|
+
app.state.flock_instance = dev_flock_instance
|
|
484
|
+
app.state.run_store = temp_run_store
|
|
485
|
+
app.state.flock_filename = "development_standalone.flock.yaml"
|
|
486
|
+
|
|
487
|
+
logger.info("Running webapp.app.main directly for development with a dummy Flock instance.")
|
|
488
|
+
uvicorn.run(app, host="127.0.0.1", port=8344, reload=True)
|