Spaces:
Running
Running
v21.0
Browse files- .claude/settings.local.json +5 -1
- backend/code.py +332 -4
- backend/main.py +182 -19
- dev/dr-tulu-integradion.md +585 -0
- features.md +2 -1
- frontend/index.html +22 -3
- frontend/script.js +327 -10
- frontend/style.css +317 -27
- tests/backend/test_api.py +130 -0
- tests/e2e/app.spec.js +30 -0
- tests/playwright.config.js +1 -1
- workspace/test/data.json +802 -0
- workspace/test/synthetic_data.csv +151 -0
- workspace/test/synthetic_data_analysis.png +0 -0
.claude/settings.local.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
{
|
| 2 |
"permissions": {
|
| 3 |
"allow": [
|
| 4 |
-
"Bash(open index.html)"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
],
|
| 6 |
"deny": [],
|
| 7 |
"ask": []
|
|
|
|
| 1 |
{
|
| 2 |
"permissions": {
|
| 3 |
"allow": [
|
| 4 |
+
"Bash(open index.html)",
|
| 5 |
+
"Bash(npm test:*)",
|
| 6 |
+
"Bash(npx playwright test:*)",
|
| 7 |
+
"Bash(source env312/bin/activate:*)",
|
| 8 |
+
"Bash(python -m pytest:*)"
|
| 9 |
],
|
| 10 |
"deny": [],
|
| 11 |
"ask": []
|
backend/code.py
CHANGED
|
@@ -25,6 +25,55 @@ TOOLS = [
|
|
| 25 |
"required": ["code"]
|
| 26 |
}
|
| 27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
]
|
| 30 |
|
|
@@ -81,12 +130,126 @@ def format_thinking_cell(content: str):
|
|
| 81 |
}
|
| 82 |
|
| 83 |
|
| 84 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
"""
|
| 86 |
Stream code execution results
|
| 87 |
|
| 88 |
Yields:
|
| 89 |
dict: Updates with type 'thinking', 'code', or 'done'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
"""
|
| 91 |
turns = 0
|
| 92 |
done = False
|
|
@@ -140,9 +303,53 @@ def stream_code_execution(client, model: str, messages: List[Dict], sbx: Sandbox
|
|
| 140 |
try:
|
| 141 |
args = json.loads(tool_call.function.arguments)
|
| 142 |
code = args["code"]
|
| 143 |
-
except:
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
# Send code cell (before execution)
|
| 148 |
yield {"type": "code_start", "code": code}
|
|
@@ -204,6 +411,127 @@ def stream_code_execution(client, model: str, messages: List[Dict], sbx: Sandbox
|
|
| 204 |
}]
|
| 205 |
})
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
messages.append({
|
| 208 |
"role": "tool",
|
| 209 |
"tool_call_id": tool_call.id,
|
|
|
|
| 25 |
"required": ["code"]
|
| 26 |
}
|
| 27 |
}
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"type": "function",
|
| 31 |
+
"function": {
|
| 32 |
+
"name": "upload_files",
|
| 33 |
+
"description": "Upload files from the local workspace to the code execution environment for analysis. Files will be available at /home/user/<filename>. Use this to load data files, scripts, or any files you need to analyze.",
|
| 34 |
+
"parameters": {
|
| 35 |
+
"type": "object",
|
| 36 |
+
"properties": {
|
| 37 |
+
"paths": {
|
| 38 |
+
"type": "array",
|
| 39 |
+
"items": {"type": "string"},
|
| 40 |
+
"description": "List of file paths relative to the workspace root (e.g., ['data/sales.csv', 'config.json'])"
|
| 41 |
+
}
|
| 42 |
+
},
|
| 43 |
+
"required": ["paths"]
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"type": "function",
|
| 49 |
+
"function": {
|
| 50 |
+
"name": "download_files",
|
| 51 |
+
"description": "Download files from the code execution environment to the local workspace. Use this to save generated files, processed data, or any output files you want to keep.",
|
| 52 |
+
"parameters": {
|
| 53 |
+
"type": "object",
|
| 54 |
+
"properties": {
|
| 55 |
+
"files": {
|
| 56 |
+
"type": "array",
|
| 57 |
+
"items": {
|
| 58 |
+
"type": "object",
|
| 59 |
+
"properties": {
|
| 60 |
+
"sandbox_path": {
|
| 61 |
+
"type": "string",
|
| 62 |
+
"description": "Path in the sandbox (e.g., '/home/user/output.csv')"
|
| 63 |
+
},
|
| 64 |
+
"local_path": {
|
| 65 |
+
"type": "string",
|
| 66 |
+
"description": "Destination path relative to workspace (e.g., 'results/output.csv')"
|
| 67 |
+
}
|
| 68 |
+
},
|
| 69 |
+
"required": ["sandbox_path", "local_path"]
|
| 70 |
+
},
|
| 71 |
+
"description": "List of files to download with their sandbox and local paths"
|
| 72 |
+
}
|
| 73 |
+
},
|
| 74 |
+
"required": ["files"]
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
}
|
| 78 |
]
|
| 79 |
|
|
|
|
| 130 |
}
|
| 131 |
|
| 132 |
|
| 133 |
+
def upload_files_to_sandbox(sbx: Sandbox, paths: List[str], files_root: str) -> str:
|
| 134 |
+
"""
|
| 135 |
+
Upload multiple files to the sandbox.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
sbx: E2B sandbox instance
|
| 139 |
+
paths: List of relative file paths
|
| 140 |
+
files_root: Root directory to resolve relative paths
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
String describing what was uploaded or errors encountered
|
| 144 |
+
"""
|
| 145 |
+
results = []
|
| 146 |
+
|
| 147 |
+
for rel_path in paths:
|
| 148 |
+
# Normalize the path (remove ./ prefix if present)
|
| 149 |
+
rel_path = rel_path.lstrip('./')
|
| 150 |
+
local_path = os.path.join(files_root, rel_path)
|
| 151 |
+
|
| 152 |
+
# Security check: ensure path doesn't escape files_root
|
| 153 |
+
real_local = os.path.realpath(local_path)
|
| 154 |
+
real_root = os.path.realpath(files_root)
|
| 155 |
+
if not real_local.startswith(real_root):
|
| 156 |
+
results.append(f"Error: {rel_path} - path outside workspace")
|
| 157 |
+
continue
|
| 158 |
+
|
| 159 |
+
if not os.path.exists(local_path):
|
| 160 |
+
results.append(f"Error: {rel_path} - file not found")
|
| 161 |
+
continue
|
| 162 |
+
|
| 163 |
+
if not os.path.isfile(local_path):
|
| 164 |
+
results.append(f"Error: {rel_path} - not a file")
|
| 165 |
+
continue
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
# Get just the filename for the sandbox path
|
| 169 |
+
filename = os.path.basename(rel_path)
|
| 170 |
+
sandbox_path = f"/home/user/{filename}"
|
| 171 |
+
|
| 172 |
+
with open(local_path, "rb") as f:
|
| 173 |
+
sbx.files.write(sandbox_path, f)
|
| 174 |
+
|
| 175 |
+
results.append(f"Uploaded: {rel_path} -> {sandbox_path}")
|
| 176 |
+
except Exception as e:
|
| 177 |
+
results.append(f"Error uploading {rel_path}: {str(e)}")
|
| 178 |
+
|
| 179 |
+
return "\n".join(results)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def download_files_from_sandbox(sbx: Sandbox, files: List[Dict], files_root: str) -> str:
|
| 183 |
+
"""
|
| 184 |
+
Download multiple files from the sandbox to the local workspace.
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
sbx: E2B sandbox instance
|
| 188 |
+
files: List of dicts with 'sandbox_path' and 'local_path' keys
|
| 189 |
+
files_root: Root directory to resolve relative paths
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
String describing what was downloaded or errors encountered
|
| 193 |
+
"""
|
| 194 |
+
results = []
|
| 195 |
+
|
| 196 |
+
for file_spec in files:
|
| 197 |
+
sandbox_path = file_spec.get('sandbox_path', '')
|
| 198 |
+
local_rel_path = file_spec.get('local_path', '')
|
| 199 |
+
|
| 200 |
+
if not sandbox_path or not local_rel_path:
|
| 201 |
+
results.append(f"Error: Missing sandbox_path or local_path")
|
| 202 |
+
continue
|
| 203 |
+
|
| 204 |
+
# Normalize the local path (remove ./ prefix if present)
|
| 205 |
+
local_rel_path = local_rel_path.lstrip('./')
|
| 206 |
+
local_path = os.path.join(files_root, local_rel_path)
|
| 207 |
+
|
| 208 |
+
# Security check: ensure path doesn't escape files_root
|
| 209 |
+
real_local = os.path.realpath(os.path.dirname(local_path))
|
| 210 |
+
real_root = os.path.realpath(files_root)
|
| 211 |
+
# Need to handle case where parent dir doesn't exist yet
|
| 212 |
+
test_path = local_path
|
| 213 |
+
while not os.path.exists(os.path.dirname(test_path)):
|
| 214 |
+
test_path = os.path.dirname(test_path)
|
| 215 |
+
real_local = os.path.realpath(test_path)
|
| 216 |
+
if not real_local.startswith(real_root):
|
| 217 |
+
results.append(f"Error: {local_rel_path} - path outside workspace")
|
| 218 |
+
continue
|
| 219 |
+
|
| 220 |
+
try:
|
| 221 |
+
# Read file content from sandbox
|
| 222 |
+
content = sbx.files.read(sandbox_path)
|
| 223 |
+
|
| 224 |
+
# Create parent directories if needed
|
| 225 |
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
| 226 |
+
|
| 227 |
+
# Write to local file
|
| 228 |
+
# Content from e2b can be bytes or string
|
| 229 |
+
mode = 'wb' if isinstance(content, bytes) else 'w'
|
| 230 |
+
with open(local_path, mode) as f:
|
| 231 |
+
f.write(content)
|
| 232 |
+
|
| 233 |
+
results.append(f"Downloaded: {sandbox_path} -> {local_rel_path}")
|
| 234 |
+
except Exception as e:
|
| 235 |
+
results.append(f"Error downloading {sandbox_path}: {str(e)}")
|
| 236 |
+
|
| 237 |
+
return "\n".join(results)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def stream_code_execution(client, model: str, messages: List[Dict], sbx: Sandbox, files_root: str = None):
|
| 241 |
"""
|
| 242 |
Stream code execution results
|
| 243 |
|
| 244 |
Yields:
|
| 245 |
dict: Updates with type 'thinking', 'code', or 'done'
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
client: OpenAI-compatible client
|
| 249 |
+
model: Model name to use
|
| 250 |
+
messages: Conversation messages
|
| 251 |
+
sbx: E2B sandbox instance
|
| 252 |
+
files_root: Root directory for file uploads (optional)
|
| 253 |
"""
|
| 254 |
turns = 0
|
| 255 |
done = False
|
|
|
|
| 303 |
try:
|
| 304 |
args = json.loads(tool_call.function.arguments)
|
| 305 |
code = args["code"]
|
| 306 |
+
except json.JSONDecodeError as e:
|
| 307 |
+
error_msg = f"JSON parse error: {e}. Raw arguments: {tool_call.function.arguments[:500]}"
|
| 308 |
+
print(f"[code.py] {error_msg}")
|
| 309 |
+
# Treat as tool error so LLM can recover
|
| 310 |
+
output = f"Error parsing code arguments: {e}"
|
| 311 |
+
messages.append({
|
| 312 |
+
"role": "assistant",
|
| 313 |
+
"content": content,
|
| 314 |
+
"tool_calls": [{
|
| 315 |
+
"id": tool_call.id,
|
| 316 |
+
"type": "function",
|
| 317 |
+
"function": {
|
| 318 |
+
"name": tool_call.function.name,
|
| 319 |
+
"arguments": tool_call.function.arguments,
|
| 320 |
+
}
|
| 321 |
+
}]
|
| 322 |
+
})
|
| 323 |
+
messages.append({
|
| 324 |
+
"role": "tool",
|
| 325 |
+
"tool_call_id": tool_call.id,
|
| 326 |
+
"content": output
|
| 327 |
+
})
|
| 328 |
+
yield {"type": "error", "content": f"Failed to parse code arguments: {e}"}
|
| 329 |
+
continue
|
| 330 |
+
except KeyError as e:
|
| 331 |
+
error_msg = f"Missing required key {e} in arguments: {tool_call.function.arguments[:500]}"
|
| 332 |
+
print(f"[code.py] {error_msg}")
|
| 333 |
+
output = f"Error: Missing required 'code' parameter"
|
| 334 |
+
messages.append({
|
| 335 |
+
"role": "assistant",
|
| 336 |
+
"content": content,
|
| 337 |
+
"tool_calls": [{
|
| 338 |
+
"id": tool_call.id,
|
| 339 |
+
"type": "function",
|
| 340 |
+
"function": {
|
| 341 |
+
"name": tool_call.function.name,
|
| 342 |
+
"arguments": tool_call.function.arguments,
|
| 343 |
+
}
|
| 344 |
+
}]
|
| 345 |
+
})
|
| 346 |
+
messages.append({
|
| 347 |
+
"role": "tool",
|
| 348 |
+
"tool_call_id": tool_call.id,
|
| 349 |
+
"content": output
|
| 350 |
+
})
|
| 351 |
+
yield {"type": "error", "content": output}
|
| 352 |
+
continue
|
| 353 |
|
| 354 |
# Send code cell (before execution)
|
| 355 |
yield {"type": "code_start", "code": code}
|
|
|
|
| 411 |
}]
|
| 412 |
})
|
| 413 |
|
| 414 |
+
messages.append({
|
| 415 |
+
"role": "tool",
|
| 416 |
+
"tool_call_id": tool_call.id,
|
| 417 |
+
"content": output
|
| 418 |
+
})
|
| 419 |
+
|
| 420 |
+
elif tool_call.function.name == "upload_files":
|
| 421 |
+
# Parse arguments
|
| 422 |
+
try:
|
| 423 |
+
args = json.loads(tool_call.function.arguments)
|
| 424 |
+
paths = args["paths"]
|
| 425 |
+
except (json.JSONDecodeError, KeyError) as e:
|
| 426 |
+
error_msg = f"Failed to parse upload_files arguments: {e}. Raw: {tool_call.function.arguments[:500]}"
|
| 427 |
+
print(f"[code.py] {error_msg}")
|
| 428 |
+
output = f"Error parsing upload_files arguments: {e}"
|
| 429 |
+
messages.append({
|
| 430 |
+
"role": "assistant",
|
| 431 |
+
"content": content,
|
| 432 |
+
"tool_calls": [{
|
| 433 |
+
"id": tool_call.id,
|
| 434 |
+
"type": "function",
|
| 435 |
+
"function": {
|
| 436 |
+
"name": tool_call.function.name,
|
| 437 |
+
"arguments": tool_call.function.arguments,
|
| 438 |
+
}
|
| 439 |
+
}]
|
| 440 |
+
})
|
| 441 |
+
messages.append({
|
| 442 |
+
"role": "tool",
|
| 443 |
+
"tool_call_id": tool_call.id,
|
| 444 |
+
"content": output
|
| 445 |
+
})
|
| 446 |
+
yield {"type": "error", "content": output}
|
| 447 |
+
continue
|
| 448 |
+
|
| 449 |
+
# Check if files_root is available
|
| 450 |
+
if not files_root:
|
| 451 |
+
output = "Error: File upload not available - no workspace configured"
|
| 452 |
+
else:
|
| 453 |
+
# Upload files
|
| 454 |
+
output = upload_files_to_sandbox(sbx, paths, files_root)
|
| 455 |
+
|
| 456 |
+
# Send upload notification to UI
|
| 457 |
+
yield {"type": "upload", "paths": paths, "output": output}
|
| 458 |
+
|
| 459 |
+
# Add to message history
|
| 460 |
+
messages.append({
|
| 461 |
+
"role": "assistant",
|
| 462 |
+
"content": content,
|
| 463 |
+
"tool_calls": [{
|
| 464 |
+
"id": tool_call.id,
|
| 465 |
+
"type": "function",
|
| 466 |
+
"function": {
|
| 467 |
+
"name": tool_call.function.name,
|
| 468 |
+
"arguments": tool_call.function.arguments,
|
| 469 |
+
}
|
| 470 |
+
}]
|
| 471 |
+
})
|
| 472 |
+
|
| 473 |
+
messages.append({
|
| 474 |
+
"role": "tool",
|
| 475 |
+
"tool_call_id": tool_call.id,
|
| 476 |
+
"content": output
|
| 477 |
+
})
|
| 478 |
+
|
| 479 |
+
elif tool_call.function.name == "download_files":
|
| 480 |
+
# Parse arguments
|
| 481 |
+
try:
|
| 482 |
+
args = json.loads(tool_call.function.arguments)
|
| 483 |
+
files = args["files"]
|
| 484 |
+
except (json.JSONDecodeError, KeyError) as e:
|
| 485 |
+
error_msg = f"Failed to parse download_files arguments: {e}. Raw: {tool_call.function.arguments[:500]}"
|
| 486 |
+
print(f"[code.py] {error_msg}")
|
| 487 |
+
output = f"Error parsing download_files arguments: {e}"
|
| 488 |
+
messages.append({
|
| 489 |
+
"role": "assistant",
|
| 490 |
+
"content": content,
|
| 491 |
+
"tool_calls": [{
|
| 492 |
+
"id": tool_call.id,
|
| 493 |
+
"type": "function",
|
| 494 |
+
"function": {
|
| 495 |
+
"name": tool_call.function.name,
|
| 496 |
+
"arguments": tool_call.function.arguments,
|
| 497 |
+
}
|
| 498 |
+
}]
|
| 499 |
+
})
|
| 500 |
+
messages.append({
|
| 501 |
+
"role": "tool",
|
| 502 |
+
"tool_call_id": tool_call.id,
|
| 503 |
+
"content": output
|
| 504 |
+
})
|
| 505 |
+
yield {"type": "error", "content": output}
|
| 506 |
+
continue
|
| 507 |
+
|
| 508 |
+
# Check if files_root is available
|
| 509 |
+
if not files_root:
|
| 510 |
+
output = "Error: File download not available - no workspace configured"
|
| 511 |
+
else:
|
| 512 |
+
# Download files
|
| 513 |
+
output = download_files_from_sandbox(sbx, files, files_root)
|
| 514 |
+
|
| 515 |
+
# Extract paths for UI display
|
| 516 |
+
paths = [f"{f.get('sandbox_path', '')} -> {f.get('local_path', '')}" for f in files]
|
| 517 |
+
|
| 518 |
+
# Send download notification to UI
|
| 519 |
+
yield {"type": "download", "paths": paths, "output": output}
|
| 520 |
+
|
| 521 |
+
# Add to message history
|
| 522 |
+
messages.append({
|
| 523 |
+
"role": "assistant",
|
| 524 |
+
"content": content,
|
| 525 |
+
"tool_calls": [{
|
| 526 |
+
"id": tool_call.id,
|
| 527 |
+
"type": "function",
|
| 528 |
+
"function": {
|
| 529 |
+
"name": tool_call.function.name,
|
| 530 |
+
"arguments": tool_call.function.arguments,
|
| 531 |
+
}
|
| 532 |
+
}]
|
| 533 |
+
})
|
| 534 |
+
|
| 535 |
messages.append({
|
| 536 |
"role": "tool",
|
| 537 |
"tool_call_id": tool_call.id,
|
backend/main.py
CHANGED
|
@@ -88,6 +88,11 @@ Examples:
|
|
| 88 |
You: Summarize the research results without using tools
|
| 89 |
|
| 90 |
Be concise and helpful. Don't duplicate effort - either answer directly OR launch a notebook, not both. Answer questions about results directly without launching new notebooks.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
""",
|
| 92 |
"agent": """You are an autonomous agent assistant specialized in breaking down and executing multi-step tasks.
|
| 93 |
|
|
@@ -103,24 +108,37 @@ Focus on being proactive, organized, and thorough in completing multi-step workf
|
|
| 103 |
|
| 104 |
Your role is to:
|
| 105 |
- Write and execute Python code to solve problems
|
| 106 |
-
- Analyze data and
|
| 107 |
- Debug code and explain errors
|
| 108 |
- Break down complex tasks into executable steps
|
| 109 |
|
| 110 |
You have access to a Jupyter kernel with these packages:
|
| 111 |
pandas, matplotlib, numpy, scipy, scikit-learn, seaborn, plotly, requests, beautifulsoup4, and more.
|
| 112 |
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
When solving problems:
|
| 116 |
1. Break down the task into logical steps
|
| 117 |
2. Execute code incrementally
|
| 118 |
3. Check outputs before proceeding
|
| 119 |
-
4.
|
| 120 |
|
| 121 |
-
IMPORTANT: When you generate plots/figures, they are automatically named as figure_1, figure_2, etc. The execution output will show which figures were created (e.g., "[Generated figures: figure_1, figure_2]").
|
| 122 |
|
| 123 |
-
|
|
|
|
|
|
|
| 124 |
|
| 125 |
Example:
|
| 126 |
<result>
|
|
@@ -133,7 +151,7 @@ The plot shows both functions overlaid on the same axes.
|
|
| 133 |
|
| 134 |
IMPORTANT: Use self-closing tags like <figure_1> (NOT </figure_1> or <figure_1></figure_1>). Each tag will be replaced with the actual image.
|
| 135 |
|
| 136 |
-
The result will be sent back to the command center with embedded images.
|
| 137 |
|
| 138 |
Focus on being precise, practical, and thorough in your coding assistance.
|
| 139 |
""",
|
|
@@ -203,6 +221,12 @@ class Message(BaseModel):
|
|
| 203 |
content: str
|
| 204 |
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
class ChatRequest(BaseModel):
|
| 207 |
messages: List[Message]
|
| 208 |
notebook_type: str = "command"
|
|
@@ -216,6 +240,7 @@ class ChatRequest(BaseModel):
|
|
| 216 |
research_parallel_workers: Optional[int] = None # Number of parallel workers for research
|
| 217 |
research_max_websites: Optional[int] = None # Max websites to analyze per research session
|
| 218 |
notebook_id: Optional[str] = None # Unique notebook/tab ID for session management
|
|
|
|
| 219 |
|
| 220 |
|
| 221 |
class TitleRequest(BaseModel):
|
|
@@ -241,7 +266,8 @@ async def stream_code_notebook(
|
|
| 241 |
model: str,
|
| 242 |
e2b_key: str,
|
| 243 |
session_id: str,
|
| 244 |
-
tab_id: str = "default"
|
|
|
|
| 245 |
):
|
| 246 |
"""Handle code notebook with execution capabilities"""
|
| 247 |
|
|
@@ -264,8 +290,8 @@ async def stream_code_notebook(
|
|
| 264 |
# Create OpenAI client with user's endpoint
|
| 265 |
client = OpenAI(base_url=endpoint, api_key=token)
|
| 266 |
|
| 267 |
-
# Add system prompt for code notebook
|
| 268 |
-
system_prompt =
|
| 269 |
full_messages = [
|
| 270 |
{"role": "system", "content": system_prompt}
|
| 271 |
] + messages
|
|
@@ -274,7 +300,7 @@ async def stream_code_notebook(
|
|
| 274 |
record_api_call(tab_id, full_messages)
|
| 275 |
|
| 276 |
# Stream code execution
|
| 277 |
-
for update in stream_code_execution(client, model, full_messages, sbx):
|
| 278 |
# Forward updates to frontend
|
| 279 |
yield f"data: {json.dumps(update)}\n\n"
|
| 280 |
|
|
@@ -306,7 +332,7 @@ async def stream_code_notebook(
|
|
| 306 |
yield f"data: {json.dumps({'type': 'info', 'content': 'New sandbox created. Retrying execution...'})}\n\n"
|
| 307 |
|
| 308 |
# Retry code execution with new sandbox
|
| 309 |
-
for update in stream_code_execution(client, model, full_messages, sbx):
|
| 310 |
yield f"data: {json.dumps(update)}\n\n"
|
| 311 |
|
| 312 |
except Exception as retry_error:
|
|
@@ -347,8 +373,8 @@ async def stream_research_notebook(
|
|
| 347 |
# Create OpenAI client
|
| 348 |
client = OpenAI(base_url=endpoint, api_key=token)
|
| 349 |
|
| 350 |
-
# Get system prompt for research
|
| 351 |
-
system_prompt =
|
| 352 |
|
| 353 |
# Store for debugging (simplified version for research)
|
| 354 |
full_messages = [{"role": "system", "content": system_prompt}] + messages
|
|
@@ -399,8 +425,8 @@ async def stream_command_center_notebook(
|
|
| 399 |
if tab_id not in CONVERSATION_HISTORY:
|
| 400 |
CONVERSATION_HISTORY[tab_id] = []
|
| 401 |
|
| 402 |
-
# Add system prompt for command center
|
| 403 |
-
system_prompt =
|
| 404 |
|
| 405 |
# Build full messages: system + stored history + new messages
|
| 406 |
print(f"DEBUG: tab_id={tab_id}, incoming messages={messages}")
|
|
@@ -458,8 +484,8 @@ async def stream_chat_response(
|
|
| 458 |
print(f"Messages: {len(messages)} messages")
|
| 459 |
print(f"Token provided: {bool(token)}")
|
| 460 |
|
| 461 |
-
# Prepare messages with appropriate system prompt based on notebook type
|
| 462 |
-
system_prompt =
|
| 463 |
full_messages = [
|
| 464 |
{"role": "system", "content": system_prompt}
|
| 465 |
] + messages
|
|
@@ -623,6 +649,9 @@ async def chat_stream(request: ChatRequest):
|
|
| 623 |
# Get tab_id for debugging
|
| 624 |
tab_id = request.notebook_id or "0"
|
| 625 |
|
|
|
|
|
|
|
|
|
|
| 626 |
# Route to code execution handler for code notebooks
|
| 627 |
if request.notebook_type == "code":
|
| 628 |
# Use notebook_id as session key, fallback to "default" if not provided
|
|
@@ -636,7 +665,8 @@ async def chat_stream(request: ChatRequest):
|
|
| 636 |
request.model or "gpt-4",
|
| 637 |
request.e2b_key or "",
|
| 638 |
session_id,
|
| 639 |
-
tab_id
|
|
|
|
| 640 |
),
|
| 641 |
media_type="text/event-stream",
|
| 642 |
headers={
|
|
@@ -809,6 +839,15 @@ async def health():
|
|
| 809 |
# These can be overridden via command-line arguments or set_*_file functions
|
| 810 |
SETTINGS_FILE = os.path.join(PROJECT_ROOT, "settings.json")
|
| 811 |
WORKSPACE_FILE = os.path.join(PROJECT_ROOT, "workspace.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
|
| 813 |
def set_settings_file(path: str):
|
| 814 |
"""Set the settings file path (used for testing)"""
|
|
@@ -822,10 +861,11 @@ def set_workspace_file(path: str):
|
|
| 822 |
|
| 823 |
def set_data_dir(directory: str):
|
| 824 |
"""Set the data directory containing settings.json and workspace.json"""
|
| 825 |
-
global SETTINGS_FILE, WORKSPACE_FILE
|
| 826 |
os.makedirs(directory, exist_ok=True)
|
| 827 |
SETTINGS_FILE = os.path.join(directory, "settings.json")
|
| 828 |
WORKSPACE_FILE = os.path.join(directory, "workspace.json")
|
|
|
|
| 829 |
|
| 830 |
|
| 831 |
@app.get("/api/settings")
|
|
@@ -923,6 +963,129 @@ async def clear_workspace():
|
|
| 923 |
raise HTTPException(status_code=500, detail=f"Failed to clear workspace: {str(e)}")
|
| 924 |
|
| 925 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
# ============================================
|
| 927 |
# Static File Serving (Frontend)
|
| 928 |
# ============================================
|
|
|
|
| 88 |
You: Summarize the research results without using tools
|
| 89 |
|
| 90 |
Be concise and helpful. Don't duplicate effort - either answer directly OR launch a notebook, not both. Answer questions about results directly without launching new notebooks.
|
| 91 |
+
|
| 92 |
+
IMPORTANT guidelines when delegating to notebooks:
|
| 93 |
+
- Do NOT ask notebooks to generate plots, visualizations, or artifacts unless the user explicitly requests them. Just ask for analysis/answers.
|
| 94 |
+
- Do NOT ask notebooks to save or create files unless the user explicitly requests it.
|
| 95 |
+
- NEVER overwrite existing files without explicit user permission.
|
| 96 |
""",
|
| 97 |
"agent": """You are an autonomous agent assistant specialized in breaking down and executing multi-step tasks.
|
| 98 |
|
|
|
|
| 108 |
|
| 109 |
Your role is to:
|
| 110 |
- Write and execute Python code to solve problems
|
| 111 |
+
- Analyze data and answer questions
|
| 112 |
- Debug code and explain errors
|
| 113 |
- Break down complex tasks into executable steps
|
| 114 |
|
| 115 |
You have access to a Jupyter kernel with these packages:
|
| 116 |
pandas, matplotlib, numpy, scipy, scikit-learn, seaborn, plotly, requests, beautifulsoup4, and more.
|
| 117 |
|
| 118 |
+
You have three tools available:
|
| 119 |
+
- execute_code: Run Python code. The execution environment is stateful - variables and imports persist between calls.
|
| 120 |
+
- upload_files: Upload files from the workspace to the execution environment for analysis. Files will be available at /home/user/<filename>. Use this when you need to analyze data files, scripts, or other files from the project.
|
| 121 |
+
- download_files: Download files from the execution environment to the workspace. ONLY use this when the user explicitly asks to save/download files, or when saving files is clearly part of the task (e.g., "generate a dataset and save it"). Do NOT automatically save intermediate files, plots, or outputs unless requested.
|
| 122 |
+
|
| 123 |
+
## IMPORTANT Guidelines
|
| 124 |
+
|
| 125 |
+
**Only create artifacts when explicitly requested:**
|
| 126 |
+
- Do NOT generate plots, charts, or visualizations unless the user explicitly asks for them
|
| 127 |
+
- Do NOT save files unless the user explicitly asks to save/export/download
|
| 128 |
+
- NEVER overwrite existing files without explicit user permission - always ask first or use a new filename
|
| 129 |
+
- Focus on answering questions and providing analysis, not producing visual outputs by default
|
| 130 |
|
| 131 |
When solving problems:
|
| 132 |
1. Break down the task into logical steps
|
| 133 |
2. Execute code incrementally
|
| 134 |
3. Check outputs before proceeding
|
| 135 |
+
4. Only create visualizations if explicitly requested
|
| 136 |
|
| 137 |
+
IMPORTANT: When you DO generate plots/figures (only when requested), they are automatically named as figure_1, figure_2, etc. The execution output will show which figures were created (e.g., "[Generated figures: figure_1, figure_2]").
|
| 138 |
|
| 139 |
+
## CRITICAL: You MUST provide a <result> tag
|
| 140 |
+
|
| 141 |
+
When you have completed the task, you MUST ALWAYS provide a summary using the <result> tag. This is REQUIRED - without it, your work will not be visible in the command center. To include figures in your result (when you've created them), use self-closing figure tags like <figure_1>, <figure_2>, <figure_3> etc.:
|
| 142 |
|
| 143 |
Example:
|
| 144 |
<result>
|
|
|
|
| 151 |
|
| 152 |
IMPORTANT: Use self-closing tags like <figure_1> (NOT </figure_1> or <figure_1></figure_1>). Each tag will be replaced with the actual image.
|
| 153 |
|
| 154 |
+
The result will be sent back to the command center with embedded images. DO NOT forget the <result> tag - it is mandatory for every completed task.
|
| 155 |
|
| 156 |
Focus on being precise, practical, and thorough in your coding assistance.
|
| 157 |
""",
|
|
|
|
| 221 |
content: str
|
| 222 |
|
| 223 |
|
| 224 |
+
class FrontendContext(BaseModel):
|
| 225 |
+
"""Dynamic context from the frontend that can affect system prompts"""
|
| 226 |
+
theme: Optional[Dict] = None # Current theme colors {name, accent, bg, etc.}
|
| 227 |
+
open_notebooks: Optional[List[str]] = None # List of open notebook types/names
|
| 228 |
+
|
| 229 |
+
|
| 230 |
class ChatRequest(BaseModel):
|
| 231 |
messages: List[Message]
|
| 232 |
notebook_type: str = "command"
|
|
|
|
| 240 |
research_parallel_workers: Optional[int] = None # Number of parallel workers for research
|
| 241 |
research_max_websites: Optional[int] = None # Max websites to analyze per research session
|
| 242 |
notebook_id: Optional[str] = None # Unique notebook/tab ID for session management
|
| 243 |
+
frontend_context: Optional[FrontendContext] = None # Dynamic context from frontend
|
| 244 |
|
| 245 |
|
| 246 |
class TitleRequest(BaseModel):
|
|
|
|
| 266 |
model: str,
|
| 267 |
e2b_key: str,
|
| 268 |
session_id: str,
|
| 269 |
+
tab_id: str = "default",
|
| 270 |
+
frontend_context: Optional[Dict] = None
|
| 271 |
):
|
| 272 |
"""Handle code notebook with execution capabilities"""
|
| 273 |
|
|
|
|
| 290 |
# Create OpenAI client with user's endpoint
|
| 291 |
client = OpenAI(base_url=endpoint, api_key=token)
|
| 292 |
|
| 293 |
+
# Add system prompt for code notebook (with file tree and styling context)
|
| 294 |
+
system_prompt = get_system_prompt("code", frontend_context)
|
| 295 |
full_messages = [
|
| 296 |
{"role": "system", "content": system_prompt}
|
| 297 |
] + messages
|
|
|
|
| 300 |
record_api_call(tab_id, full_messages)
|
| 301 |
|
| 302 |
# Stream code execution
|
| 303 |
+
for update in stream_code_execution(client, model, full_messages, sbx, files_root=FILES_ROOT):
|
| 304 |
# Forward updates to frontend
|
| 305 |
yield f"data: {json.dumps(update)}\n\n"
|
| 306 |
|
|
|
|
| 332 |
yield f"data: {json.dumps({'type': 'info', 'content': 'New sandbox created. Retrying execution...'})}\n\n"
|
| 333 |
|
| 334 |
# Retry code execution with new sandbox
|
| 335 |
+
for update in stream_code_execution(client, model, full_messages, sbx, files_root=FILES_ROOT):
|
| 336 |
yield f"data: {json.dumps(update)}\n\n"
|
| 337 |
|
| 338 |
except Exception as retry_error:
|
|
|
|
| 373 |
# Create OpenAI client
|
| 374 |
client = OpenAI(base_url=endpoint, api_key=token)
|
| 375 |
|
| 376 |
+
# Get system prompt for research (with file tree)
|
| 377 |
+
system_prompt = get_system_prompt("research")
|
| 378 |
|
| 379 |
# Store for debugging (simplified version for research)
|
| 380 |
full_messages = [{"role": "system", "content": system_prompt}] + messages
|
|
|
|
| 425 |
if tab_id not in CONVERSATION_HISTORY:
|
| 426 |
CONVERSATION_HISTORY[tab_id] = []
|
| 427 |
|
| 428 |
+
# Add system prompt for command center (with file tree)
|
| 429 |
+
system_prompt = get_system_prompt("command")
|
| 430 |
|
| 431 |
# Build full messages: system + stored history + new messages
|
| 432 |
print(f"DEBUG: tab_id={tab_id}, incoming messages={messages}")
|
|
|
|
| 484 |
print(f"Messages: {len(messages)} messages")
|
| 485 |
print(f"Token provided: {bool(token)}")
|
| 486 |
|
| 487 |
+
# Prepare messages with appropriate system prompt based on notebook type (with file tree)
|
| 488 |
+
system_prompt = get_system_prompt(notebook_type)
|
| 489 |
full_messages = [
|
| 490 |
{"role": "system", "content": system_prompt}
|
| 491 |
] + messages
|
|
|
|
| 649 |
# Get tab_id for debugging
|
| 650 |
tab_id = request.notebook_id or "0"
|
| 651 |
|
| 652 |
+
# Convert frontend_context to dict if provided
|
| 653 |
+
frontend_context = request.frontend_context.model_dump() if request.frontend_context else None
|
| 654 |
+
|
| 655 |
# Route to code execution handler for code notebooks
|
| 656 |
if request.notebook_type == "code":
|
| 657 |
# Use notebook_id as session key, fallback to "default" if not provided
|
|
|
|
| 665 |
request.model or "gpt-4",
|
| 666 |
request.e2b_key or "",
|
| 667 |
session_id,
|
| 668 |
+
tab_id,
|
| 669 |
+
frontend_context
|
| 670 |
),
|
| 671 |
media_type="text/event-stream",
|
| 672 |
headers={
|
|
|
|
| 839 |
# These can be overridden via command-line arguments or set_*_file functions
|
| 840 |
SETTINGS_FILE = os.path.join(PROJECT_ROOT, "settings.json")
|
| 841 |
WORKSPACE_FILE = os.path.join(PROJECT_ROOT, "workspace.json")
|
| 842 |
+
FILES_ROOT = PROJECT_ROOT # Root directory for file tree
|
| 843 |
+
|
| 844 |
+
# Directories/patterns to exclude from file tree
|
| 845 |
+
FILES_EXCLUDE = {
|
| 846 |
+
'node_modules', '__pycache__', '.git', '.pytest_cache',
|
| 847 |
+
'env', 'venv', 'env312', '.venv', 'dist', 'build',
|
| 848 |
+
'.egg-info', '.tox', '.coverage', 'htmlcov',
|
| 849 |
+
'test-results', 'playwright-report'
|
| 850 |
+
}
|
| 851 |
|
| 852 |
def set_settings_file(path: str):
|
| 853 |
"""Set the settings file path (used for testing)"""
|
|
|
|
| 861 |
|
| 862 |
def set_data_dir(directory: str):
|
| 863 |
"""Set the data directory containing settings.json and workspace.json"""
|
| 864 |
+
global SETTINGS_FILE, WORKSPACE_FILE, FILES_ROOT
|
| 865 |
os.makedirs(directory, exist_ok=True)
|
| 866 |
SETTINGS_FILE = os.path.join(directory, "settings.json")
|
| 867 |
WORKSPACE_FILE = os.path.join(directory, "workspace.json")
|
| 868 |
+
FILES_ROOT = directory
|
| 869 |
|
| 870 |
|
| 871 |
@app.get("/api/settings")
|
|
|
|
| 963 |
raise HTTPException(status_code=500, detail=f"Failed to clear workspace: {str(e)}")
|
| 964 |
|
| 965 |
|
| 966 |
+
# ============================================
|
| 967 |
+
# File Tree API
|
| 968 |
+
# ============================================
|
| 969 |
+
|
| 970 |
+
def build_file_tree(root_path: str, show_hidden: bool = False) -> list:
|
| 971 |
+
"""Build a file tree structure from a directory"""
|
| 972 |
+
tree = []
|
| 973 |
+
|
| 974 |
+
try:
|
| 975 |
+
entries = sorted(os.listdir(root_path))
|
| 976 |
+
except PermissionError:
|
| 977 |
+
return tree
|
| 978 |
+
|
| 979 |
+
for entry in entries:
|
| 980 |
+
# Skip hidden files unless show_hidden is True
|
| 981 |
+
if entry.startswith('.') and not show_hidden:
|
| 982 |
+
continue
|
| 983 |
+
|
| 984 |
+
# Skip excluded directories
|
| 985 |
+
if entry in FILES_EXCLUDE:
|
| 986 |
+
continue
|
| 987 |
+
|
| 988 |
+
full_path = os.path.join(root_path, entry)
|
| 989 |
+
rel_path = os.path.relpath(full_path, FILES_ROOT)
|
| 990 |
+
|
| 991 |
+
if os.path.isdir(full_path):
|
| 992 |
+
children = build_file_tree(full_path, show_hidden)
|
| 993 |
+
tree.append({
|
| 994 |
+
"name": entry,
|
| 995 |
+
"type": "folder",
|
| 996 |
+
"path": rel_path,
|
| 997 |
+
"children": children
|
| 998 |
+
})
|
| 999 |
+
else:
|
| 1000 |
+
tree.append({
|
| 1001 |
+
"name": entry,
|
| 1002 |
+
"type": "file",
|
| 1003 |
+
"path": rel_path
|
| 1004 |
+
})
|
| 1005 |
+
|
| 1006 |
+
return tree
|
| 1007 |
+
|
| 1008 |
+
|
| 1009 |
+
def format_file_tree_text(tree: list, prefix: str = "", is_last: bool = True) -> str:
|
| 1010 |
+
"""Format file tree as text for system prompts"""
|
| 1011 |
+
lines = []
|
| 1012 |
+
|
| 1013 |
+
for i, item in enumerate(tree):
|
| 1014 |
+
is_last_item = (i == len(tree) - 1)
|
| 1015 |
+
connector = "└── " if is_last_item else "├── "
|
| 1016 |
+
lines.append(f"{prefix}{connector}{item['name']}{'/' if item['type'] == 'folder' else ''}")
|
| 1017 |
+
|
| 1018 |
+
if item['type'] == 'folder' and item.get('children'):
|
| 1019 |
+
extension = " " if is_last_item else "│ "
|
| 1020 |
+
child_text = format_file_tree_text(item['children'], prefix + extension, is_last_item)
|
| 1021 |
+
if child_text:
|
| 1022 |
+
lines.append(child_text)
|
| 1023 |
+
|
| 1024 |
+
return "\n".join(lines)
|
| 1025 |
+
|
| 1026 |
+
|
| 1027 |
+
def get_file_tree_for_prompt() -> str:
|
| 1028 |
+
"""Get formatted file tree text for inclusion in system prompts"""
|
| 1029 |
+
tree = build_file_tree(FILES_ROOT, show_hidden=False)
|
| 1030 |
+
tree_text = format_file_tree_text(tree)
|
| 1031 |
+
return f"Working Directory: {FILES_ROOT}\n{tree_text}"
|
| 1032 |
+
|
| 1033 |
+
|
| 1034 |
+
def get_styling_context(theme: Optional[Dict] = None) -> str:
|
| 1035 |
+
"""Generate styling guidance for code notebooks based on current theme"""
|
| 1036 |
+
# App style description
|
| 1037 |
+
style_desc = """## Visual Style Guidelines
|
| 1038 |
+
The application has a minimalist, technical aesthetic with clean lines and muted colors. When generating plots or visualizations:
|
| 1039 |
+
- Use white/light backgrounds to match the notebook style
|
| 1040 |
+
- Prefer clean, simple chart styles without excessive decoration
|
| 1041 |
+
- Use the theme accent color as the primary color for data series
|
| 1042 |
+
- Use neutral grays (#666, #999, #ccc) for secondary elements, gridlines, and text
|
| 1043 |
+
- Use 300 DPI for all figures unless the user specifies otherwise (e.g., plt.figure(figsize=..., dpi=300) or plt.savefig(..., dpi=300))"""
|
| 1044 |
+
|
| 1045 |
+
if theme:
|
| 1046 |
+
accent = theme.get('accent', '#1b5e20')
|
| 1047 |
+
bg = theme.get('bg', '#e8f5e9')
|
| 1048 |
+
name = theme.get('name', 'forest')
|
| 1049 |
+
style_desc += f"""
|
| 1050 |
+
|
| 1051 |
+
Current theme: {name}
|
| 1052 |
+
- Primary/accent color: {accent} (use for main data series, highlights)
|
| 1053 |
+
- Light background: {bg} (use for fills, light accents)
|
| 1054 |
+
- Keep chart backgrounds white (#ffffff) for readability"""
|
| 1055 |
+
|
| 1056 |
+
return style_desc
|
| 1057 |
+
|
| 1058 |
+
|
| 1059 |
+
def get_system_prompt(notebook_type: str, frontend_context: Optional[Dict] = None) -> str:
|
| 1060 |
+
"""Get system prompt for a notebook type with dynamic context appended"""
|
| 1061 |
+
base_prompt = SYSTEM_PROMPTS.get(notebook_type, SYSTEM_PROMPTS["command"])
|
| 1062 |
+
file_tree = get_file_tree_for_prompt()
|
| 1063 |
+
|
| 1064 |
+
# Build the full prompt with context sections
|
| 1065 |
+
sections = [base_prompt, f"## Project Files\n{file_tree}"]
|
| 1066 |
+
|
| 1067 |
+
# Add styling context for code notebooks
|
| 1068 |
+
if notebook_type == "code" and frontend_context:
|
| 1069 |
+
theme = frontend_context.get('theme') if frontend_context else None
|
| 1070 |
+
styling = get_styling_context(theme)
|
| 1071 |
+
sections.append(styling)
|
| 1072 |
+
|
| 1073 |
+
return "\n\n".join(sections)
|
| 1074 |
+
|
| 1075 |
+
|
| 1076 |
+
@app.get("/api/files")
|
| 1077 |
+
async def get_file_tree(show_hidden: bool = False):
|
| 1078 |
+
"""Get file tree structure for the working directory"""
|
| 1079 |
+
try:
|
| 1080 |
+
tree = build_file_tree(FILES_ROOT, show_hidden)
|
| 1081 |
+
return {
|
| 1082 |
+
"root": FILES_ROOT,
|
| 1083 |
+
"tree": tree
|
| 1084 |
+
}
|
| 1085 |
+
except Exception as e:
|
| 1086 |
+
raise HTTPException(status_code=500, detail=f"Failed to read file tree: {str(e)}")
|
| 1087 |
+
|
| 1088 |
+
|
| 1089 |
# ============================================
|
| 1090 |
# Static File Serving (Frontend)
|
| 1091 |
# ============================================
|
dev/dr-tulu-integradion.md
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DR-TULU Integration Specification
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
Refactor the existing deep research backend (`research_notebook.py`) to use DR-TULU-8B as the driving model. The key architectural change is shifting from **orchestrator-driven** (our code decides when to search) to **model-driven** (DR-TULU decides when to call tools).
|
| 6 |
+
|
| 7 |
+
## Constraints
|
| 8 |
+
|
| 9 |
+
1. **Model served via hosted vLLM endpoint** (OpenAI-compatible API)
|
| 10 |
+
2. **Backend API must remain the same** - `stream_research()` yields the same event types
|
| 11 |
+
3. Keep existing tool implementations (`search_web`, `extract_content`) where possible
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## Current Architecture (research_notebook.py)
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
def stream_research(client, model, question, serper_key, max_iterations=5, ...):
|
| 19 |
+
"""
|
| 20 |
+
Current flow:
|
| 21 |
+
1. Our code calls generate_queries() to get search queries
|
| 22 |
+
2. Our code executes searches in parallel
|
| 23 |
+
3. Our code calls analyze_content() on each result
|
| 24 |
+
4. Our code calls assess_completeness() to decide if done
|
| 25 |
+
5. Our code calls generate_final_report()
|
| 26 |
+
|
| 27 |
+
Yields events: status, queries, source, query_stats, assessment, result_preview, result, done, error
|
| 28 |
+
"""
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
**Key yield event types to preserve:**
|
| 32 |
+
```python
|
| 33 |
+
{"type": "status", "message": str}
|
| 34 |
+
{"type": "queries", "queries": List[str], "iteration": int}
|
| 35 |
+
{"type": "source", "query_index": int, "query_text": str, "title": str, "url": str, "analysis": str, "finding_count": int, "is_relevant": bool, "is_error": bool, "error_message": str}
|
| 36 |
+
{"type": "query_stats", "query_index": int, "relevant_count": int, "irrelevant_count": int, "error_count": int}
|
| 37 |
+
{"type": "assessment", "sufficient": bool, "missing_aspects": List[str], "findings_count": int, "reasoning": str}
|
| 38 |
+
{"type": "result_preview", "content": str, "figures": dict}
|
| 39 |
+
{"type": "result", "content": str, "figures": dict}
|
| 40 |
+
{"type": "done"}
|
| 41 |
+
{"type": "error", "content": str}
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## DR-TULU Architecture
|
| 47 |
+
|
| 48 |
+
### System Prompt
|
| 49 |
+
|
| 50 |
+
Use this system prompt (from `unified_tool_calling_cli.yaml`):
|
| 51 |
+
|
| 52 |
+
```
|
| 53 |
+
You are a research assistant who answers questions through iterative reasoning and research.
|
| 54 |
+
Your name is DR Tulu. You are a model trained by people from the University of Washington, Allen Institute for AI, Meta, MIT, and CMU for a paper called "DR Tulu: Reinforcement Learning with Evolving Rubrics for Deep Research".
|
| 55 |
+
|
| 56 |
+
## Process
|
| 57 |
+
- Use <think></think> tags to show your reasoning at any point.
|
| 58 |
+
- Use <call_tool name="...">query</call_tool> when you need information (see tools below).
|
| 59 |
+
- You can alternate between thinking and searching multiple times.
|
| 60 |
+
- Only provide <answer></answer> tags when you have enough information for a complete response.
|
| 61 |
+
- Support every non-trivial claim with retrieved evidence. Wrap the exact claim span in <cite id="ID1,ID2">...</cite>, where id are snippet IDs from searched results.
|
| 62 |
+
|
| 63 |
+
## Calling Tools (<call_tool name="...">query</call_tool>)
|
| 64 |
+
|
| 65 |
+
1. google_search
|
| 66 |
+
- Purpose: general web search.
|
| 67 |
+
- Input via: <call_tool name="google_search">your query</call_tool>
|
| 68 |
+
- Output: web search snippets.
|
| 69 |
+
|
| 70 |
+
2. browse_webpage
|
| 71 |
+
- Purpose: open a specific URL and extract readable page text.
|
| 72 |
+
- Input via: <call_tool name="browse_webpage">https://example.com/article</call_tool>
|
| 73 |
+
- Output: webpage content.
|
| 74 |
+
|
| 75 |
+
## Tool Output
|
| 76 |
+
- After you issue a tool call, we will execute it and return results wrapped in <tool_output> tags.
|
| 77 |
+
- For web search: <tool_output><snippet id=UNIQUE_ID>content</snippet>...</tool_output>
|
| 78 |
+
- For web browsing: <tool_output><webpage id=UNIQUE_ID>content</webpage></tool_output>
|
| 79 |
+
|
| 80 |
+
## Answer and Citation Format
|
| 81 |
+
- Once you collect all necessary information, generate the final answer with <answer></answer> tags.
|
| 82 |
+
- In your answer, wrap supported text in <cite id="SNIPPET_ID">...</cite> using exact IDs from returned snippets.
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### Tool Call Format (Model Output)
|
| 86 |
+
|
| 87 |
+
DR-TULU emits tool calls in this XML format:
|
| 88 |
+
|
| 89 |
+
```xml
|
| 90 |
+
<call_tool name="google_search">renewable energy trends 2024</call_tool>
|
| 91 |
+
<call_tool name="browse_webpage">https://example.com/article</call_tool>
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
### Tool Response Format (What We Inject)
|
| 95 |
+
|
| 96 |
+
After executing tools, append results in this format:
|
| 97 |
+
|
| 98 |
+
```xml
|
| 99 |
+
<tool_output>
|
| 100 |
+
<snippet id="S_abc123" url="https://example.com/page1" title="Page Title">
|
| 101 |
+
Content from the search result or webpage...
|
| 102 |
+
</snippet>
|
| 103 |
+
<snippet id="S_def456" url="https://example.com/page2" title="Another Title">
|
| 104 |
+
More content...
|
| 105 |
+
</snippet>
|
| 106 |
+
</tool_output>
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
For browse_webpage:
|
| 110 |
+
```xml
|
| 111 |
+
<tool_output>
|
| 112 |
+
<webpage id="W_xyz789" url="https://example.com/article" title="Article Title">
|
| 113 |
+
Full extracted content from the page...
|
| 114 |
+
</webpage>
|
| 115 |
+
</tool_output>
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### Other Tags
|
| 119 |
+
|
| 120 |
+
- `<think>...</think>` - Model's reasoning (can yield as status/progress)
|
| 121 |
+
- `<answer>...</answer>` - Final output (yield as result)
|
| 122 |
+
- `<cite id="S_abc123">claim text</cite>` - Citations in the answer
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
## New Implementation
|
| 127 |
+
|
| 128 |
+
### Core Loop
|
| 129 |
+
|
| 130 |
+
```python
|
| 131 |
+
import re
|
| 132 |
+
import uuid
|
| 133 |
+
from typing import AsyncGenerator, Dict, Any, List, Optional
|
| 134 |
+
|
| 135 |
+
def generate_snippet_id() -> str:
|
| 136 |
+
"""Generate unique snippet ID"""
|
| 137 |
+
return f"S_{uuid.uuid4().hex[:8]}"
|
| 138 |
+
|
| 139 |
+
def generate_webpage_id() -> str:
|
| 140 |
+
"""Generate unique webpage ID"""
|
| 141 |
+
return f"W_{uuid.uuid4().hex[:8]}"
|
| 142 |
+
|
| 143 |
+
def parse_tool_calls(text: str) -> List[Dict[str, Any]]:
|
| 144 |
+
"""
|
| 145 |
+
Parse <call_tool name="...">query</call_tool> from model output.
|
| 146 |
+
Returns list of {"name": str, "query": str, "params": dict}
|
| 147 |
+
"""
|
| 148 |
+
pattern = r'<call_tool\s+name="([^"]+)"([^>]*)>([^<]+)</call_tool>'
|
| 149 |
+
matches = re.findall(pattern, text)
|
| 150 |
+
|
| 151 |
+
tool_calls = []
|
| 152 |
+
for name, params_str, query in matches:
|
| 153 |
+
# Parse optional params like limit="8" year="2021-2025"
|
| 154 |
+
params = {}
|
| 155 |
+
param_pattern = r'(\w+)="([^"]+)"'
|
| 156 |
+
for param_name, param_value in re.findall(param_pattern, params_str):
|
| 157 |
+
params[param_name] = param_value
|
| 158 |
+
|
| 159 |
+
tool_calls.append({
|
| 160 |
+
"name": name.strip(),
|
| 161 |
+
"query": query.strip(),
|
| 162 |
+
"params": params
|
| 163 |
+
})
|
| 164 |
+
|
| 165 |
+
return tool_calls
|
| 166 |
+
|
| 167 |
+
def parse_think_blocks(text: str) -> List[str]:
|
| 168 |
+
"""Extract <think>...</think> content"""
|
| 169 |
+
pattern = r'<think>(.*?)</think>'
|
| 170 |
+
return re.findall(pattern, text, re.DOTALL)
|
| 171 |
+
|
| 172 |
+
def parse_answer(text: str) -> Optional[str]:
|
| 173 |
+
"""Extract <answer>...</answer> content"""
|
| 174 |
+
pattern = r'<answer>(.*?)</answer>'
|
| 175 |
+
match = re.search(pattern, text, re.DOTALL)
|
| 176 |
+
return match.group(1).strip() if match else None
|
| 177 |
+
|
| 178 |
+
def format_search_results(results: List[Dict], query: str) -> str:
|
| 179 |
+
"""
|
| 180 |
+
Format search results as DR-TULU tool output.
|
| 181 |
+
|
| 182 |
+
Input results format (from existing search_web):
|
| 183 |
+
[{"title": str, "url": str, "snippet": str}, ...]
|
| 184 |
+
|
| 185 |
+
Output: <tool_output><snippet id="...">...</snippet></tool_output>
|
| 186 |
+
"""
|
| 187 |
+
if not results:
|
| 188 |
+
return "<tool_output>No results found.</tool_output>"
|
| 189 |
+
|
| 190 |
+
snippets = []
|
| 191 |
+
for r in results:
|
| 192 |
+
snippet_id = generate_snippet_id()
|
| 193 |
+
snippets.append(
|
| 194 |
+
f'<snippet id="{snippet_id}" url="{r["url"]}" title="{r["title"]}">\n'
|
| 195 |
+
f'{r["snippet"]}\n'
|
| 196 |
+
f'</snippet>'
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
return f"<tool_output>\n" + "\n".join(snippets) + "\n</tool_output>"
|
| 200 |
+
|
| 201 |
+
def format_webpage_content(url: str, title: str, content: str) -> str:
|
| 202 |
+
"""
|
| 203 |
+
Format extracted webpage as DR-TULU tool output.
|
| 204 |
+
"""
|
| 205 |
+
if not content:
|
| 206 |
+
return f"<tool_output>Could not extract content from {url}</tool_output>"
|
| 207 |
+
|
| 208 |
+
webpage_id = generate_webpage_id()
|
| 209 |
+
# Truncate very long content
|
| 210 |
+
if len(content) > 8000:
|
| 211 |
+
content = content[:8000] + "\n[Content truncated...]"
|
| 212 |
+
|
| 213 |
+
return (
|
| 214 |
+
f"<tool_output>\n"
|
| 215 |
+
f'<webpage id="{webpage_id}" url="{url}" title="{title}">\n'
|
| 216 |
+
f'{content}\n'
|
| 217 |
+
f'</webpage>\n'
|
| 218 |
+
f"</tool_output>"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
async def execute_tool(
|
| 222 |
+
tool_name: str,
|
| 223 |
+
query: str,
|
| 224 |
+
params: dict,
|
| 225 |
+
serper_key: str
|
| 226 |
+
) -> tuple[str, List[Dict]]:
|
| 227 |
+
"""
|
| 228 |
+
Execute a tool and return (formatted_output, raw_results).
|
| 229 |
+
|
| 230 |
+
Uses existing search_web() and extract_content() functions.
|
| 231 |
+
"""
|
| 232 |
+
if tool_name == "google_search":
|
| 233 |
+
# Use existing search_web function
|
| 234 |
+
results = search_web(query, serper_key, num_results=params.get("limit", 10))
|
| 235 |
+
formatted = format_search_results(results, query)
|
| 236 |
+
return formatted, results
|
| 237 |
+
|
| 238 |
+
elif tool_name == "browse_webpage":
|
| 239 |
+
# query is the URL for browse_webpage
|
| 240 |
+
url = query
|
| 241 |
+
content = extract_content(url)
|
| 242 |
+
title = url # Could extract from content if needed
|
| 243 |
+
formatted = format_webpage_content(url, title, content or "")
|
| 244 |
+
return formatted, [{"url": url, "content": content}]
|
| 245 |
+
|
| 246 |
+
else:
|
| 247 |
+
return f"<tool_output>Unknown tool: {tool_name}</tool_output>", []
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def stream_research(
|
| 251 |
+
client,
|
| 252 |
+
model: str,
|
| 253 |
+
question: str,
|
| 254 |
+
serper_key: str,
|
| 255 |
+
max_tool_calls: int = 20,
|
| 256 |
+
system_prompt: str = "",
|
| 257 |
+
**kwargs # Accept other params for backwards compat
|
| 258 |
+
):
|
| 259 |
+
"""
|
| 260 |
+
Stream deep research results using DR-TULU.
|
| 261 |
+
|
| 262 |
+
The model drives the research loop - it decides when to search,
|
| 263 |
+
what to search for, and when it has enough information to answer.
|
| 264 |
+
|
| 265 |
+
Yields same event types as before for API compatibility.
|
| 266 |
+
"""
|
| 267 |
+
|
| 268 |
+
# Build system prompt
|
| 269 |
+
dr_tulu_system = get_dr_tulu_system_prompt() # See above
|
| 270 |
+
if system_prompt:
|
| 271 |
+
dr_tulu_system += f"\n\n{system_prompt}"
|
| 272 |
+
|
| 273 |
+
messages = [
|
| 274 |
+
{"role": "system", "content": dr_tulu_system},
|
| 275 |
+
{"role": "user", "content": question}
|
| 276 |
+
]
|
| 277 |
+
|
| 278 |
+
yield {"type": "status", "message": f"Starting research: {question}"}
|
| 279 |
+
|
| 280 |
+
tool_call_count = 0
|
| 281 |
+
findings = [] # Track sources for compatibility
|
| 282 |
+
all_queries = [] # Track queries for compatibility
|
| 283 |
+
|
| 284 |
+
while tool_call_count < max_tool_calls:
|
| 285 |
+
# Call DR-TULU
|
| 286 |
+
yield {"type": "status", "message": "Thinking..."}
|
| 287 |
+
|
| 288 |
+
try:
|
| 289 |
+
response = client.chat.completions.create(
|
| 290 |
+
model=model,
|
| 291 |
+
messages=messages,
|
| 292 |
+
max_tokens=4096,
|
| 293 |
+
temperature=0.7,
|
| 294 |
+
)
|
| 295 |
+
assistant_message = response.choices[0].message.content
|
| 296 |
+
except Exception as e:
|
| 297 |
+
yield {"type": "error", "content": f"Model error: {str(e)}"}
|
| 298 |
+
yield {"type": "done"}
|
| 299 |
+
return
|
| 300 |
+
|
| 301 |
+
# Parse thinking blocks and yield as status
|
| 302 |
+
think_blocks = parse_think_blocks(assistant_message)
|
| 303 |
+
for thought in think_blocks:
|
| 304 |
+
yield {"type": "status", "message": f"Reasoning: {thought[:200]}..."}
|
| 305 |
+
|
| 306 |
+
# Check for final answer
|
| 307 |
+
answer = parse_answer(assistant_message)
|
| 308 |
+
if answer:
|
| 309 |
+
yield {"type": "status", "message": "Research complete! Generating report..."}
|
| 310 |
+
yield {"type": "result_preview", "content": answer, "figures": {}}
|
| 311 |
+
yield {"type": "result", "content": answer, "figures": {}}
|
| 312 |
+
yield {"type": "done"}
|
| 313 |
+
return
|
| 314 |
+
|
| 315 |
+
# Parse and execute tool calls
|
| 316 |
+
tool_calls = parse_tool_calls(assistant_message)
|
| 317 |
+
|
| 318 |
+
if not tool_calls:
|
| 319 |
+
# No tool calls and no answer - model might be stuck
|
| 320 |
+
# Append message and continue to prompt for more
|
| 321 |
+
messages.append({"role": "assistant", "content": assistant_message})
|
| 322 |
+
messages.append({"role": "user", "content": "Please continue your research or provide your answer."})
|
| 323 |
+
continue
|
| 324 |
+
|
| 325 |
+
# Track queries for compatibility
|
| 326 |
+
new_queries = [tc["query"] for tc in tool_calls if tc["name"] == "google_search"]
|
| 327 |
+
if new_queries:
|
| 328 |
+
all_queries.extend(new_queries)
|
| 329 |
+
yield {
|
| 330 |
+
"type": "queries",
|
| 331 |
+
"queries": new_queries,
|
| 332 |
+
"iteration": len(all_queries) // 5 + 1 # Rough iteration count
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
# Execute tools and collect results
|
| 336 |
+
tool_outputs = []
|
| 337 |
+
|
| 338 |
+
for i, tc in enumerate(tool_calls):
|
| 339 |
+
tool_call_count += 1
|
| 340 |
+
|
| 341 |
+
yield {
|
| 342 |
+
"type": "status",
|
| 343 |
+
"message": f"Searching: {tc['query'][:50]}..." if tc["name"] != "browse_webpage" else f"Browsing: {tc['query'][:50]}..."
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
formatted_output, raw_results = execute_tool(
|
| 347 |
+
tc["name"],
|
| 348 |
+
tc["query"],
|
| 349 |
+
tc["params"],
|
| 350 |
+
serper_key
|
| 351 |
+
)
|
| 352 |
+
tool_outputs.append(formatted_output)
|
| 353 |
+
|
| 354 |
+
# Yield source events for compatibility
|
| 355 |
+
if tc["name"] == "google_search":
|
| 356 |
+
for j, result in enumerate(raw_results):
|
| 357 |
+
findings.append({
|
| 358 |
+
"source": result.get("url", ""),
|
| 359 |
+
"title": result.get("title", ""),
|
| 360 |
+
"analysis": result.get("snippet", "")
|
| 361 |
+
})
|
| 362 |
+
yield {
|
| 363 |
+
"type": "source",
|
| 364 |
+
"query_index": i,
|
| 365 |
+
"query_text": tc["query"],
|
| 366 |
+
"title": result.get("title", ""),
|
| 367 |
+
"url": result.get("url", ""),
|
| 368 |
+
"analysis": result.get("snippet", ""),
|
| 369 |
+
"finding_count": len(findings),
|
| 370 |
+
"is_relevant": True, # DR-TULU decides relevance
|
| 371 |
+
"is_error": False,
|
| 372 |
+
"error_message": ""
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
elif tc["name"] == "browse_webpage":
|
| 376 |
+
for result in raw_results:
|
| 377 |
+
content = result.get("content", "")
|
| 378 |
+
is_error = not content
|
| 379 |
+
findings.append({
|
| 380 |
+
"source": tc["query"],
|
| 381 |
+
"title": tc["query"],
|
| 382 |
+
"analysis": content[:500] if content else "Failed to extract"
|
| 383 |
+
})
|
| 384 |
+
yield {
|
| 385 |
+
"type": "source",
|
| 386 |
+
"query_index": i,
|
| 387 |
+
"query_text": tc["query"],
|
| 388 |
+
"title": tc["query"],
|
| 389 |
+
"url": tc["query"],
|
| 390 |
+
"analysis": content[:500] if content else "Failed to extract content",
|
| 391 |
+
"finding_count": len(findings),
|
| 392 |
+
"is_relevant": not is_error,
|
| 393 |
+
"is_error": is_error,
|
| 394 |
+
"error_message": "Content extraction failed" if is_error else ""
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
if tool_call_count >= max_tool_calls:
|
| 398 |
+
break
|
| 399 |
+
|
| 400 |
+
# Append assistant message and tool results to conversation
|
| 401 |
+
messages.append({"role": "assistant", "content": assistant_message})
|
| 402 |
+
|
| 403 |
+
# Combine all tool outputs into one user message
|
| 404 |
+
combined_output = "\n\n".join(tool_outputs)
|
| 405 |
+
messages.append({"role": "user", "content": combined_output})
|
| 406 |
+
|
| 407 |
+
# Max tool calls reached - ask for final answer
|
| 408 |
+
messages.append({
|
| 409 |
+
"role": "user",
|
| 410 |
+
"content": "You have reached the maximum number of tool calls. Please provide your final answer now based on the information gathered."
|
| 411 |
+
})
|
| 412 |
+
|
| 413 |
+
try:
|
| 414 |
+
response = client.chat.completions.create(
|
| 415 |
+
model=model,
|
| 416 |
+
messages=messages,
|
| 417 |
+
max_tokens=4096,
|
| 418 |
+
temperature=0.7,
|
| 419 |
+
)
|
| 420 |
+
final_message = response.choices[0].message.content
|
| 421 |
+
answer = parse_answer(final_message) or final_message
|
| 422 |
+
|
| 423 |
+
yield {"type": "result_preview", "content": answer, "figures": {}}
|
| 424 |
+
yield {"type": "result", "content": answer, "figures": {}}
|
| 425 |
+
except Exception as e:
|
| 426 |
+
yield {"type": "error", "content": f"Failed to generate final answer: {str(e)}"}
|
| 427 |
+
|
| 428 |
+
yield {"type": "done"}
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def get_dr_tulu_system_prompt() -> str:
|
| 432 |
+
"""Return the DR-TULU system prompt"""
|
| 433 |
+
return '''You are a research assistant who answers questions through iterative reasoning and research.
|
| 434 |
+
|
| 435 |
+
## Process
|
| 436 |
+
- Use <think></think> tags to show your reasoning at any point.
|
| 437 |
+
- Use <call_tool name="...">query</call_tool> when you need information (see tools below).
|
| 438 |
+
- You can alternate between thinking and searching multiple times.
|
| 439 |
+
- Only provide <answer></answer> tags when you have enough information for a complete response.
|
| 440 |
+
- Support every non-trivial claim with retrieved evidence. Wrap the exact claim span in <cite id="ID1,ID2">...</cite>, where id are snippet IDs from searched results.
|
| 441 |
+
|
| 442 |
+
## Calling Tools (<call_tool name="...">query</call_tool>)
|
| 443 |
+
|
| 444 |
+
1. google_search
|
| 445 |
+
- Purpose: general web search.
|
| 446 |
+
- Input via: <call_tool name="google_search">your query</call_tool>
|
| 447 |
+
- Output: web search snippets.
|
| 448 |
+
|
| 449 |
+
2. browse_webpage
|
| 450 |
+
- Purpose: open a specific URL and extract readable page text.
|
| 451 |
+
- Input via: <call_tool name="browse_webpage">https://example.com/article</call_tool>
|
| 452 |
+
- Output: webpage content.
|
| 453 |
+
|
| 454 |
+
## Tool Output
|
| 455 |
+
- After you issue a tool call, we will execute it and return results wrapped in <tool_output> tags.
|
| 456 |
+
- For web search: <tool_output><snippet id=UNIQUE_ID url="..." title="...">content</snippet>...</tool_output>
|
| 457 |
+
- For web browsing: <tool_output><webpage id=UNIQUE_ID url="..." title="...">content</webpage></tool_output>
|
| 458 |
+
|
| 459 |
+
## Answer and Citation Format
|
| 460 |
+
- Once you collect all necessary information, generate the final answer with <answer></answer> tags.
|
| 461 |
+
- In your answer, wrap supported text in <cite id="SNIPPET_ID">...</cite> using exact IDs from returned snippets.
|
| 462 |
+
- Write comprehensive, well-structured answers with clear sections when appropriate.
|
| 463 |
+
'''
|
| 464 |
+
```
|
| 465 |
+
|
| 466 |
+
---
|
| 467 |
+
|
| 468 |
+
## Migration Checklist
|
| 469 |
+
|
| 470 |
+
### Keep From Original
|
| 471 |
+
- [x] `search_web()` function - works as-is
|
| 472 |
+
- [x] `extract_content()` function - works as-is
|
| 473 |
+
- [x] Event yield interface - same types
|
| 474 |
+
- [x] Function signature of `stream_research()` - mostly same params
|
| 475 |
+
|
| 476 |
+
### Remove/Replace
|
| 477 |
+
- [ ] `generate_queries()` - DR-TULU generates its own
|
| 478 |
+
- [ ] `analyze_content()` - DR-TULU does this internally
|
| 479 |
+
- [ ] `assess_completeness()` - DR-TULU decides when done
|
| 480 |
+
- [ ] `generate_final_report()` - DR-TULU generates answer directly
|
| 481 |
+
- [ ] Iteration loop logic - replaced by model-driven loop
|
| 482 |
+
- [ ] ThreadPoolExecutor parallelism - can keep for tool execution, but loop is sequential
|
| 483 |
+
|
| 484 |
+
### New Components
|
| 485 |
+
- [ ] `parse_tool_calls()` - parse DR-TULU XML output
|
| 486 |
+
- [ ] `parse_think_blocks()` - extract reasoning
|
| 487 |
+
- [ ] `parse_answer()` - extract final answer
|
| 488 |
+
- [ ] `format_search_results()` - format for DR-TULU
|
| 489 |
+
- [ ] `format_webpage_content()` - format for DR-TULU
|
| 490 |
+
- [ ] `execute_tool()` - unified tool execution
|
| 491 |
+
- [ ] `get_dr_tulu_system_prompt()` - system prompt
|
| 492 |
+
|
| 493 |
+
---
|
| 494 |
+
|
| 495 |
+
## Testing
|
| 496 |
+
|
| 497 |
+
### Basic Test
|
| 498 |
+
```python
|
| 499 |
+
from openai import OpenAI
|
| 500 |
+
|
| 501 |
+
client = OpenAI(
|
| 502 |
+
base_url="https://your-vllm-endpoint/v1",
|
| 503 |
+
api_key="your-key"
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
for event in stream_research(
|
| 507 |
+
client=client,
|
| 508 |
+
model="rl-research/DR-Tulu-8B",
|
| 509 |
+
question="What are the latest developments in quantum computing?",
|
| 510 |
+
serper_key="your-serper-key",
|
| 511 |
+
max_tool_calls=10
|
| 512 |
+
):
|
| 513 |
+
print(event["type"], event.get("message", event.get("content", ""))[:100])
|
| 514 |
+
```
|
| 515 |
+
|
| 516 |
+
### Verify Event Compatibility
|
| 517 |
+
Ensure the UI receives the same event types it expects:
|
| 518 |
+
- `status` with `message`
|
| 519 |
+
- `queries` with `queries` list
|
| 520 |
+
- `source` with url, title, analysis, etc.
|
| 521 |
+
- `result` with `content`
|
| 522 |
+
- `done`
|
| 523 |
+
|
| 524 |
+
---
|
| 525 |
+
|
| 526 |
+
## Optional Enhancements
|
| 527 |
+
|
| 528 |
+
### 1. Async Version
|
| 529 |
+
Convert to async for better performance:
|
| 530 |
+
```python
|
| 531 |
+
async def stream_research_async(...):
|
| 532 |
+
async for event in ...:
|
| 533 |
+
yield event
|
| 534 |
+
```
|
| 535 |
+
|
| 536 |
+
### 2. Parallel Tool Execution
|
| 537 |
+
When DR-TULU emits multiple tool calls, execute them in parallel:
|
| 538 |
+
```python
|
| 539 |
+
import asyncio
|
| 540 |
+
|
| 541 |
+
async def execute_tools_parallel(tool_calls, serper_key):
|
| 542 |
+
tasks = [execute_tool(tc["name"], tc["query"], tc["params"], serper_key) for tc in tool_calls]
|
| 543 |
+
return await asyncio.gather(*tasks)
|
| 544 |
+
```
|
| 545 |
+
|
| 546 |
+
### 3. Streaming Model Output
|
| 547 |
+
If vLLM supports streaming, yield thinking in real-time:
|
| 548 |
+
```python
|
| 549 |
+
for chunk in client.chat.completions.create(..., stream=True):
|
| 550 |
+
# Parse partial output for <think> tags
|
| 551 |
+
# Yield status updates as thinking happens
|
| 552 |
+
```
|
| 553 |
+
|
| 554 |
+
### 4. Add snippet_search Tool
|
| 555 |
+
If you want scientific paper search (Semantic Scholar):
|
| 556 |
+
```python
|
| 557 |
+
def search_papers(query: str, s2_api_key: str, limit: int = 5, year: str = None) -> List[Dict]:
|
| 558 |
+
# Implement Semantic Scholar API call
|
| 559 |
+
pass
|
| 560 |
+
```
|
| 561 |
+
|
| 562 |
+
---
|
| 563 |
+
|
| 564 |
+
## Model Configuration
|
| 565 |
+
|
| 566 |
+
### vLLM Serving
|
| 567 |
+
```bash
|
| 568 |
+
vllm serve rl-research/DR-Tulu-8B \
|
| 569 |
+
--dtype auto \
|
| 570 |
+
--port 8000 \
|
| 571 |
+
--max-model-len 40960
|
| 572 |
+
```
|
| 573 |
+
|
| 574 |
+
### Client Configuration
|
| 575 |
+
```python
|
| 576 |
+
client = OpenAI(
|
| 577 |
+
base_url="https://your-endpoint/v1",
|
| 578 |
+
api_key="your-api-key" # or "EMPTY" for local vLLM
|
| 579 |
+
)
|
| 580 |
+
```
|
| 581 |
+
|
| 582 |
+
### Recommended Parameters
|
| 583 |
+
- `max_tokens`: 4096 (for reasoning + tool calls)
|
| 584 |
+
- `temperature`: 0.7 (as used in DR-TULU training)
|
| 585 |
+
- `max_tool_calls`: 10-20 (DR-TULU averages ~4.3 per query)
|
features.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
Features
|
| 2 |
-
- [ ] maybe rename the whole thing to "taskbook"
|
| 3 |
- [ ] Add working files system tree of working directory
|
| 4 |
- should be in sytem prompt
|
| 5 |
- should have a visual widget (e.g. next to settings/debug) or always open on the left
|
|
|
|
| 6 |
- drag and drop elements into chat which copies the path
|
| 7 |
- [ ] include the color scheme in the system prompt of the models (e.g. when they make plots)
|
|
|
|
| 8 |
- [ ] add AI2 DeepResearch model as the research model
|
| 9 |
- [ ] rename the chat notebook to "base"
|
| 10 |
- [ ] add general code/websearch to the base task
|
|
|
|
| 1 |
Features
|
|
|
|
| 2 |
- [ ] Add working files system tree of working directory
|
| 3 |
- should be in sytem prompt
|
| 4 |
- should have a visual widget (e.g. next to settings/debug) or always open on the left
|
| 5 |
+
- widget could look like a the deep research one a bit
|
| 6 |
- drag and drop elements into chat which copies the path
|
| 7 |
- [ ] include the color scheme in the system prompt of the models (e.g. when they make plots)
|
| 8 |
+
- [ ] maybe rename the whole thing to "taskbook"
|
| 9 |
- [ ] add AI2 DeepResearch model as the research model
|
| 10 |
- [ ] rename the chat notebook to "base"
|
| 11 |
- [ ] add general code/websearch to the base task
|
frontend/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
<title>Productive</title>
|
| 7 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
|
| 9 |
-
<link rel="stylesheet" href="style.css?v=
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<div class="app-container">
|
|
@@ -26,6 +26,7 @@
|
|
| 26 |
</div>
|
| 27 |
</div>
|
| 28 |
<div class="tab-bar-spacer"></div>
|
|
|
|
| 29 |
<button class="debug-btn" id="debugBtn">DEBUG</button>
|
| 30 |
<button class="settings-btn" id="settingsBtn">SETTINGS</button>
|
| 31 |
</div>
|
|
@@ -300,9 +301,27 @@
|
|
| 300 |
</div>
|
| 301 |
</div>
|
| 302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
| 304 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
| 305 |
-
<script src="research-ui.js?v=
|
| 306 |
-
<script src="script.js?v=
|
| 307 |
</body>
|
| 308 |
</html>
|
|
|
|
| 6 |
<title>Productive</title>
|
| 7 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
|
| 9 |
+
<link rel="stylesheet" href="style.css?v=15">
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<div class="app-container">
|
|
|
|
| 26 |
</div>
|
| 27 |
</div>
|
| 28 |
<div class="tab-bar-spacer"></div>
|
| 29 |
+
<button class="files-btn" id="filesBtn">FILES</button>
|
| 30 |
<button class="debug-btn" id="debugBtn">DEBUG</button>
|
| 31 |
<button class="settings-btn" id="settingsBtn">SETTINGS</button>
|
| 32 |
</div>
|
|
|
|
| 301 |
</div>
|
| 302 |
</div>
|
| 303 |
|
| 304 |
+
<!-- Files Panel -->
|
| 305 |
+
<div class="files-panel" id="filesPanel">
|
| 306 |
+
<div class="files-panel-header">
|
| 307 |
+
<h3>FILES</h3>
|
| 308 |
+
<div class="files-panel-controls">
|
| 309 |
+
<label class="files-show-hidden">
|
| 310 |
+
<input type="checkbox" id="showHiddenFiles">
|
| 311 |
+
<span>Hidden</span>
|
| 312 |
+
</label>
|
| 313 |
+
<button class="files-refresh-btn" id="filesRefresh" title="Refresh">↻</button>
|
| 314 |
+
</div>
|
| 315 |
+
<button class="files-panel-close" id="filesPanelClose">×</button>
|
| 316 |
+
</div>
|
| 317 |
+
<div class="files-panel-body" id="fileTree">
|
| 318 |
+
<div class="files-loading">Loading...</div>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
|
| 322 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
| 323 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
| 324 |
+
<script src="research-ui.js?v=15"></script>
|
| 325 |
+
<script src="script.js?v=15"></script>
|
| 326 |
</body>
|
| 327 |
</html>
|
frontend/script.js
CHANGED
|
@@ -550,7 +550,8 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
|
|
| 550 |
research_sub_agent_model: currentSettings.researchSubAgentModel || null,
|
| 551 |
research_parallel_workers: currentSettings.researchParallelWorkers || null,
|
| 552 |
research_max_websites: currentSettings.researchMaxWebsites || null,
|
| 553 |
-
notebook_id: tabId.toString() // Send unique tab ID for sandbox sessions
|
|
|
|
| 554 |
})
|
| 555 |
});
|
| 556 |
|
|
@@ -592,8 +593,20 @@ async function streamChatResponse(messages, chatContainer, notebookType, tabId)
|
|
| 592 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 593 |
|
| 594 |
} else if (data.type === 'code_start') {
|
| 595 |
-
// Code cell starting execution
|
| 596 |
-
createCodeCell(chatContainer, data.code,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
currentMessageEl = null;
|
| 598 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 599 |
|
|
@@ -788,17 +801,23 @@ function appendToMessage(messageEl, content) {
|
|
| 788 |
}
|
| 789 |
}
|
| 790 |
|
| 791 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
| 792 |
const codeCell = document.createElement('div');
|
| 793 |
codeCell.className = 'code-cell';
|
| 794 |
|
| 795 |
let outputHtml = '';
|
| 796 |
-
if (output) {
|
| 797 |
outputHtml = `<div class="code-cell-output${isError ? ' error' : ''}">${escapeHtml(output)}</div>`;
|
| 798 |
}
|
| 799 |
|
|
|
|
|
|
|
| 800 |
codeCell.innerHTML = `
|
| 801 |
-
<div class="code-cell-label">CODE</div>
|
| 802 |
<div class="code-cell-code"><pre><code class="language-python">${escapeHtml(code)}</code></pre></div>
|
| 803 |
${outputHtml}
|
| 804 |
`;
|
|
@@ -813,12 +832,61 @@ function createCodeCell(chatContainer, code, output, isError) {
|
|
| 813 |
}
|
| 814 |
}
|
| 815 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 816 |
function updateLastCodeCell(chatContainer, output, isError, images) {
|
| 817 |
const codeCells = chatContainer.querySelectorAll('.code-cell');
|
| 818 |
if (codeCells.length === 0) return;
|
| 819 |
|
| 820 |
const lastCell = codeCells[codeCells.length - 1];
|
| 821 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 822 |
// Remove existing output if any
|
| 823 |
const existingOutput = lastCell.querySelector('.code-cell-output');
|
| 824 |
if (existingOutput) {
|
|
@@ -1955,6 +2023,43 @@ function getSettings() {
|
|
| 1955 |
return settings;
|
| 1956 |
}
|
| 1957 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1958 |
// Sandbox management for code notebooks
|
| 1959 |
async function startSandbox(tabId) {
|
| 1960 |
const currentSettings = getSettings();
|
|
@@ -2085,9 +2190,11 @@ if (debugBtn) {
|
|
| 2085 |
debugBtn.addEventListener('click', () => {
|
| 2086 |
const isOpening = !debugPanel.classList.contains('active');
|
| 2087 |
|
| 2088 |
-
// Close
|
| 2089 |
settingsPanel.classList.remove('active');
|
| 2090 |
settingsBtn.classList.remove('active');
|
|
|
|
|
|
|
| 2091 |
|
| 2092 |
// Toggle debug panel
|
| 2093 |
debugPanel.classList.toggle('active');
|
|
@@ -2178,13 +2285,16 @@ const appContainer = document.querySelector('.app-container');
|
|
| 2178 |
// Open settings panel when SETTINGS button is clicked
|
| 2179 |
if (settingsBtn) {
|
| 2180 |
settingsBtn.addEventListener('click', () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2181 |
openSettings(); // Populate form fields with current values
|
| 2182 |
settingsPanel.classList.add('active');
|
| 2183 |
settingsBtn.classList.add('active');
|
| 2184 |
appContainer.classList.add('panel-open');
|
| 2185 |
-
// Close debug panel if open
|
| 2186 |
-
debugPanel.classList.remove('active');
|
| 2187 |
-
debugBtn.classList.remove('active');
|
| 2188 |
});
|
| 2189 |
}
|
| 2190 |
|
|
@@ -2197,3 +2307,210 @@ if (settingsPanelClose) {
|
|
| 2197 |
});
|
| 2198 |
}
|
| 2199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
research_sub_agent_model: currentSettings.researchSubAgentModel || null,
|
| 551 |
research_parallel_workers: currentSettings.researchParallelWorkers || null,
|
| 552 |
research_max_websites: currentSettings.researchMaxWebsites || null,
|
| 553 |
+
notebook_id: tabId.toString(), // Send unique tab ID for sandbox sessions
|
| 554 |
+
frontend_context: getFrontendContext() // Dynamic context for system prompts
|
| 555 |
})
|
| 556 |
});
|
| 557 |
|
|
|
|
| 593 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 594 |
|
| 595 |
} else if (data.type === 'code_start') {
|
| 596 |
+
// Code cell starting execution - show with spinner
|
| 597 |
+
createCodeCell(chatContainer, data.code, null, false, true);
|
| 598 |
+
currentMessageEl = null;
|
| 599 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 600 |
+
|
| 601 |
+
} else if (data.type === 'upload') {
|
| 602 |
+
// File upload notification
|
| 603 |
+
createUploadMessage(chatContainer, data.paths, data.output);
|
| 604 |
+
currentMessageEl = null;
|
| 605 |
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 606 |
+
|
| 607 |
+
} else if (data.type === 'download') {
|
| 608 |
+
// File download notification
|
| 609 |
+
createDownloadMessage(chatContainer, data.paths, data.output);
|
| 610 |
currentMessageEl = null;
|
| 611 |
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 612 |
|
|
|
|
| 801 |
}
|
| 802 |
}
|
| 803 |
|
| 804 |
+
function createSpinnerHtml() {
|
| 805 |
+
return `<div class="tool-spinner"><span></span><span></span><span></span></div>`;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
function createCodeCell(chatContainer, code, output, isError, isExecuting = false) {
|
| 809 |
const codeCell = document.createElement('div');
|
| 810 |
codeCell.className = 'code-cell';
|
| 811 |
|
| 812 |
let outputHtml = '';
|
| 813 |
+
if (output && !isExecuting) {
|
| 814 |
outputHtml = `<div class="code-cell-output${isError ? ' error' : ''}">${escapeHtml(output)}</div>`;
|
| 815 |
}
|
| 816 |
|
| 817 |
+
const spinnerHtml = isExecuting ? createSpinnerHtml() : '';
|
| 818 |
+
|
| 819 |
codeCell.innerHTML = `
|
| 820 |
+
<div class="code-cell-label"><span>CODE</span>${spinnerHtml}</div>
|
| 821 |
<div class="code-cell-code"><pre><code class="language-python">${escapeHtml(code)}</code></pre></div>
|
| 822 |
${outputHtml}
|
| 823 |
`;
|
|
|
|
| 832 |
}
|
| 833 |
}
|
| 834 |
|
| 835 |
+
function createFileTransferCell(chatContainer, type, paths, output, isExecuting = false) {
|
| 836 |
+
const cell = document.createElement('div');
|
| 837 |
+
cell.className = 'action-widget';
|
| 838 |
+
|
| 839 |
+
const label = type === 'upload' ? 'UPLOAD' : 'DOWNLOAD';
|
| 840 |
+
const hasError = output && output.includes('Error:');
|
| 841 |
+
|
| 842 |
+
// Indicator: spinner while executing, checkmark when done
|
| 843 |
+
const indicatorHtml = isExecuting
|
| 844 |
+
? `<div class="orbit-indicator"><span></span><span></span><span></span></div>`
|
| 845 |
+
: `<div class="done-indicator"></div>`;
|
| 846 |
+
|
| 847 |
+
// Format paths as list items
|
| 848 |
+
const pathsList = paths.map(p => `<li>${escapeHtml(p)}</li>`).join('');
|
| 849 |
+
|
| 850 |
+
let outputHtml = '';
|
| 851 |
+
if (output && !isExecuting) {
|
| 852 |
+
const outputClass = hasError ? 'transfer-output error' : 'transfer-output';
|
| 853 |
+
outputHtml = `<div class="${outputClass}">${escapeHtml(output)}</div>`;
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
cell.innerHTML = `
|
| 857 |
+
<div class="action-widget-header">
|
| 858 |
+
<span class="action-widget-type">${label}</span>
|
| 859 |
+
<div class="action-widget-bar-right">${indicatorHtml}</div>
|
| 860 |
+
</div>
|
| 861 |
+
<div class="action-widget-body">
|
| 862 |
+
<ul class="transfer-paths">${pathsList}</ul>
|
| 863 |
+
${outputHtml}
|
| 864 |
+
</div>
|
| 865 |
+
`;
|
| 866 |
+
chatContainer.appendChild(cell);
|
| 867 |
+
return cell;
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
function createUploadMessage(chatContainer, paths, output) {
|
| 871 |
+
createFileTransferCell(chatContainer, 'upload', paths, output, false);
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
function createDownloadMessage(chatContainer, paths, output) {
|
| 875 |
+
createFileTransferCell(chatContainer, 'download', paths, output, false);
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
function updateLastCodeCell(chatContainer, output, isError, images) {
|
| 879 |
const codeCells = chatContainer.querySelectorAll('.code-cell');
|
| 880 |
if (codeCells.length === 0) return;
|
| 881 |
|
| 882 |
const lastCell = codeCells[codeCells.length - 1];
|
| 883 |
|
| 884 |
+
// Remove spinner if present
|
| 885 |
+
const spinner = lastCell.querySelector('.tool-spinner');
|
| 886 |
+
if (spinner) {
|
| 887 |
+
spinner.remove();
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
// Remove existing output if any
|
| 891 |
const existingOutput = lastCell.querySelector('.code-cell-output');
|
| 892 |
if (existingOutput) {
|
|
|
|
| 2023 |
return settings;
|
| 2024 |
}
|
| 2025 |
|
| 2026 |
+
// Build frontend context for API requests
|
| 2027 |
+
function getFrontendContext() {
|
| 2028 |
+
const currentThemeName = settings.themeColor || 'forest';
|
| 2029 |
+
const theme = themeColors[currentThemeName];
|
| 2030 |
+
|
| 2031 |
+
return {
|
| 2032 |
+
theme: theme ? {
|
| 2033 |
+
name: currentThemeName,
|
| 2034 |
+
accent: theme.accent,
|
| 2035 |
+
bg: theme.bg,
|
| 2036 |
+
border: theme.border
|
| 2037 |
+
} : null,
|
| 2038 |
+
open_notebooks: getOpenNotebookTypes()
|
| 2039 |
+
};
|
| 2040 |
+
}
|
| 2041 |
+
|
| 2042 |
+
// Get list of open notebook types
|
| 2043 |
+
function getOpenNotebookTypes() {
|
| 2044 |
+
const tabs = document.querySelectorAll('.tab[data-tab-id]');
|
| 2045 |
+
const types = [];
|
| 2046 |
+
tabs.forEach(tab => {
|
| 2047 |
+
const tabId = tab.dataset.tabId;
|
| 2048 |
+
if (tabId === '0') {
|
| 2049 |
+
types.push('command');
|
| 2050 |
+
} else {
|
| 2051 |
+
const content = document.querySelector(`[data-content-id="${tabId}"]`);
|
| 2052 |
+
if (content) {
|
| 2053 |
+
const chatContainer = content.querySelector('.chat-container');
|
| 2054 |
+
if (chatContainer && chatContainer.dataset.notebookType) {
|
| 2055 |
+
types.push(chatContainer.dataset.notebookType);
|
| 2056 |
+
}
|
| 2057 |
+
}
|
| 2058 |
+
}
|
| 2059 |
+
});
|
| 2060 |
+
return types;
|
| 2061 |
+
}
|
| 2062 |
+
|
| 2063 |
// Sandbox management for code notebooks
|
| 2064 |
async function startSandbox(tabId) {
|
| 2065 |
const currentSettings = getSettings();
|
|
|
|
| 2190 |
debugBtn.addEventListener('click', () => {
|
| 2191 |
const isOpening = !debugPanel.classList.contains('active');
|
| 2192 |
|
| 2193 |
+
// Close other panels if open
|
| 2194 |
settingsPanel.classList.remove('active');
|
| 2195 |
settingsBtn.classList.remove('active');
|
| 2196 |
+
if (filesPanel) filesPanel.classList.remove('active');
|
| 2197 |
+
if (filesBtn) filesBtn.classList.remove('active');
|
| 2198 |
|
| 2199 |
// Toggle debug panel
|
| 2200 |
debugPanel.classList.toggle('active');
|
|
|
|
| 2285 |
// Open settings panel when SETTINGS button is clicked
|
| 2286 |
if (settingsBtn) {
|
| 2287 |
settingsBtn.addEventListener('click', () => {
|
| 2288 |
+
// Close other panels if open
|
| 2289 |
+
debugPanel.classList.remove('active');
|
| 2290 |
+
debugBtn.classList.remove('active');
|
| 2291 |
+
if (filesPanel) filesPanel.classList.remove('active');
|
| 2292 |
+
if (filesBtn) filesBtn.classList.remove('active');
|
| 2293 |
+
|
| 2294 |
openSettings(); // Populate form fields with current values
|
| 2295 |
settingsPanel.classList.add('active');
|
| 2296 |
settingsBtn.classList.add('active');
|
| 2297 |
appContainer.classList.add('panel-open');
|
|
|
|
|
|
|
|
|
|
| 2298 |
});
|
| 2299 |
}
|
| 2300 |
|
|
|
|
| 2307 |
});
|
| 2308 |
}
|
| 2309 |
|
| 2310 |
+
|
| 2311 |
+
// ============= FILES PANEL =============
|
| 2312 |
+
|
| 2313 |
+
const filesPanel = document.getElementById('filesPanel');
|
| 2314 |
+
const filesPanelClose = document.getElementById('filesPanelClose');
|
| 2315 |
+
const filesBtn = document.getElementById('filesBtn');
|
| 2316 |
+
const fileTree = document.getElementById('fileTree');
|
| 2317 |
+
const showHiddenFiles = document.getElementById('showHiddenFiles');
|
| 2318 |
+
const filesRefresh = document.getElementById('filesRefresh');
|
| 2319 |
+
|
| 2320 |
+
// Track expanded folder paths to preserve state on refresh
|
| 2321 |
+
let expandedPaths = new Set();
|
| 2322 |
+
let filesRoot = '';
|
| 2323 |
+
|
| 2324 |
+
// Load file tree from API
|
| 2325 |
+
async function loadFileTree() {
|
| 2326 |
+
const showHidden = showHiddenFiles?.checked || false;
|
| 2327 |
+
try {
|
| 2328 |
+
const response = await fetch(`/api/files?show_hidden=${showHidden}`);
|
| 2329 |
+
if (response.ok) {
|
| 2330 |
+
const data = await response.json();
|
| 2331 |
+
filesRoot = data.root;
|
| 2332 |
+
renderFileTree(data.tree, fileTree, data.root);
|
| 2333 |
+
} else {
|
| 2334 |
+
fileTree.innerHTML = '<div class="files-loading">Failed to load files</div>';
|
| 2335 |
+
}
|
| 2336 |
+
} catch (e) {
|
| 2337 |
+
console.error('Failed to load file tree:', e);
|
| 2338 |
+
fileTree.innerHTML = '<div class="files-loading">Failed to load files</div>';
|
| 2339 |
+
}
|
| 2340 |
+
}
|
| 2341 |
+
|
| 2342 |
+
// Render file tree recursively
|
| 2343 |
+
function renderFileTree(tree, container, rootPath) {
|
| 2344 |
+
container.innerHTML = '';
|
| 2345 |
+
const rootWrapper = document.createElement('div');
|
| 2346 |
+
rootWrapper.className = 'file-tree-root';
|
| 2347 |
+
|
| 2348 |
+
// Add header with folder name
|
| 2349 |
+
const header = document.createElement('div');
|
| 2350 |
+
header.className = 'file-tree-header';
|
| 2351 |
+
const folderName = rootPath.split('/').pop() || rootPath;
|
| 2352 |
+
header.textContent = './' + folderName;
|
| 2353 |
+
rootWrapper.appendChild(header);
|
| 2354 |
+
|
| 2355 |
+
// Container with vertical line
|
| 2356 |
+
const treeContainer = document.createElement('div');
|
| 2357 |
+
treeContainer.className = 'file-tree-container';
|
| 2358 |
+
renderTreeItems(tree, treeContainer);
|
| 2359 |
+
rootWrapper.appendChild(treeContainer);
|
| 2360 |
+
|
| 2361 |
+
container.appendChild(rootWrapper);
|
| 2362 |
+
}
|
| 2363 |
+
|
| 2364 |
+
function renderTreeItems(tree, container) {
|
| 2365 |
+
const len = tree.length;
|
| 2366 |
+
for (let i = 0; i < len; i++) {
|
| 2367 |
+
const item = tree[i];
|
| 2368 |
+
const isLast = (i === len - 1);
|
| 2369 |
+
|
| 2370 |
+
const itemEl = document.createElement('div');
|
| 2371 |
+
itemEl.className = `file-tree-item ${item.type}`;
|
| 2372 |
+
if (isLast) itemEl.classList.add('last');
|
| 2373 |
+
itemEl.dataset.path = item.path;
|
| 2374 |
+
|
| 2375 |
+
// Check if this folder was previously expanded
|
| 2376 |
+
const wasExpanded = expandedPaths.has(item.path);
|
| 2377 |
+
|
| 2378 |
+
// Create the clickable line element
|
| 2379 |
+
const lineEl = document.createElement('div');
|
| 2380 |
+
lineEl.className = 'file-tree-line';
|
| 2381 |
+
lineEl.draggable = true;
|
| 2382 |
+
|
| 2383 |
+
// Only folders get an icon (arrow), files get empty icon
|
| 2384 |
+
const icon = item.type === 'folder' ? (wasExpanded ? '▼' : '▶') : '';
|
| 2385 |
+
lineEl.innerHTML = `
|
| 2386 |
+
<span class="file-tree-icon">${icon}</span>
|
| 2387 |
+
<span class="file-tree-name">${item.name}</span>
|
| 2388 |
+
`;
|
| 2389 |
+
itemEl.appendChild(lineEl);
|
| 2390 |
+
|
| 2391 |
+
container.appendChild(itemEl);
|
| 2392 |
+
|
| 2393 |
+
// Handle folder expansion
|
| 2394 |
+
if (item.type === 'folder' && item.children && item.children.length > 0) {
|
| 2395 |
+
const childrenContainer = document.createElement('div');
|
| 2396 |
+
childrenContainer.className = 'file-tree-children';
|
| 2397 |
+
if (wasExpanded) {
|
| 2398 |
+
childrenContainer.classList.add('expanded');
|
| 2399 |
+
itemEl.classList.add('expanded');
|
| 2400 |
+
}
|
| 2401 |
+
renderTreeItems(item.children, childrenContainer);
|
| 2402 |
+
itemEl.appendChild(childrenContainer);
|
| 2403 |
+
|
| 2404 |
+
// Use click delay to distinguish single vs double click
|
| 2405 |
+
let clickTimer = null;
|
| 2406 |
+
lineEl.addEventListener('click', (e) => {
|
| 2407 |
+
e.stopPropagation();
|
| 2408 |
+
if (clickTimer) {
|
| 2409 |
+
// Double click detected - clear timer and expand/collapse
|
| 2410 |
+
clearTimeout(clickTimer);
|
| 2411 |
+
clickTimer = null;
|
| 2412 |
+
const isExpanded = itemEl.classList.toggle('expanded');
|
| 2413 |
+
childrenContainer.classList.toggle('expanded');
|
| 2414 |
+
const iconEl = lineEl.querySelector('.file-tree-icon');
|
| 2415 |
+
if (iconEl) iconEl.textContent = isExpanded ? '▼' : '▶';
|
| 2416 |
+
if (isExpanded) {
|
| 2417 |
+
expandedPaths.add(item.path);
|
| 2418 |
+
} else {
|
| 2419 |
+
expandedPaths.delete(item.path);
|
| 2420 |
+
}
|
| 2421 |
+
} else {
|
| 2422 |
+
// Single click - wait to see if it's a double click
|
| 2423 |
+
clickTimer = setTimeout(() => {
|
| 2424 |
+
clickTimer = null;
|
| 2425 |
+
insertPathIntoInput('./' + item.path);
|
| 2426 |
+
showClickFeedback(lineEl);
|
| 2427 |
+
}, 250);
|
| 2428 |
+
}
|
| 2429 |
+
});
|
| 2430 |
+
} else if (item.type === 'file') {
|
| 2431 |
+
// Single click on file inserts path
|
| 2432 |
+
lineEl.addEventListener('click', (e) => {
|
| 2433 |
+
e.stopPropagation();
|
| 2434 |
+
insertPathIntoInput('./' + item.path);
|
| 2435 |
+
showClickFeedback(lineEl);
|
| 2436 |
+
});
|
| 2437 |
+
}
|
| 2438 |
+
|
| 2439 |
+
// Drag start handler for future drag-and-drop
|
| 2440 |
+
lineEl.addEventListener('dragstart', (e) => {
|
| 2441 |
+
e.dataTransfer.setData('text/plain', './' + item.path);
|
| 2442 |
+
e.dataTransfer.setData('application/x-file-path', './' + item.path);
|
| 2443 |
+
e.dataTransfer.effectAllowed = 'copy';
|
| 2444 |
+
});
|
| 2445 |
+
}
|
| 2446 |
+
}
|
| 2447 |
+
|
| 2448 |
+
// Helper to insert path into active input
|
| 2449 |
+
function insertPathIntoInput(path) {
|
| 2450 |
+
const inputId = activeTabId === 0 ? 'input-command' : `input-${activeTabId}`;
|
| 2451 |
+
const inputEl = document.getElementById(inputId);
|
| 2452 |
+
if (inputEl) {
|
| 2453 |
+
const start = inputEl.selectionStart;
|
| 2454 |
+
const end = inputEl.selectionEnd;
|
| 2455 |
+
const text = inputEl.value;
|
| 2456 |
+
// Wrap path in backticks and add trailing space
|
| 2457 |
+
const formattedPath = '`' + path + '` ';
|
| 2458 |
+
inputEl.value = text.substring(0, start) + formattedPath + text.substring(end);
|
| 2459 |
+
inputEl.focus();
|
| 2460 |
+
inputEl.selectionStart = inputEl.selectionEnd = start + formattedPath.length;
|
| 2461 |
+
}
|
| 2462 |
+
}
|
| 2463 |
+
|
| 2464 |
+
// Helper to show click feedback
|
| 2465 |
+
function showClickFeedback(el) {
|
| 2466 |
+
const originalColor = el.style.color;
|
| 2467 |
+
el.style.color = 'var(--theme-accent)';
|
| 2468 |
+
setTimeout(() => {
|
| 2469 |
+
el.style.color = originalColor;
|
| 2470 |
+
}, 300);
|
| 2471 |
+
}
|
| 2472 |
+
|
| 2473 |
+
// Open files panel when FILES button is clicked
|
| 2474 |
+
if (filesBtn) {
|
| 2475 |
+
filesBtn.addEventListener('click', () => {
|
| 2476 |
+
const isOpening = !filesPanel.classList.contains('active');
|
| 2477 |
+
|
| 2478 |
+
// Close other panels first
|
| 2479 |
+
settingsPanel.classList.remove('active');
|
| 2480 |
+
settingsBtn.classList.remove('active');
|
| 2481 |
+
debugPanel.classList.remove('active');
|
| 2482 |
+
debugBtn.classList.remove('active');
|
| 2483 |
+
|
| 2484 |
+
// Toggle files panel
|
| 2485 |
+
filesPanel.classList.toggle('active');
|
| 2486 |
+
filesBtn.classList.toggle('active');
|
| 2487 |
+
|
| 2488 |
+
// Load file tree when opening
|
| 2489 |
+
if (isOpening) {
|
| 2490 |
+
loadFileTree();
|
| 2491 |
+
}
|
| 2492 |
+
});
|
| 2493 |
+
}
|
| 2494 |
+
|
| 2495 |
+
// Close files panel
|
| 2496 |
+
if (filesPanelClose) {
|
| 2497 |
+
filesPanelClose.addEventListener('click', () => {
|
| 2498 |
+
filesPanel.classList.remove('active');
|
| 2499 |
+
filesBtn.classList.remove('active');
|
| 2500 |
+
});
|
| 2501 |
+
}
|
| 2502 |
+
|
| 2503 |
+
// Refresh button
|
| 2504 |
+
if (filesRefresh) {
|
| 2505 |
+
filesRefresh.addEventListener('click', () => {
|
| 2506 |
+
loadFileTree();
|
| 2507 |
+
});
|
| 2508 |
+
}
|
| 2509 |
+
|
| 2510 |
+
// Show hidden files toggle
|
| 2511 |
+
if (showHiddenFiles) {
|
| 2512 |
+
showHiddenFiles.addEventListener('change', () => {
|
| 2513 |
+
loadFileTree();
|
| 2514 |
+
});
|
| 2515 |
+
}
|
| 2516 |
+
|
frontend/style.css
CHANGED
|
@@ -134,11 +134,13 @@ body {
|
|
| 134 |
100% { transform: rotate(360deg); }
|
| 135 |
}
|
| 136 |
|
| 137 |
-
.settings-btn
|
|
|
|
|
|
|
| 138 |
background: #f5f5f5;
|
| 139 |
color: #666;
|
| 140 |
border: none;
|
| 141 |
-
border-
|
| 142 |
padding: 8px 16px;
|
| 143 |
font-size: 12px;
|
| 144 |
font-weight: 500;
|
|
@@ -148,12 +150,16 @@ body {
|
|
| 148 |
font-family: inherit;
|
| 149 |
}
|
| 150 |
|
| 151 |
-
.settings-btn:hover
|
|
|
|
|
|
|
| 152 |
background: #eee;
|
| 153 |
color: #1a1a1a;
|
| 154 |
}
|
| 155 |
|
| 156 |
-
.settings-btn.active
|
|
|
|
|
|
|
| 157 |
background: var(--theme-accent);
|
| 158 |
color: white;
|
| 159 |
}
|
|
@@ -711,6 +717,42 @@ body {
|
|
| 711 |
border-radius: 3px;
|
| 712 |
}
|
| 713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
.input-area {
|
| 715 |
padding: 15px 20px;
|
| 716 |
background: white;
|
|
@@ -1173,6 +1215,33 @@ body {
|
|
| 1173 |
opacity: 0.85;
|
| 1174 |
}
|
| 1175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1176 |
/* Result Preview in CODE notebook */
|
| 1177 |
.result-preview {
|
| 1178 |
margin: 16px 0;
|
|
@@ -2068,29 +2137,7 @@ body {
|
|
| 2068 |
overflow-x: auto;
|
| 2069 |
}
|
| 2070 |
|
| 2071 |
-
|
| 2072 |
-
background: #f5f5f5;
|
| 2073 |
-
color: #666;
|
| 2074 |
-
border: none;
|
| 2075 |
-
border-right: 1px solid #ccc;
|
| 2076 |
-
padding: 8px 16px;
|
| 2077 |
-
font-size: 12px;
|
| 2078 |
-
font-weight: 500;
|
| 2079 |
-
letter-spacing: 1px;
|
| 2080 |
-
cursor: pointer;
|
| 2081 |
-
transition: all 0.2s;
|
| 2082 |
-
font-family: inherit;
|
| 2083 |
-
}
|
| 2084 |
-
|
| 2085 |
-
.debug-btn:hover {
|
| 2086 |
-
background: #eee;
|
| 2087 |
-
color: #1a1a1a;
|
| 2088 |
-
}
|
| 2089 |
-
|
| 2090 |
-
.debug-btn.active {
|
| 2091 |
-
background: var(--theme-accent);
|
| 2092 |
-
color: white;
|
| 2093 |
-
}
|
| 2094 |
|
| 2095 |
/* Settings Panel (side panel like debug) */
|
| 2096 |
.settings-panel {
|
|
@@ -2158,3 +2205,246 @@ body {
|
|
| 2158 |
padding: 20px;
|
| 2159 |
overflow-y: auto;
|
| 2160 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
100% { transform: rotate(360deg); }
|
| 135 |
}
|
| 136 |
|
| 137 |
+
.settings-btn,
|
| 138 |
+
.files-btn,
|
| 139 |
+
.debug-btn {
|
| 140 |
background: #f5f5f5;
|
| 141 |
color: #666;
|
| 142 |
border: none;
|
| 143 |
+
border-left: 1px solid #ccc;
|
| 144 |
padding: 8px 16px;
|
| 145 |
font-size: 12px;
|
| 146 |
font-weight: 500;
|
|
|
|
| 150 |
font-family: inherit;
|
| 151 |
}
|
| 152 |
|
| 153 |
+
.settings-btn:hover,
|
| 154 |
+
.files-btn:hover,
|
| 155 |
+
.debug-btn:hover {
|
| 156 |
background: #eee;
|
| 157 |
color: #1a1a1a;
|
| 158 |
}
|
| 159 |
|
| 160 |
+
.settings-btn.active,
|
| 161 |
+
.files-btn.active,
|
| 162 |
+
.debug-btn.active {
|
| 163 |
background: var(--theme-accent);
|
| 164 |
color: white;
|
| 165 |
}
|
|
|
|
| 717 |
border-radius: 3px;
|
| 718 |
}
|
| 719 |
|
| 720 |
+
/* Tool cell label with spinner */
|
| 721 |
+
.code-cell-label {
|
| 722 |
+
display: flex;
|
| 723 |
+
align-items: center;
|
| 724 |
+
gap: 8px;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
.code-cell-label .tool-spinner {
|
| 728 |
+
width: 12px;
|
| 729 |
+
height: 12px;
|
| 730 |
+
position: relative;
|
| 731 |
+
animation: orbit-rotate 1.2s linear infinite;
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
.code-cell-label .tool-spinner span {
|
| 735 |
+
position: absolute;
|
| 736 |
+
width: 3px;
|
| 737 |
+
height: 3px;
|
| 738 |
+
border-radius: 50%;
|
| 739 |
+
background: white;
|
| 740 |
+
top: 50%;
|
| 741 |
+
left: 50%;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
.code-cell-label .tool-spinner span:nth-child(1) {
|
| 745 |
+
transform: translate(-50%, -50%) translateY(-4.5px);
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
.code-cell-label .tool-spinner span:nth-child(2) {
|
| 749 |
+
transform: translate(-50%, -50%) rotate(120deg) translateY(-4.5px);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
.code-cell-label .tool-spinner span:nth-child(3) {
|
| 753 |
+
transform: translate(-50%, -50%) rotate(240deg) translateY(-4.5px);
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
.input-area {
|
| 757 |
padding: 15px 20px;
|
| 758 |
background: white;
|
|
|
|
| 1215 |
opacity: 0.85;
|
| 1216 |
}
|
| 1217 |
|
| 1218 |
+
/* File transfer (upload/download) widget styles */
|
| 1219 |
+
.transfer-paths {
|
| 1220 |
+
margin: 0;
|
| 1221 |
+
padding-left: 20px;
|
| 1222 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1223 |
+
font-size: 12px;
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
+
.transfer-paths li {
|
| 1227 |
+
margin: 4px 0;
|
| 1228 |
+
color: #333;
|
| 1229 |
+
}
|
| 1230 |
+
|
| 1231 |
+
.transfer-output {
|
| 1232 |
+
margin-top: 8px;
|
| 1233 |
+
padding-top: 8px;
|
| 1234 |
+
border-top: 1px solid #e0e0e0;
|
| 1235 |
+
font-family: 'JetBrains Mono', monospace;
|
| 1236 |
+
font-size: 11px;
|
| 1237 |
+
color: #666;
|
| 1238 |
+
white-space: pre-wrap;
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
.transfer-output.error {
|
| 1242 |
+
color: #c62828;
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
/* Result Preview in CODE notebook */
|
| 1246 |
.result-preview {
|
| 1247 |
margin: 16px 0;
|
|
|
|
| 2137 |
overflow-x: auto;
|
| 2138 |
}
|
| 2139 |
|
| 2140 |
+
/* Debug button uses same styling as settings/files buttons */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2141 |
|
| 2142 |
/* Settings Panel (side panel like debug) */
|
| 2143 |
.settings-panel {
|
|
|
|
| 2205 |
padding: 20px;
|
| 2206 |
overflow-y: auto;
|
| 2207 |
}
|
| 2208 |
+
|
| 2209 |
+
/* Files Panel (right side panel, like settings/debug) */
|
| 2210 |
+
.files-panel {
|
| 2211 |
+
position: fixed;
|
| 2212 |
+
top: 37px;
|
| 2213 |
+
right: -320px;
|
| 2214 |
+
width: 320px;
|
| 2215 |
+
height: calc(100vh - 37px);
|
| 2216 |
+
background: white;
|
| 2217 |
+
border-left: 2px solid var(--theme-accent);
|
| 2218 |
+
z-index: 1000;
|
| 2219 |
+
display: flex;
|
| 2220 |
+
flex-direction: column;
|
| 2221 |
+
transition: right 0.3s ease;
|
| 2222 |
+
}
|
| 2223 |
+
|
| 2224 |
+
.files-panel.active {
|
| 2225 |
+
right: 0;
|
| 2226 |
+
}
|
| 2227 |
+
|
| 2228 |
+
.files-panel-header {
|
| 2229 |
+
padding: 8px 12px;
|
| 2230 |
+
border-bottom: 1px solid #e0e0e0;
|
| 2231 |
+
display: flex;
|
| 2232 |
+
justify-content: space-between;
|
| 2233 |
+
align-items: center;
|
| 2234 |
+
background: var(--theme-accent);
|
| 2235 |
+
gap: 8px;
|
| 2236 |
+
}
|
| 2237 |
+
|
| 2238 |
+
.files-panel-header h3 {
|
| 2239 |
+
margin: 0;
|
| 2240 |
+
font-size: 12px;
|
| 2241 |
+
font-weight: 600;
|
| 2242 |
+
color: white;
|
| 2243 |
+
text-transform: uppercase;
|
| 2244 |
+
letter-spacing: 0.5px;
|
| 2245 |
+
}
|
| 2246 |
+
|
| 2247 |
+
.files-panel-controls {
|
| 2248 |
+
display: flex;
|
| 2249 |
+
align-items: center;
|
| 2250 |
+
gap: 8px;
|
| 2251 |
+
margin-left: auto;
|
| 2252 |
+
}
|
| 2253 |
+
|
| 2254 |
+
.files-show-hidden {
|
| 2255 |
+
display: flex;
|
| 2256 |
+
align-items: center;
|
| 2257 |
+
gap: 6px;
|
| 2258 |
+
font-size: 10px;
|
| 2259 |
+
color: rgba(255, 255, 255, 0.8);
|
| 2260 |
+
cursor: pointer;
|
| 2261 |
+
}
|
| 2262 |
+
|
| 2263 |
+
.files-show-hidden input[type="checkbox"] {
|
| 2264 |
+
appearance: none;
|
| 2265 |
+
-webkit-appearance: none;
|
| 2266 |
+
width: 14px;
|
| 2267 |
+
height: 14px;
|
| 2268 |
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
| 2269 |
+
border-radius: 2px;
|
| 2270 |
+
background: transparent;
|
| 2271 |
+
cursor: pointer;
|
| 2272 |
+
position: relative;
|
| 2273 |
+
}
|
| 2274 |
+
|
| 2275 |
+
.files-show-hidden input[type="checkbox"]:checked {
|
| 2276 |
+
background: white;
|
| 2277 |
+
border-color: white;
|
| 2278 |
+
}
|
| 2279 |
+
|
| 2280 |
+
.files-show-hidden input[type="checkbox"]:checked::after {
|
| 2281 |
+
content: '✓';
|
| 2282 |
+
position: absolute;
|
| 2283 |
+
top: -1px;
|
| 2284 |
+
left: 2px;
|
| 2285 |
+
font-size: 11px;
|
| 2286 |
+
color: var(--theme-accent);
|
| 2287 |
+
font-weight: bold;
|
| 2288 |
+
}
|
| 2289 |
+
|
| 2290 |
+
.files-refresh-btn {
|
| 2291 |
+
background: none;
|
| 2292 |
+
border: none;
|
| 2293 |
+
color: white;
|
| 2294 |
+
font-size: 16px;
|
| 2295 |
+
cursor: pointer;
|
| 2296 |
+
padding: 2px 6px;
|
| 2297 |
+
border-radius: 4px;
|
| 2298 |
+
transition: background 0.2s;
|
| 2299 |
+
}
|
| 2300 |
+
|
| 2301 |
+
.files-refresh-btn:hover {
|
| 2302 |
+
background: rgba(255, 255, 255, 0.2);
|
| 2303 |
+
}
|
| 2304 |
+
|
| 2305 |
+
.files-panel-close {
|
| 2306 |
+
background: none;
|
| 2307 |
+
border: none;
|
| 2308 |
+
font-size: 20px;
|
| 2309 |
+
color: white;
|
| 2310 |
+
cursor: pointer;
|
| 2311 |
+
padding: 0;
|
| 2312 |
+
width: 24px;
|
| 2313 |
+
height: 24px;
|
| 2314 |
+
display: flex;
|
| 2315 |
+
align-items: center;
|
| 2316 |
+
justify-content: center;
|
| 2317 |
+
border-radius: 4px;
|
| 2318 |
+
transition: background 0.2s;
|
| 2319 |
+
}
|
| 2320 |
+
|
| 2321 |
+
.files-panel-close:hover {
|
| 2322 |
+
background: rgba(255, 255, 255, 0.2);
|
| 2323 |
+
}
|
| 2324 |
+
|
| 2325 |
+
.files-panel-body {
|
| 2326 |
+
flex: 1;
|
| 2327 |
+
padding: 8px 0;
|
| 2328 |
+
overflow-y: auto;
|
| 2329 |
+
font-size: 12px;
|
| 2330 |
+
}
|
| 2331 |
+
|
| 2332 |
+
.files-loading {
|
| 2333 |
+
padding: 16px;
|
| 2334 |
+
color: #666;
|
| 2335 |
+
text-align: center;
|
| 2336 |
+
}
|
| 2337 |
+
|
| 2338 |
+
/* File Tree Styles - matching research tree pattern */
|
| 2339 |
+
.file-tree-root {
|
| 2340 |
+
padding: 8px 12px;
|
| 2341 |
+
}
|
| 2342 |
+
|
| 2343 |
+
.file-tree-header {
|
| 2344 |
+
font-size: 12px;
|
| 2345 |
+
color: #666;
|
| 2346 |
+
padding: 0 0 4px 0;
|
| 2347 |
+
margin-bottom: 4px;
|
| 2348 |
+
}
|
| 2349 |
+
|
| 2350 |
+
/* Container for tree items - has vertical line */
|
| 2351 |
+
.file-tree-container {
|
| 2352 |
+
position: relative;
|
| 2353 |
+
padding-left: 20px;
|
| 2354 |
+
}
|
| 2355 |
+
|
| 2356 |
+
.file-tree-container::before {
|
| 2357 |
+
content: '';
|
| 2358 |
+
position: absolute;
|
| 2359 |
+
left: 0;
|
| 2360 |
+
top: 0;
|
| 2361 |
+
bottom: 0;
|
| 2362 |
+
width: 1px;
|
| 2363 |
+
background: #ccc;
|
| 2364 |
+
}
|
| 2365 |
+
|
| 2366 |
+
/* Individual tree item */
|
| 2367 |
+
.file-tree-item {
|
| 2368 |
+
position: relative;
|
| 2369 |
+
margin-bottom: 1px;
|
| 2370 |
+
}
|
| 2371 |
+
|
| 2372 |
+
/* Horizontal branch line */
|
| 2373 |
+
.file-tree-item::before {
|
| 2374 |
+
content: '';
|
| 2375 |
+
position: absolute;
|
| 2376 |
+
left: -20px;
|
| 2377 |
+
top: 10px;
|
| 2378 |
+
width: 12px;
|
| 2379 |
+
height: 1px;
|
| 2380 |
+
background: #ccc;
|
| 2381 |
+
}
|
| 2382 |
+
|
| 2383 |
+
/* Last item covers the vertical line below it */
|
| 2384 |
+
.file-tree-item.last::after {
|
| 2385 |
+
content: '';
|
| 2386 |
+
position: absolute;
|
| 2387 |
+
left: -20px;
|
| 2388 |
+
top: 11px;
|
| 2389 |
+
bottom: 0;
|
| 2390 |
+
width: 1px;
|
| 2391 |
+
background: white;
|
| 2392 |
+
}
|
| 2393 |
+
|
| 2394 |
+
.file-tree-line {
|
| 2395 |
+
display: flex;
|
| 2396 |
+
align-items: center;
|
| 2397 |
+
gap: 5px;
|
| 2398 |
+
padding: 3px 0;
|
| 2399 |
+
cursor: pointer;
|
| 2400 |
+
user-select: none;
|
| 2401 |
+
white-space: nowrap;
|
| 2402 |
+
font-size: 12px;
|
| 2403 |
+
color: #333;
|
| 2404 |
+
}
|
| 2405 |
+
|
| 2406 |
+
.file-tree-line:hover {
|
| 2407 |
+
color: var(--theme-accent);
|
| 2408 |
+
}
|
| 2409 |
+
|
| 2410 |
+
.file-tree-line:hover .file-tree-name {
|
| 2411 |
+
text-decoration: underline;
|
| 2412 |
+
}
|
| 2413 |
+
|
| 2414 |
+
.file-tree-icon {
|
| 2415 |
+
font-size: 10px;
|
| 2416 |
+
min-width: 12px;
|
| 2417 |
+
text-align: center;
|
| 2418 |
+
color: var(--theme-accent);
|
| 2419 |
+
flex-shrink: 0;
|
| 2420 |
+
}
|
| 2421 |
+
|
| 2422 |
+
.file-tree-item.file .file-tree-icon {
|
| 2423 |
+
color: transparent;
|
| 2424 |
+
}
|
| 2425 |
+
|
| 2426 |
+
.file-tree-name {
|
| 2427 |
+
overflow: hidden;
|
| 2428 |
+
text-overflow: ellipsis;
|
| 2429 |
+
}
|
| 2430 |
+
|
| 2431 |
+
/* Children container - same pattern as parent */
|
| 2432 |
+
.file-tree-children {
|
| 2433 |
+
display: none;
|
| 2434 |
+
position: relative;
|
| 2435 |
+
padding-left: 20px;
|
| 2436 |
+
}
|
| 2437 |
+
|
| 2438 |
+
.file-tree-children::before {
|
| 2439 |
+
content: '';
|
| 2440 |
+
position: absolute;
|
| 2441 |
+
left: 0;
|
| 2442 |
+
top: 0;
|
| 2443 |
+
bottom: 0;
|
| 2444 |
+
width: 1px;
|
| 2445 |
+
background: #ccc;
|
| 2446 |
+
}
|
| 2447 |
+
|
| 2448 |
+
.file-tree-children.expanded {
|
| 2449 |
+
display: block;
|
| 2450 |
+
}
|
tests/backend/test_api.py
CHANGED
|
@@ -152,6 +152,102 @@ class TestChatEndpoints:
|
|
| 152 |
# Empty messages should return 400
|
| 153 |
assert response.status_code == 400
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
class TestDebugEndpoints:
|
| 157 |
"""Test debug message history endpoints"""
|
|
@@ -198,6 +294,40 @@ class TestTitleGeneration:
|
|
| 198 |
assert response.status_code == 500
|
| 199 |
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
class TestStaticFiles:
|
| 202 |
"""Test static file serving"""
|
| 203 |
|
|
|
|
| 152 |
# Empty messages should return 400
|
| 153 |
assert response.status_code == 400
|
| 154 |
|
| 155 |
+
def test_chat_stream_accepts_frontend_context(self, client):
|
| 156 |
+
"""Test that chat accepts frontend_context parameter"""
|
| 157 |
+
response = client.post("/api/chat/stream", json={
|
| 158 |
+
"messages": [{"role": "user", "content": "Hello"}],
|
| 159 |
+
"notebook_type": "code",
|
| 160 |
+
"endpoint": "https://api.openai.com/v1",
|
| 161 |
+
"token": "test",
|
| 162 |
+
"model": "gpt-4",
|
| 163 |
+
"notebook_id": "1",
|
| 164 |
+
"frontend_context": {
|
| 165 |
+
"theme": {
|
| 166 |
+
"name": "forest",
|
| 167 |
+
"accent": "#1b5e20",
|
| 168 |
+
"bg": "#e8f5e9",
|
| 169 |
+
"border": "#1b5e20"
|
| 170 |
+
},
|
| 171 |
+
"open_notebooks": ["command", "code"]
|
| 172 |
+
}
|
| 173 |
+
})
|
| 174 |
+
# Request should be valid (actual streaming would fail due to invalid endpoint, but request is accepted)
|
| 175 |
+
# The request gets accepted and starts streaming, so we won't get a 4xx error
|
| 176 |
+
# Since we're using an invalid API key, it will eventually error but should accept the request format
|
| 177 |
+
assert response.status_code == 200 # SSE stream starts
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class TestFrontendContextModel:
|
| 181 |
+
"""Test FrontendContext Pydantic model"""
|
| 182 |
+
|
| 183 |
+
def test_frontend_context_model_creation(self):
|
| 184 |
+
"""Test that FrontendContext model can be created"""
|
| 185 |
+
import main
|
| 186 |
+
ctx = main.FrontendContext(
|
| 187 |
+
theme={"name": "forest", "accent": "#1b5e20", "bg": "#e8f5e9"},
|
| 188 |
+
open_notebooks=["command", "code"]
|
| 189 |
+
)
|
| 190 |
+
assert ctx.theme["name"] == "forest"
|
| 191 |
+
assert ctx.open_notebooks == ["command", "code"]
|
| 192 |
+
|
| 193 |
+
def test_frontend_context_optional_fields(self):
|
| 194 |
+
"""Test that FrontendContext fields are optional"""
|
| 195 |
+
import main
|
| 196 |
+
ctx = main.FrontendContext()
|
| 197 |
+
assert ctx.theme is None
|
| 198 |
+
assert ctx.open_notebooks is None
|
| 199 |
+
|
| 200 |
+
def test_chat_request_with_frontend_context(self):
|
| 201 |
+
"""Test ChatRequest with frontend_context"""
|
| 202 |
+
import main
|
| 203 |
+
ctx = main.FrontendContext(
|
| 204 |
+
theme={"name": "sapphire", "accent": "#0d47a1"}
|
| 205 |
+
)
|
| 206 |
+
req = main.ChatRequest(
|
| 207 |
+
messages=[],
|
| 208 |
+
endpoint="https://api.openai.com/v1",
|
| 209 |
+
frontend_context=ctx
|
| 210 |
+
)
|
| 211 |
+
assert req.frontend_context is not None
|
| 212 |
+
assert req.frontend_context.theme["name"] == "sapphire"
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class TestStylingContext:
|
| 216 |
+
"""Test styling context generation"""
|
| 217 |
+
|
| 218 |
+
def test_get_styling_context_without_theme(self):
|
| 219 |
+
"""Test get_styling_context without theme"""
|
| 220 |
+
import main
|
| 221 |
+
result = main.get_styling_context(None)
|
| 222 |
+
assert "Visual Style Guidelines" in result
|
| 223 |
+
assert "minimalist" in result
|
| 224 |
+
|
| 225 |
+
def test_get_styling_context_with_theme(self):
|
| 226 |
+
"""Test get_styling_context with theme"""
|
| 227 |
+
import main
|
| 228 |
+
theme = {"name": "forest", "accent": "#1b5e20", "bg": "#e8f5e9"}
|
| 229 |
+
result = main.get_styling_context(theme)
|
| 230 |
+
assert "forest" in result
|
| 231 |
+
assert "#1b5e20" in result
|
| 232 |
+
assert "#e8f5e9" in result
|
| 233 |
+
|
| 234 |
+
def test_get_system_prompt_code_with_context(self):
|
| 235 |
+
"""Test get_system_prompt for code notebook includes styling"""
|
| 236 |
+
import main
|
| 237 |
+
context = {"theme": {"name": "ocean", "accent": "#00796b", "bg": "#e0f2f1"}}
|
| 238 |
+
result = main.get_system_prompt("code", context)
|
| 239 |
+
assert "Visual Style Guidelines" in result
|
| 240 |
+
assert "ocean" in result
|
| 241 |
+
assert "#00796b" in result
|
| 242 |
+
|
| 243 |
+
def test_get_system_prompt_chat_no_styling(self):
|
| 244 |
+
"""Test get_system_prompt for chat notebook doesn't include styling"""
|
| 245 |
+
import main
|
| 246 |
+
context = {"theme": {"name": "forest", "accent": "#1b5e20"}}
|
| 247 |
+
result = main.get_system_prompt("chat", context)
|
| 248 |
+
# Chat notebooks should not have styling guidelines
|
| 249 |
+
assert "Visual Style Guidelines" not in result
|
| 250 |
+
|
| 251 |
|
| 252 |
class TestDebugEndpoints:
|
| 253 |
"""Test debug message history endpoints"""
|
|
|
|
| 294 |
assert response.status_code == 500
|
| 295 |
|
| 296 |
|
| 297 |
+
class TestFilesEndpoint:
|
| 298 |
+
"""Test file tree endpoint"""
|
| 299 |
+
|
| 300 |
+
def test_get_files_returns_tree(self, client):
|
| 301 |
+
"""Test that /api/files returns a file tree"""
|
| 302 |
+
response = client.get("/api/files")
|
| 303 |
+
assert response.status_code == 200
|
| 304 |
+
data = response.json()
|
| 305 |
+
assert "root" in data
|
| 306 |
+
assert "tree" in data
|
| 307 |
+
assert isinstance(data["tree"], list)
|
| 308 |
+
|
| 309 |
+
def test_get_files_with_show_hidden(self, client):
|
| 310 |
+
"""Test that show_hidden parameter works"""
|
| 311 |
+
response = client.get("/api/files?show_hidden=true")
|
| 312 |
+
assert response.status_code == 200
|
| 313 |
+
data = response.json()
|
| 314 |
+
assert "tree" in data
|
| 315 |
+
|
| 316 |
+
def test_file_tree_structure(self, client):
|
| 317 |
+
"""Test that file tree items have correct structure"""
|
| 318 |
+
response = client.get("/api/files")
|
| 319 |
+
data = response.json()
|
| 320 |
+
# Should have at least some items (frontend, backend folders)
|
| 321 |
+
assert len(data["tree"]) > 0
|
| 322 |
+
for item in data["tree"]:
|
| 323 |
+
assert "name" in item
|
| 324 |
+
assert "type" in item
|
| 325 |
+
assert item["type"] in ["file", "folder"]
|
| 326 |
+
assert "path" in item
|
| 327 |
+
if item["type"] == "folder":
|
| 328 |
+
assert "children" in item
|
| 329 |
+
|
| 330 |
+
|
| 331 |
class TestStaticFiles:
|
| 332 |
"""Test static file serving"""
|
| 333 |
|
tests/e2e/app.spec.js
CHANGED
|
@@ -129,6 +129,36 @@ test.describe('Debug Panel', () => {
|
|
| 129 |
});
|
| 130 |
});
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
test.describe('Input Fields', () => {
|
| 133 |
test('should have input field in command center', async ({ page }) => {
|
| 134 |
await page.goto('/');
|
|
|
|
| 129 |
});
|
| 130 |
});
|
| 131 |
|
| 132 |
+
test.describe('Files Panel', () => {
|
| 133 |
+
test('should open and close files panel', async ({ page }) => {
|
| 134 |
+
await page.goto('/');
|
| 135 |
+
|
| 136 |
+
// Click files button
|
| 137 |
+
await page.locator('#filesBtn').click();
|
| 138 |
+
|
| 139 |
+
// Panel should be visible
|
| 140 |
+
await expect(page.locator('#filesPanel')).toHaveClass(/active/);
|
| 141 |
+
await expect(page.locator('#filesBtn')).toHaveClass(/active/);
|
| 142 |
+
|
| 143 |
+
// Should load file tree
|
| 144 |
+
await page.waitForTimeout(500);
|
| 145 |
+
const treeItems = page.locator('.file-tree-item');
|
| 146 |
+
await expect(treeItems.first()).toBeVisible();
|
| 147 |
+
|
| 148 |
+
// Close panel
|
| 149 |
+
await page.locator('#filesPanelClose').click();
|
| 150 |
+
await expect(page.locator('#filesPanel')).not.toHaveClass(/active/);
|
| 151 |
+
});
|
| 152 |
+
|
| 153 |
+
test('should have show hidden toggle and refresh button', async ({ page }) => {
|
| 154 |
+
await page.goto('/');
|
| 155 |
+
|
| 156 |
+
await page.locator('#filesBtn').click();
|
| 157 |
+
await expect(page.locator('#showHiddenFiles')).toBeVisible();
|
| 158 |
+
await expect(page.locator('#filesRefresh')).toBeVisible();
|
| 159 |
+
});
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
test.describe('Input Fields', () => {
|
| 163 |
test('should have input field in command center', async ({ page }) => {
|
| 164 |
await page.goto('/');
|
tests/playwright.config.js
CHANGED
|
@@ -29,7 +29,7 @@ export default defineConfig({
|
|
| 29 |
},
|
| 30 |
],
|
| 31 |
webServer: {
|
| 32 |
-
command: `mkdir -p "${testDataDir}" && cd "${projectRoot}/backend
|
| 33 |
url: 'http://localhost:8766',
|
| 34 |
reuseExistingServer: !process.env.CI,
|
| 35 |
timeout: 30000,
|
|
|
|
| 29 |
},
|
| 30 |
],
|
| 31 |
webServer: {
|
| 32 |
+
command: `mkdir -p "${testDataDir}" && cd "${projectRoot}" && source env312/bin/activate && cd backend && python main.py --port 8766 --no-browser --data-dir "${testDataDir}"`,
|
| 33 |
url: 'http://localhost:8766',
|
| 34 |
reuseExistingServer: !process.env.CI,
|
| 35 |
timeout: 30000,
|
workspace/test/data.json
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"x": 0.0,
|
| 4 |
+
"y": -0.164171512786321
|
| 5 |
+
},
|
| 6 |
+
{
|
| 7 |
+
"x": 0.06314759102693052,
|
| 8 |
+
"y": 0.163486253247035
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"x": 0.12629518205386103,
|
| 12 |
+
"y": 0.06261328015881538
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"x": 0.18944277308079155,
|
| 16 |
+
"y": 0.1287950820033433
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"x": 0.25259036410772207,
|
| 20 |
+
"y": 0.4162089174928977
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"x": 0.3157379551346526,
|
| 24 |
+
"y": 0.10724584107532617
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"x": 0.3788855461615831,
|
| 28 |
+
"y": 0.4987717988412488
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"x": 0.4420331371885136,
|
| 32 |
+
"y": 0.8581795430773769
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"x": 0.5051807282154441,
|
| 36 |
+
"y": 0.36924957416756565
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"x": 0.5683283192423747,
|
| 40 |
+
"y": 0.5772833266593322
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"x": 0.6314759102693052,
|
| 44 |
+
"y": 0.27454277951949413
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"x": 0.6946235012962356,
|
| 48 |
+
"y": 0.7687628470013369
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"x": 0.7577710923231662,
|
| 52 |
+
"y": 0.20400266081078122
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"x": 0.8209186833500968,
|
| 56 |
+
"y": 0.6464892997888316
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"x": 0.8840662743770272,
|
| 60 |
+
"y": 0.883198389772504
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"x": 0.9472138654039577,
|
| 64 |
+
"y": 0.33362855877086944
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"x": 1.0103614564308883,
|
| 68 |
+
"y": 1.3986711499478823
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"x": 1.0735090474578188,
|
| 72 |
+
"y": 0.6106210353416557
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"x": 1.1366566384847494,
|
| 76 |
+
"y": 1.191478286764215
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"x": 1.1998042295116798,
|
| 80 |
+
"y": 1.2612451639413258
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"x": 1.2629518205386103,
|
| 84 |
+
"y": 1.2800466075403019
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"x": 1.326099411565541,
|
| 88 |
+
"y": 1.3440291604812473
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"x": 1.3892470025924712,
|
| 92 |
+
"y": 1.1624599256020136
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"x": 1.4523945936194018,
|
| 96 |
+
"y": 1.0545608609636905
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"x": 1.5155421846463324,
|
| 100 |
+
"y": 1.2356356840077487
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"x": 1.578689775673263,
|
| 104 |
+
"y": 0.9086702625892683
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"x": 1.6418373667001935,
|
| 108 |
+
"y": 0.9439604908602219
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"x": 1.7049849577271239,
|
| 112 |
+
"y": 0.8630401725643511
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"x": 1.7681325487540545,
|
| 116 |
+
"y": 1.121959888544887
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"x": 1.831280139780985,
|
| 120 |
+
"y": 1.1861278593856923
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"x": 1.8944277308079154,
|
| 124 |
+
"y": 0.9237535337281273
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"x": 1.957575321834846,
|
| 128 |
+
"y": 0.6374338821271788
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"x": 2.0207229128617765,
|
| 132 |
+
"y": 0.7915592047370582
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"x": 2.083870503888707,
|
| 136 |
+
"y": 1.0790349015495115
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"x": 2.1470180949156377,
|
| 140 |
+
"y": 0.8412001427609885
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"x": 2.210165685942568,
|
| 144 |
+
"y": 0.7471330119640848
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"x": 2.273313276969499,
|
| 148 |
+
"y": 0.7916058803441647
|
| 149 |
+
},
|
| 150 |
+
{
|
| 151 |
+
"x": 2.336460867996429,
|
| 152 |
+
"y": 1.1478520916781954
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"x": 2.3996084590233595,
|
| 156 |
+
"y": 1.102927250024849
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"x": 2.4627560500502903,
|
| 160 |
+
"y": 1.0222803505714153
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"x": 2.5259036410772207,
|
| 164 |
+
"y": 0.24594572888301336
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"x": 2.589051232104151,
|
| 168 |
+
"y": 0.3044731356625734
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"x": 2.652198823131082,
|
| 172 |
+
"y": 0.980832694746469
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"x": 2.715346414158012,
|
| 176 |
+
"y": 0.3098313928561607
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"x": 2.7784940051849425,
|
| 180 |
+
"y": 0.5088460679801076
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"x": 2.8416415962118733,
|
| 184 |
+
"y": 0.33875060761239734
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"x": 2.9047891872388036,
|
| 188 |
+
"y": 0.031108350101302168
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"x": 2.9679367782657344,
|
| 192 |
+
"y": 0.048145116342521194
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"x": 3.031084369292665,
|
| 196 |
+
"y": 0.4878372210124654
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"x": 3.094231960319595,
|
| 200 |
+
"y": -0.1434572705091495
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"x": 3.157379551346526,
|
| 204 |
+
"y": -0.2506176171524982
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"x": 3.2205271423734563,
|
| 208 |
+
"y": -0.3137172220787668
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"x": 3.283674733400387,
|
| 212 |
+
"y": -0.03622420388515449
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"x": 3.3468223244273174,
|
| 216 |
+
"y": -0.6134287755190555
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"x": 3.4099699154542478,
|
| 220 |
+
"y": -0.5437046121555442
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"x": 3.4731175064811786,
|
| 224 |
+
"y": -0.5672415784533531
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"x": 3.536265097508109,
|
| 228 |
+
"y": -0.6591927925628567
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"x": 3.5994126885350393,
|
| 232 |
+
"y": -0.07212837355303453
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"x": 3.66256027956197,
|
| 236 |
+
"y": -1.1270338805013864
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"x": 3.7257078705889004,
|
| 240 |
+
"y": -0.39267447979562786
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"x": 3.7888554616158308,
|
| 244 |
+
"y": -1.100799104320724
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"x": 3.8520030526427615,
|
| 248 |
+
"y": -1.1534773728612362
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"x": 3.915150643669692,
|
| 252 |
+
"y": -1.0075038531610818
|
| 253 |
+
},
|
| 254 |
+
{
|
| 255 |
+
"x": 3.9782982346966227,
|
| 256 |
+
"y": -1.2143815081773557
|
| 257 |
+
},
|
| 258 |
+
{
|
| 259 |
+
"x": 4.041445825723553,
|
| 260 |
+
"y": -0.7516687923490095
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"x": 4.104593416750483,
|
| 264 |
+
"y": -0.67087026419052
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"x": 4.167741007777414,
|
| 268 |
+
"y": -0.4759748204278203
|
| 269 |
+
},
|
| 270 |
+
{
|
| 271 |
+
"x": 4.230888598804345,
|
| 272 |
+
"y": -0.5949711890796414
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"x": 4.294036189831275,
|
| 276 |
+
"y": -0.9959877106987852
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
"x": 4.357183780858206,
|
| 280 |
+
"y": -0.6836905591037317
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"x": 4.420331371885136,
|
| 284 |
+
"y": -0.9883010946906967
|
| 285 |
+
},
|
| 286 |
+
{
|
| 287 |
+
"x": 4.483478962912066,
|
| 288 |
+
"y": -1.0835565110252792
|
| 289 |
+
},
|
| 290 |
+
{
|
| 291 |
+
"x": 4.546626553938998,
|
| 292 |
+
"y": -0.7897297525496717
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
"x": 4.609774144965928,
|
| 296 |
+
"y": -1.4249288518380685
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
"x": 4.672921735992858,
|
| 300 |
+
"y": -1.093316588219836
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"x": 4.736069327019789,
|
| 304 |
+
"y": -1.2279258571060911
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"x": 4.799216918046719,
|
| 308 |
+
"y": -1.1244341966895806
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
"x": 4.862364509073649,
|
| 312 |
+
"y": -1.0131952038478669
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"x": 4.925512100100581,
|
| 316 |
+
"y": -0.7973338101080346
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
"x": 4.988659691127511,
|
| 320 |
+
"y": -1.4699281322164164
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"x": 5.051807282154441,
|
| 324 |
+
"y": -1.0445495623407317
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
"x": 5.114954873181372,
|
| 328 |
+
"y": -1.0688809476224235
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"x": 5.178102464208302,
|
| 332 |
+
"y": -0.7811048981060283
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"x": 5.241250055235233,
|
| 336 |
+
"y": -0.8622544816438432
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"x": 5.304397646262164,
|
| 340 |
+
"y": -0.8200809952408314
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"x": 5.367545237289094,
|
| 344 |
+
"y": -0.5632508023113583
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"x": 5.430692828316024,
|
| 348 |
+
"y": -0.8531962033267921
|
| 349 |
+
},
|
| 350 |
+
{
|
| 351 |
+
"x": 5.493840419342955,
|
| 352 |
+
"y": -0.5707860276698525
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
"x": 5.556988010369885,
|
| 356 |
+
"y": -0.24510318810662557
|
| 357 |
+
},
|
| 358 |
+
{
|
| 359 |
+
"x": 5.620135601396816,
|
| 360 |
+
"y": -0.5845323088129928
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"x": 5.683283192423747,
|
| 364 |
+
"y": -0.8852990629058648
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
"x": 5.746430783450677,
|
| 368 |
+
"y": -0.09046021871108612
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"x": 5.809578374477607,
|
| 372 |
+
"y": -0.2620257018787725
|
| 373 |
+
},
|
| 374 |
+
{
|
| 375 |
+
"x": 5.872725965504538,
|
| 376 |
+
"y": -0.4648789856873344
|
| 377 |
+
},
|
| 378 |
+
{
|
| 379 |
+
"x": 5.935873556531469,
|
| 380 |
+
"y": -0.2873710734188944
|
| 381 |
+
},
|
| 382 |
+
{
|
| 383 |
+
"x": 5.999021147558399,
|
| 384 |
+
"y": -0.040189428299803626
|
| 385 |
+
},
|
| 386 |
+
{
|
| 387 |
+
"x": 6.06216873858533,
|
| 388 |
+
"y": -0.2828671334244661
|
| 389 |
+
},
|
| 390 |
+
{
|
| 391 |
+
"x": 6.12531632961226,
|
| 392 |
+
"y": -0.13276545533424783
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
"x": 6.18846392063919,
|
| 396 |
+
"y": 0.12576175072770018
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"x": 6.2516115116661215,
|
| 400 |
+
"y": -0.20227606757172212
|
| 401 |
+
},
|
| 402 |
+
{
|
| 403 |
+
"x": 6.314759102693052,
|
| 404 |
+
"y": 0.18443496978191243
|
| 405 |
+
},
|
| 406 |
+
{
|
| 407 |
+
"x": 6.377906693719982,
|
| 408 |
+
"y": 0.6654671164991045
|
| 409 |
+
},
|
| 410 |
+
{
|
| 411 |
+
"x": 6.4410542847469126,
|
| 412 |
+
"y": 0.005923853694681153
|
| 413 |
+
},
|
| 414 |
+
{
|
| 415 |
+
"x": 6.504201875773843,
|
| 416 |
+
"y": 0.17336545296245054
|
| 417 |
+
},
|
| 418 |
+
{
|
| 419 |
+
"x": 6.567349466800774,
|
| 420 |
+
"y": 0.013180109983592536
|
| 421 |
+
},
|
| 422 |
+
{
|
| 423 |
+
"x": 6.6304970578277045,
|
| 424 |
+
"y": 0.44832314627570274
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"x": 6.693644648854635,
|
| 428 |
+
"y": 0.1859579298190778
|
| 429 |
+
},
|
| 430 |
+
{
|
| 431 |
+
"x": 6.756792239881565,
|
| 432 |
+
"y": 0.33142152760319854
|
| 433 |
+
},
|
| 434 |
+
{
|
| 435 |
+
"x": 6.8199398309084955,
|
| 436 |
+
"y": 0.4489501812855101
|
| 437 |
+
},
|
| 438 |
+
{
|
| 439 |
+
"x": 6.883087421935426,
|
| 440 |
+
"y": 0.6286649015324393
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"x": 6.946235012962357,
|
| 444 |
+
"y": 1.1017509992163785
|
| 445 |
+
},
|
| 446 |
+
{
|
| 447 |
+
"x": 7.0093826039892875,
|
| 448 |
+
"y": 0.046005745188661074
|
| 449 |
+
},
|
| 450 |
+
{
|
| 451 |
+
"x": 7.072530195016218,
|
| 452 |
+
"y": 0.8630844450949116
|
| 453 |
+
},
|
| 454 |
+
{
|
| 455 |
+
"x": 7.135677786043148,
|
| 456 |
+
"y": 0.5866169106138275
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"x": 7.1988253770700785,
|
| 460 |
+
"y": 0.8471167331747427
|
| 461 |
+
},
|
| 462 |
+
{
|
| 463 |
+
"x": 7.26197296809701,
|
| 464 |
+
"y": 0.3755080122113592
|
| 465 |
+
},
|
| 466 |
+
{
|
| 467 |
+
"x": 7.32512055912394,
|
| 468 |
+
"y": 1.4001383615856322
|
| 469 |
+
},
|
| 470 |
+
{
|
| 471 |
+
"x": 7.3882681501508705,
|
| 472 |
+
"y": 1.056872792099301
|
| 473 |
+
},
|
| 474 |
+
{
|
| 475 |
+
"x": 7.451415741177801,
|
| 476 |
+
"y": 0.927622796811582
|
| 477 |
+
},
|
| 478 |
+
{
|
| 479 |
+
"x": 7.514563332204731,
|
| 480 |
+
"y": 1.0317644736936173
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"x": 7.5777109232316615,
|
| 484 |
+
"y": 1.2900239127063728
|
| 485 |
+
},
|
| 486 |
+
{
|
| 487 |
+
"x": 7.640858514258593,
|
| 488 |
+
"y": 0.4826112615934133
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"x": 7.704006105285523,
|
| 492 |
+
"y": 1.3282957305777943
|
| 493 |
+
},
|
| 494 |
+
{
|
| 495 |
+
"x": 7.7671536963124534,
|
| 496 |
+
"y": 0.9503048197434517
|
| 497 |
+
},
|
| 498 |
+
{
|
| 499 |
+
"x": 7.830301287339384,
|
| 500 |
+
"y": 0.8337681217692257
|
| 501 |
+
},
|
| 502 |
+
{
|
| 503 |
+
"x": 7.893448878366314,
|
| 504 |
+
"y": 1.2693367746926236
|
| 505 |
+
},
|
| 506 |
+
{
|
| 507 |
+
"x": 7.956596469393245,
|
| 508 |
+
"y": 1.1893868376430397
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"x": 8.019744060420175,
|
| 512 |
+
"y": 1.0221508314347647
|
| 513 |
+
},
|
| 514 |
+
{
|
| 515 |
+
"x": 8.082891651447106,
|
| 516 |
+
"y": 1.0508367571681816
|
| 517 |
+
},
|
| 518 |
+
{
|
| 519 |
+
"x": 8.146039242474037,
|
| 520 |
+
"y": 0.5927617697173999
|
| 521 |
+
},
|
| 522 |
+
{
|
| 523 |
+
"x": 8.209186833500967,
|
| 524 |
+
"y": 1.3099213762130097
|
| 525 |
+
},
|
| 526 |
+
{
|
| 527 |
+
"x": 8.272334424527898,
|
| 528 |
+
"y": 1.5395993246195032
|
| 529 |
+
},
|
| 530 |
+
{
|
| 531 |
+
"x": 8.335482015554827,
|
| 532 |
+
"y": 0.9139099685410099
|
| 533 |
+
},
|
| 534 |
+
{
|
| 535 |
+
"x": 8.398629606581759,
|
| 536 |
+
"y": 0.8605135181886638
|
| 537 |
+
},
|
| 538 |
+
{
|
| 539 |
+
"x": 8.46177719760869,
|
| 540 |
+
"y": 1.0023221551863521
|
| 541 |
+
},
|
| 542 |
+
{
|
| 543 |
+
"x": 8.52492478863562,
|
| 544 |
+
"y": 0.778626113997441
|
| 545 |
+
},
|
| 546 |
+
{
|
| 547 |
+
"x": 8.58807237966255,
|
| 548 |
+
"y": 0.7040585197240989
|
| 549 |
+
},
|
| 550 |
+
{
|
| 551 |
+
"x": 8.65121997068948,
|
| 552 |
+
"y": 0.8356536401766204
|
| 553 |
+
},
|
| 554 |
+
{
|
| 555 |
+
"x": 8.714367561716411,
|
| 556 |
+
"y": 0.551214633626738
|
| 557 |
+
},
|
| 558 |
+
{
|
| 559 |
+
"x": 8.777515152743343,
|
| 560 |
+
"y": 0.12502069242348735
|
| 561 |
+
},
|
| 562 |
+
{
|
| 563 |
+
"x": 8.840662743770272,
|
| 564 |
+
"y": 0.1946906386376765
|
| 565 |
+
},
|
| 566 |
+
{
|
| 567 |
+
"x": 8.903810334797203,
|
| 568 |
+
"y": 0.4898616845022501
|
| 569 |
+
},
|
| 570 |
+
{
|
| 571 |
+
"x": 8.966957925824133,
|
| 572 |
+
"y": 0.0891643765998798
|
| 573 |
+
},
|
| 574 |
+
{
|
| 575 |
+
"x": 9.030105516851064,
|
| 576 |
+
"y": 0.3559871777856316
|
| 577 |
+
},
|
| 578 |
+
{
|
| 579 |
+
"x": 9.093253107877995,
|
| 580 |
+
"y": 0.4509391287898228
|
| 581 |
+
},
|
| 582 |
+
{
|
| 583 |
+
"x": 9.156400698904925,
|
| 584 |
+
"y": -0.17054735202703009
|
| 585 |
+
},
|
| 586 |
+
{
|
| 587 |
+
"x": 9.219548289931856,
|
| 588 |
+
"y": -0.03455046652518412
|
| 589 |
+
},
|
| 590 |
+
{
|
| 591 |
+
"x": 9.282695880958785,
|
| 592 |
+
"y": -0.24024028916603768
|
| 593 |
+
},
|
| 594 |
+
{
|
| 595 |
+
"x": 9.345843471985717,
|
| 596 |
+
"y": 0.49602887100611226
|
| 597 |
+
},
|
| 598 |
+
{
|
| 599 |
+
"x": 9.408991063012646,
|
| 600 |
+
"y": -0.21652837137778178
|
| 601 |
+
},
|
| 602 |
+
{
|
| 603 |
+
"x": 9.472138654039577,
|
| 604 |
+
"y": -0.4173513379049771
|
| 605 |
+
},
|
| 606 |
+
{
|
| 607 |
+
"x": 9.535286245066509,
|
| 608 |
+
"y": -0.01660495139973743
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"x": 9.598433836093438,
|
| 612 |
+
"y": 0.10512700333750288
|
| 613 |
+
},
|
| 614 |
+
{
|
| 615 |
+
"x": 9.66158142712037,
|
| 616 |
+
"y": -0.7939214616363623
|
| 617 |
+
},
|
| 618 |
+
{
|
| 619 |
+
"x": 9.724729018147299,
|
| 620 |
+
"y": -0.23761946954476132
|
| 621 |
+
},
|
| 622 |
+
{
|
| 623 |
+
"x": 9.78787660917423,
|
| 624 |
+
"y": -0.3714976580077742
|
| 625 |
+
},
|
| 626 |
+
{
|
| 627 |
+
"x": 9.851024200201161,
|
| 628 |
+
"y": -0.003531267598519905
|
| 629 |
+
},
|
| 630 |
+
{
|
| 631 |
+
"x": 9.91417179122809,
|
| 632 |
+
"y": -0.7961663694079557
|
| 633 |
+
},
|
| 634 |
+
{
|
| 635 |
+
"x": 9.977319382255022,
|
| 636 |
+
"y": -0.31629538059521983
|
| 637 |
+
},
|
| 638 |
+
{
|
| 639 |
+
"x": 10.040466973281951,
|
| 640 |
+
"y": -0.42770312455028653
|
| 641 |
+
},
|
| 642 |
+
{
|
| 643 |
+
"x": 10.103614564308883,
|
| 644 |
+
"y": -0.8182108550353944
|
| 645 |
+
},
|
| 646 |
+
{
|
| 647 |
+
"x": 10.166762155335814,
|
| 648 |
+
"y": -0.2691246828378265
|
| 649 |
+
},
|
| 650 |
+
{
|
| 651 |
+
"x": 10.229909746362743,
|
| 652 |
+
"y": -0.9194522138147393
|
| 653 |
+
},
|
| 654 |
+
{
|
| 655 |
+
"x": 10.293057337389675,
|
| 656 |
+
"y": -0.7019982784275451
|
| 657 |
+
},
|
| 658 |
+
{
|
| 659 |
+
"x": 10.356204928416604,
|
| 660 |
+
"y": -0.7447445602245387
|
| 661 |
+
},
|
| 662 |
+
{
|
| 663 |
+
"x": 10.419352519443535,
|
| 664 |
+
"y": -0.9736255218039706
|
| 665 |
+
},
|
| 666 |
+
{
|
| 667 |
+
"x": 10.482500110470466,
|
| 668 |
+
"y": -1.0004812863703996
|
| 669 |
+
},
|
| 670 |
+
{
|
| 671 |
+
"x": 10.545647701497396,
|
| 672 |
+
"y": -0.9965174674911578
|
| 673 |
+
},
|
| 674 |
+
{
|
| 675 |
+
"x": 10.608795292524327,
|
| 676 |
+
"y": -0.8664202856415287
|
| 677 |
+
},
|
| 678 |
+
{
|
| 679 |
+
"x": 10.671942883551257,
|
| 680 |
+
"y": -1.1120285277235913
|
| 681 |
+
},
|
| 682 |
+
{
|
| 683 |
+
"x": 10.735090474578188,
|
| 684 |
+
"y": -0.9947249637973103
|
| 685 |
+
},
|
| 686 |
+
{
|
| 687 |
+
"x": 10.79823806560512,
|
| 688 |
+
"y": -0.8874804425495924
|
| 689 |
+
},
|
| 690 |
+
{
|
| 691 |
+
"x": 10.861385656632049,
|
| 692 |
+
"y": -1.0741905493668167
|
| 693 |
+
},
|
| 694 |
+
{
|
| 695 |
+
"x": 10.92453324765898,
|
| 696 |
+
"y": -0.6584825545192039
|
| 697 |
+
},
|
| 698 |
+
{
|
| 699 |
+
"x": 10.98768083868591,
|
| 700 |
+
"y": -0.665907715020841
|
| 701 |
+
},
|
| 702 |
+
{
|
| 703 |
+
"x": 11.05082842971284,
|
| 704 |
+
"y": -0.6659335744731814
|
| 705 |
+
},
|
| 706 |
+
{
|
| 707 |
+
"x": 11.11397602073977,
|
| 708 |
+
"y": -1.0222906279196728
|
| 709 |
+
},
|
| 710 |
+
{
|
| 711 |
+
"x": 11.177123611766701,
|
| 712 |
+
"y": -1.203755211455369
|
| 713 |
+
},
|
| 714 |
+
{
|
| 715 |
+
"x": 11.240271202793632,
|
| 716 |
+
"y": -0.6462563794879772
|
| 717 |
+
},
|
| 718 |
+
{
|
| 719 |
+
"x": 11.303418793820562,
|
| 720 |
+
"y": -0.9571887636228328
|
| 721 |
+
},
|
| 722 |
+
{
|
| 723 |
+
"x": 11.366566384847493,
|
| 724 |
+
"y": -0.7045729933655224
|
| 725 |
+
},
|
| 726 |
+
{
|
| 727 |
+
"x": 11.429713975874423,
|
| 728 |
+
"y": -0.8634863157789737
|
| 729 |
+
},
|
| 730 |
+
{
|
| 731 |
+
"x": 11.492861566901354,
|
| 732 |
+
"y": -0.6082344417839276
|
| 733 |
+
},
|
| 734 |
+
{
|
| 735 |
+
"x": 11.556009157928285,
|
| 736 |
+
"y": -0.4975954484935668
|
| 737 |
+
},
|
| 738 |
+
{
|
| 739 |
+
"x": 11.619156748955215,
|
| 740 |
+
"y": -0.7194991919306026
|
| 741 |
+
},
|
| 742 |
+
{
|
| 743 |
+
"x": 11.682304339982146,
|
| 744 |
+
"y": -0.9492131430891632
|
| 745 |
+
},
|
| 746 |
+
{
|
| 747 |
+
"x": 11.745451931009075,
|
| 748 |
+
"y": -0.8775978335046639
|
| 749 |
+
},
|
| 750 |
+
{
|
| 751 |
+
"x": 11.808599522036006,
|
| 752 |
+
"y": -1.3596565296630223
|
| 753 |
+
},
|
| 754 |
+
{
|
| 755 |
+
"x": 11.871747113062938,
|
| 756 |
+
"y": -0.33550469301555175
|
| 757 |
+
},
|
| 758 |
+
{
|
| 759 |
+
"x": 11.934894704089867,
|
| 760 |
+
"y": -0.46799163746781575
|
| 761 |
+
},
|
| 762 |
+
{
|
| 763 |
+
"x": 11.998042295116798,
|
| 764 |
+
"y": -0.8311283805255221
|
| 765 |
+
},
|
| 766 |
+
{
|
| 767 |
+
"x": 12.061189886143728,
|
| 768 |
+
"y": -0.5108402774253894
|
| 769 |
+
},
|
| 770 |
+
{
|
| 771 |
+
"x": 12.12433747717066,
|
| 772 |
+
"y": -0.9338327430742724
|
| 773 |
+
},
|
| 774 |
+
{
|
| 775 |
+
"x": 12.18748506819759,
|
| 776 |
+
"y": -0.7326288644118045
|
| 777 |
+
},
|
| 778 |
+
{
|
| 779 |
+
"x": 12.25063265922452,
|
| 780 |
+
"y": -0.12990942292220112
|
| 781 |
+
},
|
| 782 |
+
{
|
| 783 |
+
"x": 12.313780250251451,
|
| 784 |
+
"y": 0.18453948514732188
|
| 785 |
+
},
|
| 786 |
+
{
|
| 787 |
+
"x": 12.37692784127838,
|
| 788 |
+
"y": 0.13031087879597583
|
| 789 |
+
},
|
| 790 |
+
{
|
| 791 |
+
"x": 12.440075432305312,
|
| 792 |
+
"y": 0.024949724612247842
|
| 793 |
+
},
|
| 794 |
+
{
|
| 795 |
+
"x": 12.503223023332243,
|
| 796 |
+
"y": -0.586521279399101
|
| 797 |
+
},
|
| 798 |
+
{
|
| 799 |
+
"x": 12.566370614359172,
|
| 800 |
+
"y": 0.38312468760165397
|
| 801 |
+
}
|
| 802 |
+
]
|
workspace/test/synthetic_data.csv
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Customer_ID,Age,Gender,Income,Region,Product_Category,Purchase_Amount,Purchase_Frequency,Payment_Method
|
| 2 |
+
1,40,Male,65009,North,Books,500,4.0,Credit Card
|
| 3 |
+
2,33,Female,56428,West,Electronics,500,4.8,Credit Card
|
| 4 |
+
3,42,Female,49399,North,Electronics,500,4.0,Bank Transfer
|
| 5 |
+
4,53,Female,84145,North,Electronics,500,4.0,Credit Card
|
| 6 |
+
5,32,Male,53861,North,Electronics,500,4.8,Bank Transfer
|
| 7 |
+
6,32,Female,33712,North,Clothing,500,4.8,Cash on Delivery
|
| 8 |
+
7,53,Female,116815,North,Electronics,500,4.0,Cash on Delivery
|
| 9 |
+
8,44,Male,75476,East,Electronics,500,4.0,Bank Transfer
|
| 10 |
+
9,29,Male,20000,South,Electronics,500,4.4,Cash on Delivery
|
| 11 |
+
10,41,Female,74631,West,Home & Kitchen,500,4.0,Credit Card
|
| 12 |
+
11,29,Female,24006,West,Clothing,493,4.7,Bank Transfer
|
| 13 |
+
12,29,Female,59241,South,Clothing,500,4.8,Credit Card
|
| 14 |
+
13,37,Female,78671,North,Home & Kitchen,500,4.0,Credit Card
|
| 15 |
+
14,18,Female,20000,South,Books,444,3.7,PayPal
|
| 16 |
+
15,18,Male,46267,East,Electronics,500,4.8,Bank Transfer
|
| 17 |
+
16,28,Male,50255,North,Books,500,4.8,Cash on Delivery
|
| 18 |
+
17,22,Male,49441,East,Home & Kitchen,500,4.8,Bank Transfer
|
| 19 |
+
18,38,Female,94935,South,Electronics,500,4.0,PayPal
|
| 20 |
+
19,24,Male,31092,East,Sports,500,4.8,Bank Transfer
|
| 21 |
+
20,18,Female,20000,South,Sports,416,3.7,Bank Transfer
|
| 22 |
+
21,52,Female,60209,North,Electronics,500,4.0,Credit Card
|
| 23 |
+
22,32,Female,31683,West,Clothing,500,4.8,Credit Card
|
| 24 |
+
23,35,Male,50957,East,Sports,500,4.8,Credit Card
|
| 25 |
+
24,18,Female,33823,North,Books,500,4.6,Credit Card
|
| 26 |
+
25,28,Male,47533,West,Electronics,500,4.8,Credit Card
|
| 27 |
+
26,36,Female,70543,South,Clothing,500,4.0,Credit Card
|
| 28 |
+
27,21,Male,31760,West,Clothing,500,4.6,Credit Card
|
| 29 |
+
28,39,Male,87570,East,Home & Kitchen,500,4.0,Credit Card
|
| 30 |
+
29,27,Female,35206,South,Home & Kitchen,500,4.8,Cash on Delivery
|
| 31 |
+
30,31,Male,100903,North,Clothing,500,4.8,Credit Card
|
| 32 |
+
31,27,Male,53013,North,Home & Kitchen,500,4.8,Credit Card
|
| 33 |
+
32,57,Female,68356,South,Books,500,4.0,PayPal
|
| 34 |
+
33,34,Female,29582,East,Electronics,500,4.8,Credit Card
|
| 35 |
+
34,22,Female,42649,North,Clothing,500,4.8,Cash on Delivery
|
| 36 |
+
35,44,Female,61530,North,Books,500,4.0,Bank Transfer
|
| 37 |
+
36,20,Female,44280,West,Sports,500,4.8,Credit Card
|
| 38 |
+
37,37,Male,64964,West,Home & Kitchen,500,4.0,Credit Card
|
| 39 |
+
38,18,Male,25543,South,Home & Kitchen,500,4.1,Bank Transfer
|
| 40 |
+
39,19,Male,20000,North,Electronics,500,3.8,Cash on Delivery
|
| 41 |
+
40,37,Female,25203,North,Clothing,500,4.0,Credit Card
|
| 42 |
+
41,43,Female,55569,North,Home & Kitchen,500,4.0,Bank Transfer
|
| 43 |
+
42,37,Male,72627,West,Clothing,500,4.0,Credit Card
|
| 44 |
+
43,33,Male,53781,North,Clothing,500,4.8,Cash on Delivery
|
| 45 |
+
44,31,Female,21585,East,Clothing,436,4.6,Credit Card
|
| 46 |
+
45,18,Male,30463,North,Electronics,500,4.3,Cash on Delivery
|
| 47 |
+
46,26,Female,46706,East,Clothing,500,4.8,PayPal
|
| 48 |
+
47,29,Female,25822,East,Clothing,492,4.8,PayPal
|
| 49 |
+
48,47,Female,73574,West,Clothing,500,4.0,Bank Transfer
|
| 50 |
+
49,39,Male,59664,North,Electronics,500,4.0,Credit Card
|
| 51 |
+
50,18,Female,20000,North,Sports,413,3.7,Credit Card
|
| 52 |
+
51,38,Female,64155,North,Home & Kitchen,500,4.0,Credit Card
|
| 53 |
+
52,30,Female,56215,West,Books,500,4.8,Credit Card
|
| 54 |
+
53,26,Female,60661,West,Books,500,4.8,Bank Transfer
|
| 55 |
+
54,42,Female,84076,East,Home & Kitchen,500,4.0,Credit Card
|
| 56 |
+
55,47,Female,42946,East,Electronics,500,4.0,Credit Card
|
| 57 |
+
56,46,Male,50243,East,Home & Kitchen,500,4.0,Cash on Delivery
|
| 58 |
+
57,24,Female,46300,North,Home & Kitchen,500,4.8,Bank Transfer
|
| 59 |
+
58,31,Female,56775,West,Books,500,4.8,Cash on Delivery
|
| 60 |
+
59,38,Male,67300,South,Clothing,500,4.0,Cash on Delivery
|
| 61 |
+
60,46,Female,120000,South,Electronics,500,4.0,Credit Card
|
| 62 |
+
61,29,Female,54917,South,Clothing,500,4.8,Credit Card
|
| 63 |
+
62,32,Female,70711,North,Clothing,500,4.8,Bank Transfer
|
| 64 |
+
63,21,Male,50580,North,Home & Kitchen,500,4.8,PayPal
|
| 65 |
+
64,20,Male,43027,East,Home & Kitchen,500,4.8,Credit Card
|
| 66 |
+
65,44,Female,59694,North,Clothing,500,4.0,Bank Transfer
|
| 67 |
+
66,51,Female,91679,East,Sports,500,4.0,Credit Card
|
| 68 |
+
67,34,Male,35543,North,Home & Kitchen,500,4.8,Credit Card
|
| 69 |
+
68,47,Female,65763,South,Clothing,500,4.0,Credit Card
|
| 70 |
+
69,39,Female,48792,North,Electronics,500,4.0,Cash on Delivery
|
| 71 |
+
70,27,Female,42137,South,Electronics,500,4.8,Bank Transfer
|
| 72 |
+
71,39,Female,104793,East,Clothing,500,4.0,Credit Card
|
| 73 |
+
72,53,Male,42154,North,Electronics,500,4.0,Bank Transfer
|
| 74 |
+
73,34,Male,64725,East,Electronics,500,4.8,Credit Card
|
| 75 |
+
74,53,Male,47245,South,Clothing,500,4.0,Credit Card
|
| 76 |
+
75,18,Female,20000,East,Electronics,500,3.7,Bank Transfer
|
| 77 |
+
76,44,Male,87779,West,Sports,500,4.0,Cash on Delivery
|
| 78 |
+
77,36,Male,55285,East,Electronics,500,4.0,Bank Transfer
|
| 79 |
+
78,31,Male,24945,North,Home & Kitchen,500,4.8,Bank Transfer
|
| 80 |
+
79,36,Female,39693,North,Clothing,500,4.0,Credit Card
|
| 81 |
+
80,18,Female,40591,North,Sports,500,4.8,Credit Card
|
| 82 |
+
81,32,Male,33392,East,Electronics,500,4.8,Credit Card
|
| 83 |
+
82,39,Male,62829,North,Clothing,500,4.0,Credit Card
|
| 84 |
+
83,52,Female,78911,South,Sports,500,4.0,Credit Card
|
| 85 |
+
84,28,Male,28967,East,Sports,500,4.8,Credit Card
|
| 86 |
+
85,25,Male,80378,South,Sports,500,4.8,Bank Transfer
|
| 87 |
+
86,28,Male,54678,East,Clothing,500,4.8,PayPal
|
| 88 |
+
87,45,Male,26997,North,Electronics,500,4.0,Credit Card
|
| 89 |
+
88,38,Male,60729,North,Books,500,4.0,Credit Card
|
| 90 |
+
89,28,Female,28764,East,Sports,500,4.8,PayPal
|
| 91 |
+
90,41,Male,78548,South,Home & Kitchen,500,4.0,Credit Card
|
| 92 |
+
91,36,Female,38149,East,Home & Kitchen,500,4.0,Bank Transfer
|
| 93 |
+
92,46,Male,66705,South,Clothing,500,4.0,Credit Card
|
| 94 |
+
93,26,Female,49099,North,Books,500,4.8,Credit Card
|
| 95 |
+
94,31,Female,63815,West,Electronics,500,4.8,Credit Card
|
| 96 |
+
95,30,Male,20994,East,Clothing,475,4.6,Bank Transfer
|
| 97 |
+
96,18,Female,20309,East,Clothing,412,3.7,Credit Card
|
| 98 |
+
97,38,Male,47501,North,Home & Kitchen,500,4.0,Bank Transfer
|
| 99 |
+
98,38,Female,43933,East,Clothing,500,4.0,Bank Transfer
|
| 100 |
+
99,35,Male,87809,South,Electronics,500,4.8,Credit Card
|
| 101 |
+
100,32,Male,56099,North,Home & Kitchen,500,4.8,Bank Transfer
|
| 102 |
+
101,18,Male,20000,West,Clothing,372,3.7,Bank Transfer
|
| 103 |
+
102,29,Female,61857,North,Home & Kitchen,500,4.8,Bank Transfer
|
| 104 |
+
103,30,Female,87443,West,Electronics,500,4.8,Bank Transfer
|
| 105 |
+
104,25,Female,58149,South,Home & Kitchen,500,4.8,Bank Transfer
|
| 106 |
+
105,33,Male,20000,North,Home & Kitchen,406,4.7,Bank Transfer
|
| 107 |
+
106,39,Female,48815,South,Electronics,500,4.0,Bank Transfer
|
| 108 |
+
107,57,Male,110838,West,Clothing,500,4.0,PayPal
|
| 109 |
+
108,37,Female,41346,East,Electronics,500,4.0,PayPal
|
| 110 |
+
109,38,Male,65876,West,Electronics,500,4.0,Bank Transfer
|
| 111 |
+
110,34,Male,66492,East,Sports,500,4.8,Bank Transfer
|
| 112 |
+
111,18,Male,20000,East,Clothing,441,3.7,Credit Card
|
| 113 |
+
112,34,Male,49809,West,Sports,500,4.8,Bank Transfer
|
| 114 |
+
113,35,Female,20000,North,Clothing,373,4.8,Bank Transfer
|
| 115 |
+
114,64,Male,75512,South,Books,500,4.0,Credit Card
|
| 116 |
+
115,32,Female,42948,North,Books,500,4.8,Bank Transfer
|
| 117 |
+
116,38,Male,32044,West,Home & Kitchen,500,4.0,Credit Card
|
| 118 |
+
117,34,Female,83648,West,Home & Kitchen,500,4.8,Credit Card
|
| 119 |
+
118,20,Male,20000,North,Clothing,423,3.8,Bank Transfer
|
| 120 |
+
119,48,Male,63199,East,Clothing,500,4.0,Credit Card
|
| 121 |
+
120,44,Female,68614,West,Sports,500,4.0,Bank Transfer
|
| 122 |
+
121,44,Male,94825,North,Sports,500,4.0,Bank Transfer
|
| 123 |
+
122,24,Male,20000,East,Sports,472,4.1,Credit Card
|
| 124 |
+
123,51,Female,99763,West,Electronics,500,4.0,Credit Card
|
| 125 |
+
124,18,Female,27204,North,Books,500,4.2,Credit Card
|
| 126 |
+
125,42,Male,43369,East,Sports,500,4.0,PayPal
|
| 127 |
+
126,61,Male,100742,North,Electronics,500,4.0,Cash on Delivery
|
| 128 |
+
127,23,Female,38481,West,Electronics,500,4.8,PayPal
|
| 129 |
+
128,28,Male,29995,West,Books,500,4.8,Credit Card
|
| 130 |
+
129,36,Male,55396,West,Home & Kitchen,500,4.0,Credit Card
|
| 131 |
+
130,28,Male,34293,South,Sports,500,4.8,PayPal
|
| 132 |
+
131,18,Female,29270,West,Electronics,500,4.3,Bank Transfer
|
| 133 |
+
132,35,Female,65742,West,Books,500,4.8,Credit Card
|
| 134 |
+
133,22,Male,64720,South,Clothing,500,4.8,Bank Transfer
|
| 135 |
+
134,40,Male,35243,West,Electronics,500,4.0,Credit Card
|
| 136 |
+
135,23,Male,77160,North,Electronics,500,4.8,Cash on Delivery
|
| 137 |
+
136,53,Female,40458,East,Sports,500,4.0,Credit Card
|
| 138 |
+
137,25,Male,34464,North,Books,500,4.8,Credit Card
|
| 139 |
+
138,31,Male,58266,North,Home & Kitchen,500,4.8,Cash on Delivery
|
| 140 |
+
139,44,Female,71619,North,Clothing,500,4.0,Credit Card
|
| 141 |
+
140,20,Male,20000,East,Sports,418,3.8,Cash on Delivery
|
| 142 |
+
141,37,Female,51337,South,Clothing,500,4.0,Credit Card
|
| 143 |
+
142,50,Male,65139,East,Sports,500,4.0,Credit Card
|
| 144 |
+
143,18,Male,20000,North,Clothing,357,3.7,Credit Card
|
| 145 |
+
144,37,Male,72492,South,Clothing,500,4.0,Bank Transfer
|
| 146 |
+
145,38,Female,64140,South,Home & Kitchen,500,4.0,Credit Card
|
| 147 |
+
146,44,Female,52141,East,Clothing,500,4.0,Credit Card
|
| 148 |
+
147,20,Female,47991,South,Books,500,4.8,Credit Card
|
| 149 |
+
148,19,Male,34645,East,Home & Kitchen,500,4.7,PayPal
|
| 150 |
+
149,41,Female,77757,West,Clothing,500,4.0,Credit Card
|
| 151 |
+
150,38,Male,69592,East,Sports,500,4.0,Bank Transfer
|
workspace/test/synthetic_data_analysis.png
ADDED
|