Spaces:
Sleeping
Sleeping
Commit ·
2e8575b
1
Parent(s): 5de2050
auto
Browse files- Dockerfile +0 -39
- README.md +0 -10
- requirements.txt +0 -1
- settings.json +0 -3
- solver.py +0 -418
- static/solver.html +0 -522
Dockerfile
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
FROM node:18-slim
|
| 2 |
-
|
| 3 |
-
# Install Python3, pip
|
| 4 |
-
RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
|
| 5 |
-
|
| 6 |
-
WORKDIR /app
|
| 7 |
-
|
| 8 |
-
# Install Python dependencies
|
| 9 |
-
COPY requirements.txt .
|
| 10 |
-
RUN pip3 install --no-cache-dir --break-system-packages -r requirements.txt
|
| 11 |
-
|
| 12 |
-
# Global install Claude Code CLI
|
| 13 |
-
RUN npm install -g @anthropic-ai/claude-code
|
| 14 |
-
|
| 15 |
-
# Copy web app files only (solver files are fetched from GitHub at runtime)
|
| 16 |
-
COPY app.py solver.py /app/
|
| 17 |
-
COPY static/ /app/static/
|
| 18 |
-
|
| 19 |
-
# Create workspaces directory
|
| 20 |
-
RUN mkdir -p /app/workspaces
|
| 21 |
-
|
| 22 |
-
# Create non-root user (claude CLI refuses to run as root)
|
| 23 |
-
RUN useradd -m -s /bin/bash solver
|
| 24 |
-
RUN mkdir -p /home/solver/.claude
|
| 25 |
-
COPY settings.json /home/solver/.claude/settings.json
|
| 26 |
-
RUN chown -R solver:solver /home/solver/.claude
|
| 27 |
-
RUN chown -R solver:solver /app
|
| 28 |
-
|
| 29 |
-
EXPOSE 7860
|
| 30 |
-
|
| 31 |
-
USER solver
|
| 32 |
-
ENV HOME=/home/solver
|
| 33 |
-
ENV PATH="/usr/local/lib/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
| 34 |
-
ENV PYTHONUNBUFFERED=1
|
| 35 |
-
|
| 36 |
-
# Default: fetch solver files from this GitHub repo at startup
|
| 37 |
-
ENV SOLVER_REPO_URL="https://github.com/Usami-Mira/CC_Integrater/raw/refs/heads/main/"
|
| 38 |
-
|
| 39 |
-
CMD ["python3", "/app/app.py"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Cc Solver
|
| 3 |
-
emoji: 😻
|
| 4 |
-
colorFrom: yellow
|
| 5 |
-
colorTo: green
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Flask==2.3.3
|
|
|
|
|
|
settings.json
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"apiKeyHelper": "echo $ANTHROPIC_API_KEY"
|
| 3 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
solver.py
DELETED
|
@@ -1,418 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Background task manager for the physics solver."""
|
| 3 |
-
|
| 4 |
-
import os
|
| 5 |
-
import sys
|
| 6 |
-
import json
|
| 7 |
-
import time
|
| 8 |
-
import shutil
|
| 9 |
-
import subprocess
|
| 10 |
-
import threading
|
| 11 |
-
import urllib.request
|
| 12 |
-
from datetime import datetime, timezone
|
| 13 |
-
|
| 14 |
-
# Solver source location
|
| 15 |
-
SOLVER_SRC = "/app/solver"
|
| 16 |
-
WORKSPACE_BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workspaces")
|
| 17 |
-
# Files that come from the repo — excluded from downloads
|
| 18 |
-
SOLVER_SOURCE_FILES = {"run.py", "spawn.py", "outline.md"}
|
| 19 |
-
DEFAULT_SOLVER_URL = os.environ.get(
|
| 20 |
-
"SOLVER_REPO_URL",
|
| 21 |
-
"https://github.com/Usami-Mira/CC_Integrater/raw/refs/heads/main/",
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
tasks = {}
|
| 25 |
-
tasks_lock = threading.Lock()
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def _log(task_id, msg):
|
| 29 |
-
"""Append a line to the task's log file AND print to stdout."""
|
| 30 |
-
task = tasks.get(task_id)
|
| 31 |
-
log_path = None
|
| 32 |
-
if task:
|
| 33 |
-
log_path = os.path.join(task["workspace"], "solver.log")
|
| 34 |
-
if log_path:
|
| 35 |
-
try:
|
| 36 |
-
with open(log_path, "a", encoding="utf-8") as f:
|
| 37 |
-
f.write(f"[{_now()}] {msg}\n")
|
| 38 |
-
except Exception:
|
| 39 |
-
pass
|
| 40 |
-
print(f"[{task_id}] {msg}", flush=True)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def _now():
|
| 44 |
-
return datetime.now(timezone.utc).isoformat()
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def fetch_solver_files(repo_url):
|
| 48 |
-
"""Download solver files from GitHub raw URL into /app/solver/."""
|
| 49 |
-
os.makedirs(SOLVER_SRC, exist_ok=True)
|
| 50 |
-
for name in ("run.py", "spawn.py", "outline.md", "config.json"):
|
| 51 |
-
p = os.path.join(SOLVER_SRC, name)
|
| 52 |
-
if os.path.exists(p):
|
| 53 |
-
os.remove(p)
|
| 54 |
-
|
| 55 |
-
print(f"[INIT] Fetching solver files from: {repo_url}", flush=True)
|
| 56 |
-
files = ["run.py", "spawn.py", "outline.md", "config.json"]
|
| 57 |
-
downloaded = []
|
| 58 |
-
errors = []
|
| 59 |
-
for name in files:
|
| 60 |
-
url = f"{repo_url.rstrip('/')}/{name}"
|
| 61 |
-
try:
|
| 62 |
-
with urllib.request.urlopen(url, timeout=30) as resp:
|
| 63 |
-
data = resp.read()
|
| 64 |
-
with open(os.path.join(SOLVER_SRC, name), "wb") as f:
|
| 65 |
-
f.write(data)
|
| 66 |
-
downloaded.append(name)
|
| 67 |
-
print(f"[INIT] Downloaded {name}", flush=True)
|
| 68 |
-
except Exception as e:
|
| 69 |
-
errors.append(f"{name}: {e}")
|
| 70 |
-
print(f"[INIT] Failed to download {name}: {e}", flush=True)
|
| 71 |
-
|
| 72 |
-
if not downloaded:
|
| 73 |
-
print(f"[INIT] CRITICAL: No solver files downloaded. Errors: {errors}", flush=True)
|
| 74 |
-
return False
|
| 75 |
-
if errors:
|
| 76 |
-
print(f"[INIT] Partial download. Missing: {errors}", flush=True)
|
| 77 |
-
return True
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def active_count():
|
| 81 |
-
"""Caller must hold tasks_lock."""
|
| 82 |
-
return sum(1 for t in tasks.values() if t["status"] in ("pending", "running"))
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def submit_task(problem_text, name=None, config_override=None):
|
| 86 |
-
"""Create a new task, return task_id."""
|
| 87 |
-
print("[SUBMIT] submit_task() called", flush=True)
|
| 88 |
-
try:
|
| 89 |
-
task_id = f"task-{int(time.time()*1000)}"
|
| 90 |
-
workspace = os.path.join(WORKSPACE_BASE, task_id)
|
| 91 |
-
print(f"[SUBMIT] Creating workspace: {workspace}", flush=True)
|
| 92 |
-
os.makedirs(workspace, exist_ok=True)
|
| 93 |
-
|
| 94 |
-
# Write problem
|
| 95 |
-
with open(os.path.join(workspace, "problem.md"), "w", encoding="utf-8") as f:
|
| 96 |
-
f.write(problem_text)
|
| 97 |
-
print(f"[SUBMIT] Wrote problem.md", flush=True)
|
| 98 |
-
|
| 99 |
-
# Copy solver source into workspace
|
| 100 |
-
copied = []
|
| 101 |
-
for fname in ("run.py", "spawn.py", "outline.md", "config.json"):
|
| 102 |
-
src = os.path.join(SOLVER_SRC, fname)
|
| 103 |
-
exists = os.path.exists(src)
|
| 104 |
-
print(f"[SUBMIT] {fname}: exists={exists} at {src}", flush=True)
|
| 105 |
-
if exists:
|
| 106 |
-
shutil.copy2(src, os.path.join(workspace, fname))
|
| 107 |
-
copied.append(fname)
|
| 108 |
-
print(f"[SUBMIT] Copied: {copied}", flush=True)
|
| 109 |
-
|
| 110 |
-
# Apply config override if provided
|
| 111 |
-
if config_override:
|
| 112 |
-
config_path = os.path.join(workspace, "config.json")
|
| 113 |
-
if os.path.exists(config_path):
|
| 114 |
-
with open(config_path, "r", encoding="utf-8") as f:
|
| 115 |
-
current = json.load(f)
|
| 116 |
-
current.update(config_override)
|
| 117 |
-
with open(config_path, "w", encoding="utf-8") as f:
|
| 118 |
-
json.dump(current, f, indent=2, ensure_ascii=False)
|
| 119 |
-
print(f"[SUBMIT] Applied config override: {list(config_override.keys())}", flush=True)
|
| 120 |
-
else:
|
| 121 |
-
with open(config_path, "w", encoding="utf-8") as f:
|
| 122 |
-
json.dump(config_override, f, indent=2, ensure_ascii=False)
|
| 123 |
-
print(f"[SUBMIT] Created config from override", flush=True)
|
| 124 |
-
|
| 125 |
-
task = {
|
| 126 |
-
"task_id": task_id,
|
| 127 |
-
"name": name or f"Problem {len(tasks)+1}",
|
| 128 |
-
"status": "pending",
|
| 129 |
-
"phase": "waiting",
|
| 130 |
-
"workspace": workspace,
|
| 131 |
-
"problem_preview": problem_text[:120],
|
| 132 |
-
"problem_length": len(problem_text),
|
| 133 |
-
"created_at": _now(),
|
| 134 |
-
"started_at": None,
|
| 135 |
-
"finished_at": None,
|
| 136 |
-
"error": None,
|
| 137 |
-
}
|
| 138 |
-
with tasks_lock:
|
| 139 |
-
tasks[task_id] = task
|
| 140 |
-
print(f"[SUBMIT] Task registered in memory", flush=True)
|
| 141 |
-
|
| 142 |
-
# Create log file
|
| 143 |
-
log_path = os.path.join(workspace, "solver.log")
|
| 144 |
-
with open(log_path, "w", encoding="utf-8") as f:
|
| 145 |
-
f.write(f"[{_now()}] Task {task_id} created\n")
|
| 146 |
-
|
| 147 |
-
print(f"[SUBMIT] Created task {task_id} -> {workspace}", flush=True)
|
| 148 |
-
|
| 149 |
-
# Spawn background thread — run.py manages its own timeout & concurrency
|
| 150 |
-
t = threading.Thread(target=_run_solver, args=(task_id, workspace), daemon=True)
|
| 151 |
-
t.start()
|
| 152 |
-
print(f"[SUBMIT] Thread started", flush=True)
|
| 153 |
-
return task_id
|
| 154 |
-
except Exception as e:
|
| 155 |
-
import traceback
|
| 156 |
-
tb = traceback.format_exc()
|
| 157 |
-
print(f"[SUBMIT] ERROR: {e}", flush=True)
|
| 158 |
-
print(f"[SUBMIT] Traceback:\n{tb}", flush=True)
|
| 159 |
-
raise
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def _run_solver(task_id, workspace):
|
| 163 |
-
print(f"[{task_id}] _run_solver thread started", flush=True)
|
| 164 |
-
with tasks_lock:
|
| 165 |
-
tasks[task_id].update({"status": "running", "started_at": _now()})
|
| 166 |
-
|
| 167 |
-
_log(task_id, f"Starting solver in {workspace}")
|
| 168 |
-
_log(task_id, f"ANTHROPIC_API_KEY: {'set' if os.environ.get('ANTHROPIC_API_KEY') else 'NOT SET'}")
|
| 169 |
-
_log(task_id, f"ANTHROPIC_BASE_URL: {os.environ.get('ANTHROPIC_BASE_URL', '(default)')}")
|
| 170 |
-
|
| 171 |
-
env = os.environ.copy()
|
| 172 |
-
start = time.time()
|
| 173 |
-
proc = None
|
| 174 |
-
|
| 175 |
-
try:
|
| 176 |
-
cmd = [sys.executable, os.path.join(workspace, "run.py"), workspace]
|
| 177 |
-
print(f"[{task_id}] Running: {' '.join(cmd)}", flush=True)
|
| 178 |
-
|
| 179 |
-
proc = subprocess.Popen(
|
| 180 |
-
cmd,
|
| 181 |
-
stdout=subprocess.PIPE,
|
| 182 |
-
stderr=subprocess.STDOUT,
|
| 183 |
-
cwd=workspace,
|
| 184 |
-
env=env,
|
| 185 |
-
)
|
| 186 |
-
_log(task_id, f"spawned run.py with PID {proc.pid}")
|
| 187 |
-
|
| 188 |
-
# Read stdout line by line, append to log
|
| 189 |
-
def _read_output():
|
| 190 |
-
if proc.stdout:
|
| 191 |
-
for line in iter(proc.stdout.readline, b""):
|
| 192 |
-
decoded = line.decode("utf-8", errors="replace").strip()
|
| 193 |
-
if decoded:
|
| 194 |
-
_log(task_id, f"[stdout] {decoded}")
|
| 195 |
-
|
| 196 |
-
reader = threading.Thread(target=_read_output, daemon=True)
|
| 197 |
-
reader.start()
|
| 198 |
-
|
| 199 |
-
# Monitor workspace for progress (no timeout — run.py handles its own)
|
| 200 |
-
last_phase = "starting"
|
| 201 |
-
while proc.poll() is None:
|
| 202 |
-
phase = _infer_phase(workspace)
|
| 203 |
-
if phase != last_phase:
|
| 204 |
-
_log(task_id, f"Phase: {phase}")
|
| 205 |
-
last_phase = phase
|
| 206 |
-
|
| 207 |
-
with tasks_lock:
|
| 208 |
-
tasks[task_id]["phase"] = phase
|
| 209 |
-
tasks[task_id]["elapsed_s"] = round(time.time() - start)
|
| 210 |
-
|
| 211 |
-
time.sleep(2)
|
| 212 |
-
|
| 213 |
-
reader.join(timeout=3)
|
| 214 |
-
_log(task_id, f"run.py exited with code {proc.returncode}")
|
| 215 |
-
|
| 216 |
-
if proc.returncode != 0:
|
| 217 |
-
_log(task_id, "Solver returned non-zero exit code")
|
| 218 |
-
with tasks_lock:
|
| 219 |
-
tasks[task_id].update({
|
| 220 |
-
"status": "error",
|
| 221 |
-
"error": f"Solver exited with code {proc.returncode}",
|
| 222 |
-
"finished_at": _now(),
|
| 223 |
-
})
|
| 224 |
-
return
|
| 225 |
-
|
| 226 |
-
summary = os.path.join(workspace, "final_summary.md")
|
| 227 |
-
if os.path.exists(summary):
|
| 228 |
-
_log(task_id, "Solver completed successfully")
|
| 229 |
-
with tasks_lock:
|
| 230 |
-
tasks[task_id].update({
|
| 231 |
-
"status": "complete",
|
| 232 |
-
"phase": "complete",
|
| 233 |
-
"finished_at": _now(),
|
| 234 |
-
})
|
| 235 |
-
else:
|
| 236 |
-
_log(task_id, "Solver finished but no summary file found")
|
| 237 |
-
with tasks_lock:
|
| 238 |
-
tasks[task_id].update({
|
| 239 |
-
"status": "error",
|
| 240 |
-
"error": "Solver finished but no summary file was produced.",
|
| 241 |
-
"finished_at": _now(),
|
| 242 |
-
})
|
| 243 |
-
|
| 244 |
-
except FileNotFoundError:
|
| 245 |
-
_log(task_id, f"FileNotFoundError: run.py not found")
|
| 246 |
-
with tasks_lock:
|
| 247 |
-
tasks[task_id].update({
|
| 248 |
-
"status": "error",
|
| 249 |
-
"error": "run.py not found in workspace.",
|
| 250 |
-
"finished_at": _now(),
|
| 251 |
-
})
|
| 252 |
-
except Exception as e:
|
| 253 |
-
import traceback
|
| 254 |
-
tb = traceback.format_exc()
|
| 255 |
-
_log(task_id, f"Exception: {e}")
|
| 256 |
-
_log(task_id, tb)
|
| 257 |
-
print(f"[{task_id}] Traceback:\n{tb}", flush=True)
|
| 258 |
-
with tasks_lock:
|
| 259 |
-
tasks[task_id].update({
|
| 260 |
-
"status": "error",
|
| 261 |
-
"error": str(e),
|
| 262 |
-
"finished_at": _now(),
|
| 263 |
-
})
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
# Known solver workflow steps in execution order. Unknown files append at end alphabetically.
|
| 267 |
-
WORKFLOW_STEPS = ["plan", "solution", "review", "final_summary"]
|
| 268 |
-
# Files to exclude from progress tracking
|
| 269 |
-
PROGRESS_EXCLUDES = {"problem.md", "solver.log", "outline.md"}
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
def _check_files(workspace):
|
| 273 |
-
"""Return list of completed steps based on workspace .md files (excluding problem.md)."""
|
| 274 |
-
if not os.path.isdir(workspace):
|
| 275 |
-
return []
|
| 276 |
-
found = set()
|
| 277 |
-
for fname in os.listdir(workspace):
|
| 278 |
-
if fname.endswith(".md") and fname not in PROGRESS_EXCLUDES:
|
| 279 |
-
found.add(fname[:-3]) # strip .md
|
| 280 |
-
# Sort: known steps in workflow order, then unknown files alphabetically
|
| 281 |
-
done = [s for s in WORKFLOW_STEPS if s in found]
|
| 282 |
-
done.extend(sorted(found - set(WORKFLOW_STEPS)))
|
| 283 |
-
return done
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
def _infer_phase(workspace):
|
| 287 |
-
if os.path.exists(os.path.join(workspace, "final_summary.md")):
|
| 288 |
-
return "complete"
|
| 289 |
-
if os.path.exists(os.path.join(workspace, "review.md")):
|
| 290 |
-
return "evaluating"
|
| 291 |
-
if os.path.exists(os.path.join(workspace, "solution.md")):
|
| 292 |
-
return "building"
|
| 293 |
-
if os.path.exists(os.path.join(workspace, "plan.md")):
|
| 294 |
-
return "planning"
|
| 295 |
-
return "starting"
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
def get_task(task_id):
|
| 299 |
-
with tasks_lock:
|
| 300 |
-
task = tasks.get(task_id)
|
| 301 |
-
if task:
|
| 302 |
-
result = dict(task)
|
| 303 |
-
result.pop("problem_preview", None)
|
| 304 |
-
result["files_done"] = _check_files(task["workspace"])
|
| 305 |
-
return result
|
| 306 |
-
return None
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
def get_task_result(task_id):
|
| 310 |
-
task = get_task(task_id)
|
| 311 |
-
if not task:
|
| 312 |
-
return None
|
| 313 |
-
summary = os.path.join(task.get("workspace", ""), "final_summary.md")
|
| 314 |
-
if os.path.exists(summary):
|
| 315 |
-
with open(summary, encoding="utf-8") as f:
|
| 316 |
-
return f.read()
|
| 317 |
-
return None
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
def get_task_logs(task_id, lines=100):
|
| 321 |
-
task = tasks.get(task_id)
|
| 322 |
-
if not task:
|
| 323 |
-
return [f"Task {task_id} not found in memory."]
|
| 324 |
-
log_path = os.path.join(task["workspace"], "solver.log")
|
| 325 |
-
if not os.path.exists(log_path):
|
| 326 |
-
return ["Waiting for solver to start..."]
|
| 327 |
-
try:
|
| 328 |
-
with open(log_path, encoding="utf-8") as f:
|
| 329 |
-
all_lines = f.readlines()
|
| 330 |
-
return [l.rstrip() for l in all_lines[-lines:]]
|
| 331 |
-
except Exception as e:
|
| 332 |
-
return [f"Error reading log: {e}"]
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
def get_running_tasks():
|
| 336 |
-
"""Return list of currently running tasks (for frontend recovery after page refresh)."""
|
| 337 |
-
with tasks_lock:
|
| 338 |
-
return [
|
| 339 |
-
{
|
| 340 |
-
"task_id": t["task_id"],
|
| 341 |
-
"status": t["status"],
|
| 342 |
-
"phase": t["phase"],
|
| 343 |
-
"problem": t["problem"],
|
| 344 |
-
"created_at": t["created_at"],
|
| 345 |
-
"files_done": _check_files(t["workspace"]),
|
| 346 |
-
}
|
| 347 |
-
for t in tasks.values()
|
| 348 |
-
if t["status"] in ("pending", "running")
|
| 349 |
-
]
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
def list_tasks():
|
| 353 |
-
with tasks_lock:
|
| 354 |
-
return [
|
| 355 |
-
{
|
| 356 |
-
"task_id": t["task_id"],
|
| 357 |
-
"name": t["name"],
|
| 358 |
-
"status": t["status"],
|
| 359 |
-
"phase": t["phase"],
|
| 360 |
-
"problem_preview": t.get("problem_preview", ""),
|
| 361 |
-
"problem_length": t.get("problem_length", 0),
|
| 362 |
-
"created_at": t["created_at"],
|
| 363 |
-
"started_at": t.get("started_at"),
|
| 364 |
-
"finished_at": t.get("finished_at"),
|
| 365 |
-
"files_done": _check_files(t["workspace"]),
|
| 366 |
-
"error": t.get("error"),
|
| 367 |
-
}
|
| 368 |
-
for t in sorted(tasks.values(), key=lambda x: x["created_at"], reverse=True)
|
| 369 |
-
]
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
def delete_task(task_id):
|
| 373 |
-
"""Remove a task from memory and delete its workspace. Returns True if found."""
|
| 374 |
-
with tasks_lock:
|
| 375 |
-
task = tasks.pop(task_id, None)
|
| 376 |
-
if not task:
|
| 377 |
-
return False
|
| 378 |
-
workspace = task.get("workspace", "")
|
| 379 |
-
if os.path.isdir(workspace):
|
| 380 |
-
shutil.rmtree(workspace)
|
| 381 |
-
_log(task_id, f"Task {task_id} deleted")
|
| 382 |
-
return True
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
def cleanup_completed_tasks():
|
| 386 |
-
"""Delete all completed and errored tasks. Returns count of deleted tasks."""
|
| 387 |
-
with tasks_lock:
|
| 388 |
-
to_delete = [
|
| 389 |
-
tid for tid, t in tasks.items()
|
| 390 |
-
if t["status"] in ("complete", "error")
|
| 391 |
-
]
|
| 392 |
-
count = 0
|
| 393 |
-
for tid in to_delete:
|
| 394 |
-
if delete_task(tid):
|
| 395 |
-
count += 1
|
| 396 |
-
return count
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
def update_task_config(task_id, config_dict):
|
| 400 |
-
"""Update config.json in a task's workspace. Only allowed for pending tasks."""
|
| 401 |
-
with tasks_lock:
|
| 402 |
-
task = tasks.get(task_id)
|
| 403 |
-
if not task:
|
| 404 |
-
return None, "Task not found"
|
| 405 |
-
if task["status"] != "pending":
|
| 406 |
-
return None, f"Cannot update config for task in '{task['status']}' status (only 'pending')"
|
| 407 |
-
config_path = os.path.join(task["workspace"], "config.json")
|
| 408 |
-
try:
|
| 409 |
-
with open(config_path, "r", encoding="utf-8") as f:
|
| 410 |
-
current = json.load(f)
|
| 411 |
-
current.update(config_dict)
|
| 412 |
-
with open(config_path, "w", encoding="utf-8") as f:
|
| 413 |
-
json.dump(current, f, indent=2, ensure_ascii=False)
|
| 414 |
-
return current, None
|
| 415 |
-
except FileNotFoundError:
|
| 416 |
-
return None, "config.json not found in workspace"
|
| 417 |
-
except json.JSONDecodeError:
|
| 418 |
-
return None, "Invalid config.json in workspace"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/solver.html
DELETED
|
@@ -1,522 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="zh-CN">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>CC Solver</title>
|
| 7 |
-
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 8 |
-
<style>
|
| 9 |
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 10 |
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f2f5; color: #333; min-height: 100vh; }
|
| 11 |
-
.container { max-width: 960px; margin: 0 auto; padding: 20px; }
|
| 12 |
-
h1 { text-align: center; padding: 20px 0 6px; color: #1a1a2e; font-size: 1.8em; }
|
| 13 |
-
.subtitle { text-align: center; color: #666; margin-bottom: 24px; font-size: 0.9em; }
|
| 14 |
-
|
| 15 |
-
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; margin-bottom: 20px; }
|
| 16 |
-
.card-title { font-size: 0.95em; font-weight: 700; color: #1a1a2e; margin-bottom: 12px; }
|
| 17 |
-
|
| 18 |
-
/* Input area */
|
| 19 |
-
.input-mode-toggle { display: flex; gap: 0; margin-bottom: 12px; background: #f0f0f0; border-radius: 8px; overflow: hidden; }
|
| 20 |
-
.input-mode-toggle button { flex: 1; padding: 8px 16px; border: none; cursor: pointer; font-size: 13px; font-weight: 600; background: #f0f0f0; color: #666; transition: all 0.15s; }
|
| 21 |
-
.input-mode-toggle button.active { background: #4361ee; color: #fff; }
|
| 22 |
-
|
| 23 |
-
textarea { width: 100%; min-height: 150px; border: 1px solid #ddd; border-radius: 8px; padding: 12px; font-size: 14px; font-family: inherit; resize: vertical; line-height: 1.6; }
|
| 24 |
-
textarea:focus { outline: none; border-color: #4361ee; box-shadow: 0 0 0 3px rgba(67,97,238,0.15); }
|
| 25 |
-
|
| 26 |
-
.file-drop { border: 2px dashed #ccc; border-radius: 8px; padding: 32px 16px; text-align: center; color: #999; cursor: pointer; transition: border-color 0.2s, background 0.2s; }
|
| 27 |
-
.file-drop:hover, .file-drop.dragover { border-color: #4361ee; background: #f5f7ff; }
|
| 28 |
-
.file-drop input[type="file"] { display: none; }
|
| 29 |
-
.file-list { margin-top: 8px; font-size: 13px; }
|
| 30 |
-
.file-list .file-item { display: flex; justify-content: space-between; padding: 4px 0; }
|
| 31 |
-
.file-list .file-item .remove { color: #e74c3c; cursor: pointer; font-weight: 700; }
|
| 32 |
-
|
| 33 |
-
.btn-row { display: flex; gap: 12px; margin-top: 12px; }
|
| 34 |
-
.btn { padding: 10px 28px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: all 0.15s; }
|
| 35 |
-
.btn-primary { background: #4361ee; color: #fff; }
|
| 36 |
-
.btn-primary:hover:not(:disabled) { background: #3a56d4; }
|
| 37 |
-
.btn-primary:disabled { background: #a0b0f0; cursor: not-allowed; }
|
| 38 |
-
.btn-secondary { background: #e8e8e8; color: #333; }
|
| 39 |
-
.btn-secondary:hover { background: #d8d8d8; }
|
| 40 |
-
.btn-download { background: #2ecc71; color: #fff; padding: 6px 14px; font-size: 12px; border-radius: 6px; text-decoration: none; }
|
| 41 |
-
.btn-download:hover { background: #27ae60; }
|
| 42 |
-
|
| 43 |
-
/* Config */
|
| 44 |
-
.config-row { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
|
| 45 |
-
.config-row label { font-size: 13px; color: #666; min-width: 90px; }
|
| 46 |
-
.config-row input { flex: 1; padding: 6px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; font-family: monospace; }
|
| 47 |
-
.config-row button { padding: 6px 14px; border: none; border-radius: 6px; background: #4361ee; color: #fff; font-size: 12px; cursor: pointer; white-space: nowrap; }
|
| 48 |
-
|
| 49 |
-
/* Task list */
|
| 50 |
-
.task-list { max-height: 400px; overflow-y: auto; }
|
| 51 |
-
.task-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; gap: 8px; }
|
| 52 |
-
.task-item:last-child { border-bottom: none; }
|
| 53 |
-
.task-name { font-weight: 600; font-size: 14px; }
|
| 54 |
-
.task-preview { font-size: 12px; color: #888; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 55 |
-
.task-meta { display: flex; align-items: center; gap: 8px; }
|
| 56 |
-
.task-delete { cursor: pointer; font-size: 14px; color: #ccc; font-weight: 700; transition: color 0.15s; }
|
| 57 |
-
.task-delete:hover { color: #e74c3c; }
|
| 58 |
-
.badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
|
| 59 |
-
.badge-pending { background: #fff3cd; color: #856404; }
|
| 60 |
-
.badge-running { background: #cce5ff; color: #004085; }
|
| 61 |
-
.badge-complete { background: #d4edda; color: #155724; }
|
| 62 |
-
.badge-error { background: #f8d7da; color: #721c24; }
|
| 63 |
-
|
| 64 |
-
/* Modal */
|
| 65 |
-
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 100; align-items: center; justify-content: center; }
|
| 66 |
-
.modal-overlay.visible { display: flex; }
|
| 67 |
-
.modal { background: #fff; border-radius: 12px; max-width: 800px; width: 90%; max-height: 80vh; overflow-y: auto; padding: 24px; }
|
| 68 |
-
.modal h2 { font-size: 1.1em; margin-bottom: 12px; }
|
| 69 |
-
.modal .result-content { line-height: 1.7; }
|
| 70 |
-
.modal .result-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
|
| 71 |
-
.modal .result-content th, .modal .result-content td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
| 72 |
-
.modal .result-content th { background: #f5f5f5; }
|
| 73 |
-
.modal .result-content pre { background: #f8f8f8; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 13px; }
|
| 74 |
-
.modal .result-content code { background: #f0f0f0; padding: 1px 4px; border-radius: 3px; font-size: 13px; }
|
| 75 |
-
.modal .result-content pre code { background: none; padding: 0; }
|
| 76 |
-
.modal .result-content blockquote { border-left: 3px solid #4361ee; padding-left: 12px; color: #555; margin: 8px 0; }
|
| 77 |
-
.modal .log-console { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 12px; max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
|
| 78 |
-
|
| 79 |
-
.error-msg { background: #f8d7da; color: #721c24; padding: 10px; border-radius: 8px; margin-top: 8px; display: none; font-size: 13px; }
|
| 80 |
-
.error-msg.visible { display: block; }
|
| 81 |
-
|
| 82 |
-
/* Per-task progress */
|
| 83 |
-
.task-files { display: flex; gap: 4px; margin-top: 4px; }
|
| 84 |
-
.task-file-step { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: #e8e8e8; color: #999; }
|
| 85 |
-
.task-file-step.done { background: #d4edda; color: #155724; font-weight: 600; }
|
| 86 |
-
.task-file-step.current { background: #cce5ff; color: #004085; font-weight: 600; animation: pulse 1.5s infinite; }
|
| 87 |
-
</style>
|
| 88 |
-
</head>
|
| 89 |
-
<body>
|
| 90 |
-
<div class="container">
|
| 91 |
-
<h1>CC Solver</h1>
|
| 92 |
-
<p class="subtitle">Multi-agent physics problem solver</p>
|
| 93 |
-
|
| 94 |
-
<!-- Config -->
|
| 95 |
-
<div class="card">
|
| 96 |
-
<div class="card-title">Solver Config</div>
|
| 97 |
-
<div class="config-row">
|
| 98 |
-
<label>Repo URL</label>
|
| 99 |
-
<input type="text" id="solver-url" placeholder="https://github.com/Usami-Mira/CC_Integrater/raw/refs/heads/main/">
|
| 100 |
-
<button onclick="updateConfig()">Update</button>
|
| 101 |
-
</div>
|
| 102 |
-
<div class="config-row">
|
| 103 |
-
<label>Token</label>
|
| 104 |
-
<input type="password" id="solver-token" placeholder="Enter password">
|
| 105 |
-
</div>
|
| 106 |
-
<p style="font-size:12px;color:#888;margin-top:8px;">Model 和 timeout 由仓库 config.json 控制。</p>
|
| 107 |
-
</div>
|
| 108 |
-
|
| 109 |
-
<!-- Input -->
|
| 110 |
-
<div class="card">
|
| 111 |
-
<div class="card-title">New Task</div>
|
| 112 |
-
<div class="input-mode-toggle">
|
| 113 |
-
<button id="mode-text" class="active" onclick="setInputMode('text')">Text Input</button>
|
| 114 |
-
<button id="mode-file" onclick="setInputMode('file')">Upload Files</button>
|
| 115 |
-
</div>
|
| 116 |
-
|
| 117 |
-
<!-- Config override -->
|
| 118 |
-
<details style="margin-bottom:12px;">
|
| 119 |
-
<summary style="cursor:pointer;font-size:13px;font-weight:600;color:#666;">Custom Config (config.json)</summary>
|
| 120 |
-
<textarea id="config-input" placeholder='{"wallClock": 86400, "maxTurns": 100}' style="min-height:80px;margin-top:8px;font-family:monospace;font-size:13px;"></textarea>
|
| 121 |
-
</details>
|
| 122 |
-
|
| 123 |
-
<div id="input-text-area">
|
| 124 |
-
<textarea id="problem-input" placeholder="Paste your physics problem here..."></textarea>
|
| 125 |
-
<div class="btn-row">
|
| 126 |
-
<button class="btn btn-primary" id="submit-btn" onclick="submitText()">Solve</button>
|
| 127 |
-
</div>
|
| 128 |
-
</div>
|
| 129 |
-
|
| 130 |
-
<div id="input-file-area" style="display:none;">
|
| 131 |
-
<div class="file-drop" id="file-drop" onclick="document.getElementById('file-input').click()">
|
| 132 |
-
<p>Click or drag <strong>.md</strong> files here</p>
|
| 133 |
-
<input type="file" id="file-input" accept=".md" multiple>
|
| 134 |
-
</div>
|
| 135 |
-
<div class="file-list" id="file-list"></div>
|
| 136 |
-
<div class="btn-row">
|
| 137 |
-
<button class="btn btn-primary" id="submit-files-btn" onclick="submitFiles()">Solve All</button>
|
| 138 |
-
</div>
|
| 139 |
-
</div>
|
| 140 |
-
|
| 141 |
-
<div class="error-msg" id="error-msg"></div>
|
| 142 |
-
</div>
|
| 143 |
-
|
| 144 |
-
<!-- Tasks -->
|
| 145 |
-
<div class="card">
|
| 146 |
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
| 147 |
-
<div class="card-title" style="margin-bottom:0;">Tasks</div>
|
| 148 |
-
<div>
|
| 149 |
-
<a class="btn-download" id="cleanup-btn" style="display:none;margin-right:8px;cursor:pointer;" onclick="cleanupTasks()">Clear Done</a>
|
| 150 |
-
<a class="btn-download" id="download-all-btn" style="display:none;" onclick="downloadAll()">Download All</a>
|
| 151 |
-
</div>
|
| 152 |
-
</div>
|
| 153 |
-
<div class="task-list" id="task-list">
|
| 154 |
-
<p style="color:#999;font-size:13px;">No tasks yet.</p>
|
| 155 |
-
</div>
|
| 156 |
-
</div>
|
| 157 |
-
</div>
|
| 158 |
-
|
| 159 |
-
<!-- Task detail modal -->
|
| 160 |
-
<div class="modal-overlay" id="modal-overlay" onclick="closeModal(event)">
|
| 161 |
-
<div class="modal" id="modal-content">
|
| 162 |
-
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
| 163 |
-
<h2 id="modal-title" style="margin-bottom:0;">Task Detail</h2>
|
| 164 |
-
<div>
|
| 165 |
-
<a class="btn-download" id="modal-download" style="display:none;margin-right:8px;" onclick="">Download</a>
|
| 166 |
-
<button class="btn btn-secondary" onclick="closeModalDirect()" style="padding:6px 14px;font-size:12px;">Close</button>
|
| 167 |
-
</div>
|
| 168 |
-
</div>
|
| 169 |
-
<div id="modal-body"></div>
|
| 170 |
-
</div>
|
| 171 |
-
</div>
|
| 172 |
-
|
| 173 |
-
<script>
|
| 174 |
-
let inputMode = 'text';
|
| 175 |
-
let pendingFiles = [];
|
| 176 |
-
let pollTimers = {};
|
| 177 |
-
|
| 178 |
-
function getToken() {
|
| 179 |
-
const val = document.getElementById('solver-token').value.trim();
|
| 180 |
-
return val;
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
function getConfigOverride() {
|
| 184 |
-
const val = document.getElementById('config-input').value.trim();
|
| 185 |
-
if (!val) return null;
|
| 186 |
-
try { return JSON.parse(val); }
|
| 187 |
-
catch (e) { showError('Invalid config JSON: ' + e.message); return null; }
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
function api(url, opts = {}) {
|
| 191 |
-
const token = getToken();
|
| 192 |
-
const separator = url.includes('?') ? '&' : '?';
|
| 193 |
-
const sep = token ? `${separator}token=${encodeURIComponent(token)}` : '';
|
| 194 |
-
return fetch(url + sep, opts);
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
function setInputMode(mode) {
|
| 198 |
-
inputMode = mode;
|
| 199 |
-
document.getElementById('mode-text').classList.toggle('active', mode === 'text');
|
| 200 |
-
document.getElementById('mode-file').classList.toggle('active', mode === 'file');
|
| 201 |
-
document.getElementById('input-text-area').style.display = mode === 'text' ? 'block' : 'none';
|
| 202 |
-
document.getElementById('input-file-area').style.display = mode === 'file' ? 'block' : 'none';
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
// File handling
|
| 206 |
-
const fileInput = document.getElementById('file-input');
|
| 207 |
-
const fileDrop = document.getElementById('file-drop');
|
| 208 |
-
|
| 209 |
-
fileInput.addEventListener('change', (e) => addFiles(e.target.files));
|
| 210 |
-
|
| 211 |
-
fileDrop.addEventListener('dragover', (e) => { e.preventDefault(); fileDrop.classList.add('dragover'); });
|
| 212 |
-
fileDrop.addEventListener('dragleave', () => fileDrop.classList.remove('dragover'));
|
| 213 |
-
fileDrop.addEventListener('drop', (e) => { e.preventDefault(); fileDrop.classList.remove('dragover'); addFiles(e.dataTransfer.files); });
|
| 214 |
-
|
| 215 |
-
function addFiles(files) {
|
| 216 |
-
for (const f of files) {
|
| 217 |
-
if (f.name.endsWith('.md') && !pendingFiles.find(x => x.name === f.name)) {
|
| 218 |
-
pendingFiles.push(f);
|
| 219 |
-
}
|
| 220 |
-
}
|
| 221 |
-
renderFileList();
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
function removeFile(idx) {
|
| 225 |
-
pendingFiles.splice(idx, 1);
|
| 226 |
-
renderFileList();
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
function renderFileList() {
|
| 230 |
-
const el = document.getElementById('file-list');
|
| 231 |
-
if (pendingFiles.length === 0) { el.innerHTML = ''; return; }
|
| 232 |
-
el.innerHTML = pendingFiles.map((f, i) =>
|
| 233 |
-
`<div class="file-item"><span>${f.name} (${(f.size/1024).toFixed(1)} KB)</span><span class="remove" onclick="removeFile(${i})">×</span></div>`
|
| 234 |
-
).join('');
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
function showError(msg) {
|
| 238 |
-
const el = document.getElementById('error-msg');
|
| 239 |
-
el.textContent = msg;
|
| 240 |
-
el.classList.add('visible');
|
| 241 |
-
setTimeout(() => el.classList.remove('visible'), 5000);
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
function hideError() { document.getElementById('error-msg').classList.remove('visible'); }
|
| 245 |
-
|
| 246 |
-
// Submit text problem
|
| 247 |
-
async function submitText() {
|
| 248 |
-
const text = document.getElementById('problem-input').value.trim();
|
| 249 |
-
if (!text) return;
|
| 250 |
-
hideError();
|
| 251 |
-
try {
|
| 252 |
-
const body = { problem: text };
|
| 253 |
-
const cfg = getConfigOverride();
|
| 254 |
-
if (cfg) body.config = cfg;
|
| 255 |
-
const resp = await api('/api/submit', {
|
| 256 |
-
method: 'POST',
|
| 257 |
-
headers: {'Content-Type': 'application/json'},
|
| 258 |
-
body: JSON.stringify(body)
|
| 259 |
-
});
|
| 260 |
-
const data = await resp.json();
|
| 261 |
-
if (data.error) { showError(data.error); return; }
|
| 262 |
-
document.getElementById('problem-input').value = '';
|
| 263 |
-
loadTasks();
|
| 264 |
-
} catch (e) { showError('Network error: ' + e.message); }
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
// Submit files
|
| 268 |
-
async function submitFiles() {
|
| 269 |
-
if (pendingFiles.length === 0) { showError('No files selected'); return; }
|
| 270 |
-
hideError();
|
| 271 |
-
const fd = new FormData();
|
| 272 |
-
pendingFiles.forEach(f => fd.append('files', f));
|
| 273 |
-
const cfg = getConfigOverride();
|
| 274 |
-
if (cfg) fd.append('config', JSON.stringify(cfg));
|
| 275 |
-
try {
|
| 276 |
-
const token = getToken();
|
| 277 |
-
const resp = await api('/api/submit-file', { method: 'POST', body: fd });
|
| 278 |
-
const data = await resp.json();
|
| 279 |
-
if (data.error) { showError(data.error); return; }
|
| 280 |
-
pendingFiles = [];
|
| 281 |
-
renderFileList();
|
| 282 |
-
fileInput.value = '';
|
| 283 |
-
loadTasks();
|
| 284 |
-
} catch (e) { showError('Network error: ' + e.message); }
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
const DEFAULT_URL = 'https://github.com/Usami-Mira/CC_Integrater/raw/refs/heads/main/';
|
| 288 |
-
|
| 289 |
-
// Load config
|
| 290 |
-
const savedUrl = localStorage.getItem('solver_url');
|
| 291 |
-
document.getElementById('solver-url').value = savedUrl || DEFAULT_URL;
|
| 292 |
-
api('/api/config')
|
| 293 |
-
.then(r => r.json())
|
| 294 |
-
.then(data => {
|
| 295 |
-
if (data.solver_url) {
|
| 296 |
-
document.getElementById('solver-url').value = data.solver_url;
|
| 297 |
-
localStorage.setItem('solver_url', data.solver_url);
|
| 298 |
-
}
|
| 299 |
-
})
|
| 300 |
-
.catch(() => {});
|
| 301 |
-
|
| 302 |
-
// Load saved token
|
| 303 |
-
const savedToken = localStorage.getItem('solver_token');
|
| 304 |
-
if (savedToken) {
|
| 305 |
-
document.getElementById('solver-token').value = savedToken;
|
| 306 |
-
}
|
| 307 |
-
document.getElementById('solver-token').addEventListener('change', (e) => {
|
| 308 |
-
localStorage.setItem('solver_token', e.target.value);
|
| 309 |
-
});
|
| 310 |
-
|
| 311 |
-
async function updateConfig() {
|
| 312 |
-
const url = document.getElementById('solver-url').value.trim();
|
| 313 |
-
if (!url) return;
|
| 314 |
-
localStorage.setItem('solver_url', url);
|
| 315 |
-
const btn = document.querySelector('.config-row button');
|
| 316 |
-
btn.textContent = 'Fetching...';
|
| 317 |
-
btn.disabled = true;
|
| 318 |
-
try {
|
| 319 |
-
const resp = await api('/api/config', {
|
| 320 |
-
method: 'POST',
|
| 321 |
-
headers: {'Content-Type': 'application/json'},
|
| 322 |
-
body: JSON.stringify({solver_url: url})
|
| 323 |
-
});
|
| 324 |
-
const data = await resp.json();
|
| 325 |
-
if (data.status === 'ok') {
|
| 326 |
-
alert('Solver files updated successfully!');
|
| 327 |
-
} else {
|
| 328 |
-
alert('Update failed: ' + (data.error || 'unknown'));
|
| 329 |
-
}
|
| 330 |
-
} catch (e) { alert('Network error'); }
|
| 331 |
-
btn.textContent = 'Update';
|
| 332 |
-
btn.disabled = false;
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
// Task list
|
| 336 |
-
function renderTaskProgress(filesDone, status) {
|
| 337 |
-
const steps = filesDone.map(f =>
|
| 338 |
-
`<span class="task-file-step done">${f}</span>`
|
| 339 |
-
);
|
| 340 |
-
if (status === 'running' || status === 'pending') {
|
| 341 |
-
steps.push(`<span class="task-file-step current">running…</span>`);
|
| 342 |
-
}
|
| 343 |
-
return steps.join('');
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
async function loadTasks() {
|
| 347 |
-
try {
|
| 348 |
-
const resp = await api('/api/tasks');
|
| 349 |
-
const data = await resp.json();
|
| 350 |
-
const el = document.getElementById('task-list');
|
| 351 |
-
const downloadAll = document.getElementById('download-all-btn');
|
| 352 |
-
|
| 353 |
-
if (!data.tasks || data.tasks.length === 0) {
|
| 354 |
-
el.innerHTML = '<p style="color:#999;font-size:13px;">No tasks yet.</p>';
|
| 355 |
-
downloadAll.style.display = 'none';
|
| 356 |
-
document.getElementById('cleanup-btn').style.display = 'none';
|
| 357 |
-
return;
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
const hasComplete = data.tasks.some(t => t.status === 'complete');
|
| 361 |
-
downloadAll.style.display = hasComplete ? 'inline-block' : 'none';
|
| 362 |
-
const hasCleanup = data.tasks.some(t => t.status === 'complete' || t.status === 'error');
|
| 363 |
-
document.getElementById('cleanup-btn').style.display = hasCleanup ? 'inline-block' : 'none';
|
| 364 |
-
|
| 365 |
-
const token = getToken();
|
| 366 |
-
const tokenSep = token ? `?token=${encodeURIComponent(token)}&` : '?';
|
| 367 |
-
|
| 368 |
-
el.innerHTML = data.tasks.map(t => {
|
| 369 |
-
const badgeClass = 'badge-' + t.status;
|
| 370 |
-
const progressHtml = renderTaskProgress(t.files_done || [], t.status);
|
| 371 |
-
const deleteBtn = t.status !== 'running'
|
| 372 |
-
? `<span class="task-delete" onclick="event.stopPropagation();deleteTask('${t.task_id}')" title="Delete">✕</span>`
|
| 373 |
-
: '';
|
| 374 |
-
return `<div class="task-item" onclick="openTask('${t.task_id}')">
|
| 375 |
-
<div style="flex:1;min-width:0;">
|
| 376 |
-
<div class="task-name">${t.name || t.task_id}</div>
|
| 377 |
-
<div class="task-preview">${t.problem_preview || ''}</div>
|
| 378 |
-
<div class="task-files">${progressHtml}</div>
|
| 379 |
-
</div>
|
| 380 |
-
<div class="task-meta">
|
| 381 |
-
<span class="badge ${badgeClass}">${t.status}</span>
|
| 382 |
-
${t.status === 'complete' ? `<a class="btn-download" href="/api/download/${t.task_id}${token ? '?token=' + encodeURIComponent(token) : ''}" onclick="event.stopPropagation()">↓ Download</a>` : ''}
|
| 383 |
-
${deleteBtn}
|
| 384 |
-
</div>
|
| 385 |
-
</div>`;
|
| 386 |
-
}).join('');
|
| 387 |
-
} catch (e) {}
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
// Task detail modal
|
| 391 |
-
async function openTask(taskId) {
|
| 392 |
-
const overlay = document.getElementById('modal-overlay');
|
| 393 |
-
const body = document.getElementById('modal-body');
|
| 394 |
-
const title = document.getElementById('modal-title');
|
| 395 |
-
const downloadBtn = document.getElementById('modal-download');
|
| 396 |
-
|
| 397 |
-
title.textContent = 'Loading...';
|
| 398 |
-
downloadBtn.style.display = 'none';
|
| 399 |
-
body.innerHTML = '<p style="color:#888;">Loading task info...</p>';
|
| 400 |
-
overlay.classList.add('visible');
|
| 401 |
-
|
| 402 |
-
try {
|
| 403 |
-
const resp = await api('/api/status/' + taskId);
|
| 404 |
-
const t = await resp.json();
|
| 405 |
-
title.textContent = t.name || taskId;
|
| 406 |
-
|
| 407 |
-
const progressHtml = renderTaskProgress(t.files_done || [], t.status);
|
| 408 |
-
|
| 409 |
-
if (t.status === 'complete') {
|
| 410 |
-
const resResp = await api('/api/result/' + taskId);
|
| 411 |
-
const resData = await resResp.json();
|
| 412 |
-
if (resData.content) {
|
| 413 |
-
body.innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="result-content">${marked.parse(resData.content)}</div>`;
|
| 414 |
-
}
|
| 415 |
-
downloadBtn.href = `/api/download/${taskId}${token ? '?token=' + encodeURIComponent(token) : ''}`;
|
| 416 |
-
downloadBtn.style.display = 'inline-block';
|
| 417 |
-
} else if (t.status === 'error') {
|
| 418 |
-
body.innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="error-msg visible">${t.error || 'Task failed'}</div>`;
|
| 419 |
-
} else {
|
| 420 |
-
// Running/pending: show progress + logs
|
| 421 |
-
const logResp = await api('/api/logs/' + taskId + '?lines=50');
|
| 422 |
-
const logData = await logResp.json();
|
| 423 |
-
body.innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="log-console">${(logData.logs || []).map(l => escapeHtml(l)).join('\n')}</div>`;
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
// Start polling if running
|
| 427 |
-
if (t.status === 'running' || t.status === 'pending') {
|
| 428 |
-
startTaskPoll(taskId);
|
| 429 |
-
}
|
| 430 |
-
} catch (e) {
|
| 431 |
-
body.innerHTML = '<p style="color:#888;">Failed to load task.</p>';
|
| 432 |
-
}
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
-
function startTaskPoll(taskId) {
|
| 436 |
-
if (pollTimers[taskId]) clearInterval(pollTimers[taskId]);
|
| 437 |
-
pollTimers[taskId] = setInterval(async () => {
|
| 438 |
-
try {
|
| 439 |
-
const resp = await api('/api/status/' + taskId);
|
| 440 |
-
const t = await resp.json();
|
| 441 |
-
if (t.status === 'complete') {
|
| 442 |
-
clearInterval(pollTimers[taskId]);
|
| 443 |
-
delete pollTimers[taskId];
|
| 444 |
-
openTask(taskId); // Reload to show result
|
| 445 |
-
loadTasks();
|
| 446 |
-
} else if (t.status === 'error') {
|
| 447 |
-
clearInterval(pollTimers[taskId]);
|
| 448 |
-
delete pollTimers[taskId];
|
| 449 |
-
const progressHtml = renderTaskProgress(t.files_done || [], t.status);
|
| 450 |
-
document.getElementById('modal-body').innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="error-msg visible">${t.error || 'Task failed'}</div>`;
|
| 451 |
-
loadTasks();
|
| 452 |
-
} else {
|
| 453 |
-
// Update progress and logs
|
| 454 |
-
const progressHtml = renderTaskProgress(t.files_done || [], t.status);
|
| 455 |
-
const body = document.getElementById('modal-body');
|
| 456 |
-
if (body) {
|
| 457 |
-
const existingLogs = body.querySelector('.log-console');
|
| 458 |
-
const logContent = existingLogs ? existingLogs.innerHTML : '';
|
| 459 |
-
body.innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="log-console">${logContent}</div>`;
|
| 460 |
-
const newConsole = body.querySelector('.log-console');
|
| 461 |
-
if (newConsole) newConsole.scrollTop = newConsole.scrollHeight;
|
| 462 |
-
}
|
| 463 |
-
const logResp = await api('/api/logs/' + taskId);
|
| 464 |
-
const logData = await logResp.json();
|
| 465 |
-
const consoleEl = document.querySelector('.log-console');
|
| 466 |
-
if (consoleEl && logData.logs) {
|
| 467 |
-
consoleEl.textContent = logData.logs.join('\n');
|
| 468 |
-
consoleEl.scrollTop = consoleEl.scrollHeight;
|
| 469 |
-
}
|
| 470 |
-
}
|
| 471 |
-
} catch (e) {}
|
| 472 |
-
}, 3000);
|
| 473 |
-
}
|
| 474 |
-
|
| 475 |
-
// Delete task
|
| 476 |
-
async function deleteTask(taskId) {
|
| 477 |
-
if (!confirm('Delete this task? This cannot be undone.')) return;
|
| 478 |
-
try {
|
| 479 |
-
const resp = await api(`/api/task/${taskId}`, { method: 'DELETE' });
|
| 480 |
-
const data = await resp.json();
|
| 481 |
-
if (data.error) { showError(data.error); return; }
|
| 482 |
-
loadTasks();
|
| 483 |
-
} catch (e) { showError('Network error: ' + e.message); }
|
| 484 |
-
}
|
| 485 |
-
|
| 486 |
-
// Cleanup completed/errored tasks
|
| 487 |
-
async function cleanupTasks() {
|
| 488 |
-
if (!confirm('Clear all completed and errored tasks? This cannot be undone.')) return;
|
| 489 |
-
try {
|
| 490 |
-
const resp = await api('/api/tasks/cleanup', { method: 'POST' });
|
| 491 |
-
const data = await resp.json();
|
| 492 |
-
loadTasks();
|
| 493 |
-
} catch (e) { showError('Network error: ' + e.message); }
|
| 494 |
-
}
|
| 495 |
-
|
| 496 |
-
function closeModal(e) {
|
| 497 |
-
if (e.target === document.getElementById('modal-overlay')) {
|
| 498 |
-
closeModalDirect();
|
| 499 |
-
}
|
| 500 |
-
}
|
| 501 |
-
|
| 502 |
-
function closeModalDirect() {
|
| 503 |
-
document.getElementById('modal-overlay').classList.remove('visible');
|
| 504 |
-
Object.values(pollTimers).forEach(t => clearInterval(t));
|
| 505 |
-
pollTimers = {};
|
| 506 |
-
}
|
| 507 |
-
|
| 508 |
-
function downloadAll() {
|
| 509 |
-
const token = getToken();
|
| 510 |
-
window.location.href = `/api/download-all${token ? '?token=' + encodeURIComponent(token) : ''}`;
|
| 511 |
-
}
|
| 512 |
-
|
| 513 |
-
function escapeHtml(s) {
|
| 514 |
-
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 515 |
-
}
|
| 516 |
-
|
| 517 |
-
// Initial load
|
| 518 |
-
loadTasks();
|
| 519 |
-
setInterval(loadTasks, 5000);
|
| 520 |
-
</script>
|
| 521 |
-
</body>
|
| 522 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|