VeryOnly123 commited on
Commit
2e8575b
·
1 Parent(s): 5de2050
Files changed (6) hide show
  1. Dockerfile +0 -39
  2. README.md +0 -10
  3. requirements.txt +0 -1
  4. settings.json +0 -3
  5. solver.py +0 -418
  6. 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
515
- }
516
-
517
- // Initial load
518
- loadTasks();
519
- setInterval(loadTasks, 5000);
520
- </script>
521
- </body>
522
- </html>