aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cursor/rules/project.mdc24
-rw-r--r--.gitignore49
-rw-r--r--Containerfile37
-rw-r--r--README.md184
-rw-r--r--app.py154
-rwxr-xr-xbuild.sh146
-rw-r--r--index.html116
-rw-r--r--logo.pngbin0 -> 1602427 bytes
-rw-r--r--requirements.txt2
-rw-r--r--script.js156
10 files changed, 868 insertions, 0 deletions
diff --git a/.cursor/rules/project.mdc b/.cursor/rules/project.mdc
new file mode 100644
index 0000000..2d0eae9
--- /dev/null
+++ b/.cursor/rules/project.mdc
@@ -0,0 +1,24 @@
1---
2description:
3globs:
4alwaysApply: true
5---
6bonsai is a python command line tool used to generate realistic network latency graphs using a yaml config.
7
8The tool is called with
9```
10python -m main --net_config <network-config>.yaml --output_dir <output_dir>
11```
12The network config is a yaml file whose contents don't really matter and the output directory must exist and will be populated with two files `edges.csv` and `nodes.csv`.
13
14The goal of this project, `bonsai-web` is to create a simple web interface for bonsai since it requires a specific version of python and quite a few dependencies.
15You can assume that the bonsai source code is under the directory `/usr/src/bonsai` so you use subprocess in that directory to execute the command above.
16The web page we are trying to create is simple and will contain the following components.
17+ single html page using picocss
18+ single javascript file for client side logic
19+ simple flask backend
20
21The html page should have a header with the name bonsai and an image logo. The logo will be present at `logo.png`. The page should have some description text at the top, followed by a text box for the user to provide a configuration and then a button to generate the output files. When the user clicks that button a POST request with the contents of the config should be made to `/` and the server should execute bonsai and return a json object where the keys are the names of the generated files and the values associated with those keys will be the contents of the files.
22The page should then display, bellow the button, the list of files with a button to download and one to view. The javascript file should contain the code for all this logic.
23
24There should also be a `Containerfile` that creates a container image for this project. Make sure to use python version 3.9 as the base image and leave some space for me to write the code required to setup the bonsai project, you just need to assume that it will be present at the above location. \ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..77e4615
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
1# Python
2__pycache__/
3*.py[cod]
4*$py.class
5*.so
6.Python
7build/
8develop-eggs/
9dist/
10downloads/
11eggs/
12.eggs/
13lib/
14lib64/
15parts/
16sdist/
17var/
18wheels/
19*.egg-info/
20.installed.cfg
21*.egg
22MANIFEST
23
24# Virtual Environment
25venv/
26env/
27ENV/
28
29# IDE
30.vscode/
31.idea/
32*.swp
33*.swo
34
35# OS
36.DS_Store
37Thumbs.db
38
39# Flask
40instance/
41.webassets-cache
42
43# Environment variables
44.env
45.env.local
46
47# Container artifacts
48.buildah/
49.containerignore \ No newline at end of file
diff --git a/Containerfile b/Containerfile
new file mode 100644
index 0000000..c8ff201
--- /dev/null
+++ b/Containerfile
@@ -0,0 +1,37 @@
1FROM python:3.9-slim
2
3# Set working directory
4WORKDIR /app
5
6# Install system dependencies
7RUN apt-get update && apt-get install -y \
8 git \
9 && rm -rf /var/lib/apt/lists/*
10
11# Copy requirements and install Python dependencies
12COPY requirements.txt .
13RUN pip install --no-cache-dir -r requirements.txt
14
15# === BONSAI SETUP SECTION ===
16WORKDIR /usr/src/bonsai
17RUN git clone https://codelab.fct.unl.pt/di/computer-systems/bonsai . && \
18 pip install --no-cache-dir 'torch~=2.0.1' && \
19 pip install --no-cache-dir -r requirements.txt
20# === END BONSAI SETUP SECTION ===
21
22# Copy application files
23COPY app.py .
24COPY index.html .
25COPY script.js .
26COPY logo.png .
27
28# Expose the port
29EXPOSE 5000
30
31# Set environment variables
32ENV FLASK_APP=app.py
33ENV FLASK_ENV=production
34ENV BONSAI_TEST_MODE=false
35
36# Run the application
37CMD ["python", "app.py"] \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1467530
--- /dev/null
+++ b/README.md
@@ -0,0 +1,184 @@
1# Bonsai Web
2
3A simple web interface for the Bonsai network latency graph generator.
4
5## Features
6
7- Clean, modern UI using Pico CSS
8- YAML configuration input
9- Real-time file generation
10- File viewing and downloading capabilities
11- Containerized deployment support
12
13## Local Development
14
15### Prerequisites
16
17- Python 3.9+
18- Flask
19
20### Installation
21
221. Clone this repository
232. Install dependencies:
24 ```bash
25 pip install -r requirements.txt
26 ```
27
28### Running Locally
29
30```bash
31python app.py
32```
33
34The application will be available at `http://localhost:5000`
35
36### Environment Configuration
37
38The application behavior can be controlled using the `BONSAI_TEST_MODE` environment variable:
39
40- **Test Mode** (default for local development): `BONSAI_TEST_MODE=true`
41 - If Bonsai fails or is not available, dummy files are generated for testing
42 - Useful for development and testing the web interface
43
44- **Production Mode**: `BONSAI_TEST_MODE=false`
45 - If Bonsai fails, proper error messages are returned to the user
46 - No dummy files are generated
47
48```bash
49# Run in production mode
50BONSAI_TEST_MODE=false python app.py
51
52# Run in test mode (default)
53BONSAI_TEST_MODE=true python app.py
54# or simply
55python app.py
56```
57
58### Development Mode
59
60For development, the application includes fallback dummy data generation when `BONSAI_TEST_MODE=true` and the actual Bonsai tool is not available. This allows you to test the web interface without having the full Bonsai installation.
61
62**Error Handling**: When Bonsai fails in production mode, detailed error messages are displayed to the user in red text, including:
63- Execution failures with return codes and stderr output
64- Timeout errors (30-second limit)
65- Missing Bonsai installation errors
66
67## Container Deployment
68
69### Building the Container
70
71#### Local Build (for testing)
72```bash
73./build.sh
74```
75
76#### Production Build and Push
77```bash
78PUSH=1 ./build.sh
79```
80
81The build script will:
82- Always build the container image with appropriate tags
83- Only push to `git.d464.sh/diogo464/bonsai-web` when `PUSH=1` is set
84- Use git tags for versioning (or 'latest' if no tag)
85- Check registry authentication only when pushing
86
87#### Manual Build Commands
88
89```bash
90# Local build
91podman build -t bonsai-web -f Containerfile .
92
93# Production build and push
94podman build -t git.d464.sh/diogo464/bonsai-web:latest -f Containerfile .
95podman push git.d464.sh/diogo464/bonsai-web:latest
96```
97
98### Running the Container
99
100#### From Local Build
101```bash
102podman run -p 5000:5000 bonsai-web:local
103```
104
105#### From Registry
106```bash
107podman run -p 5000:5000 git.d464.sh/diogo464/bonsai-web:latest
108```
109
110### Registry Authentication
111
112Before pushing to the registry, authenticate with:
113```bash
114podman login git.d464.sh
115```
116
117### Bonsai Integration
118
119To integrate with the actual Bonsai tool, modify the `Containerfile` in the "BONSAI SETUP SECTION" to:
120
1211. Clone or copy the Bonsai source code to `/usr/src/bonsai`
1222. Install Bonsai's dependencies
1233. Ensure the Bonsai tool is properly configured
124
125Example:
126```dockerfile
127# In the BONSAI SETUP SECTION of Containerfile
128RUN git clone https://github.com/your-org/bonsai.git /usr/src/bonsai
129WORKDIR /usr/src/bonsai
130RUN pip install -r requirements.txt
131WORKDIR /app
132```
133
134## Usage
135
1361. Open the web application in your browser
1372. Enter your network configuration in YAML format in the text area
1383. Click "Generate Network Files"
1394. View or download the generated `edges.csv` and `nodes.csv` files
140
141### Example Configuration
142
143```yaml
144network:
145 nodes: 10
146 density: 0.3
147 latency_range: [1, 100]
148```
149
150## API
151
152### POST /
153
154Generates network files from a YAML configuration.
155
156**Request Body:**
157```json
158{
159 "config": "network:\n nodes: 10\n density: 0.3"
160}
161```
162
163**Response:**
164```json
165{
166 "edges.csv": "source,target,weight\nnode1,node2,0.5\n...",
167 "nodes.csv": "id,label,x,y\nnode1,Node 1,0,0\n..."
168}
169```
170
171## Project Structure
172
173```
174.
175├── app.py # Flask backend
176├── index.html # Main web page
177├── script.js # Client-side JavaScript
178├── logo.png # Bonsai logo
179├── requirements.txt # Python dependencies
180├── Containerfile # Container build instructions
181├── build.sh # Container build script
182├── .gitignore # Git ignore patterns
183└── README.md # This file
184``` \ No newline at end of file
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..002957a
--- /dev/null
+++ b/app.py
@@ -0,0 +1,154 @@
1import os
2import subprocess
3import tempfile
4import json
5from flask import Flask, request, jsonify, send_from_directory
6
7app = Flask(__name__)
8
9# Check if we're in test environment (defaults to True for local development)
10TEST_ENVIRONMENT = os.getenv("BONSAI_TEST_MODE", "true").lower() == "true"
11
12
13@app.route("/")
14def index():
15 """Serve the main HTML page."""
16 return send_from_directory(".", "index.html")
17
18
19@app.route("/logo.png")
20def logo():
21 """Serve the logo image."""
22 return send_from_directory(".", "logo.png")
23
24
25@app.route("/script.js")
26def script():
27 """Serve the JavaScript file."""
28 return send_from_directory(".", "script.js")
29
30
31@app.route("/", methods=["POST"])
32def generate_files():
33 """Handle configuration submission and execute bonsai."""
34 try:
35 # Get the configuration from the request
36 data = request.get_json()
37 if not data or "config" not in data:
38 return jsonify({"error": "No configuration provided"}), 400
39
40 config_content = data["config"]
41
42 # Create temporary directory for output
43 with tempfile.TemporaryDirectory() as temp_dir:
44 # Write config to temporary file
45 config_file = os.path.join(temp_dir, "config.yaml")
46 with open(config_file, "w") as f:
47 f.write(config_content)
48
49 # Create output directory
50 output_dir = os.path.join(temp_dir, "output")
51 os.makedirs(output_dir, exist_ok=True)
52
53 # Execute bonsai command
54 cmd = [
55 "python",
56 "-m",
57 "main",
58 "--net_config",
59 config_file,
60 "--output_dir",
61 output_dir,
62 ]
63
64 # Determine bonsai directory
65 bonsai_dir = "/usr/src/bonsai" if os.path.exists("/usr/src/bonsai") else "."
66
67 try:
68 result = subprocess.run(
69 cmd, cwd=bonsai_dir, capture_output=True, text=True, timeout=30
70 )
71
72 if result.returncode != 0:
73 error_msg = (
74 f"Bonsai execution failed with return code {result.returncode}"
75 )
76 if result.stderr:
77 error_msg += f": {result.stderr.strip()}"
78
79 # Only create dummy files in test environment
80 if TEST_ENVIRONMENT:
81 print(
82 f"WARNING: {error_msg}. Creating dummy files for testing."
83 )
84 edges_content = (
85 "source,target,weight\nnode1,node2,0.5\nnode2,node3,0.3\n"
86 )
87 nodes_content = "id,label,x,y\nnode1,Node 1,0,0\nnode2,Node 2,1,1\nnode3,Node 3,2,0\n"
88
89 with open(os.path.join(output_dir, "edges.csv"), "w") as f:
90 f.write(edges_content)
91 with open(os.path.join(output_dir, "nodes.csv"), "w") as f:
92 f.write(nodes_content)
93 else:
94 return jsonify({"error": error_msg}), 500
95
96 except subprocess.TimeoutExpired:
97 error_msg = "Bonsai execution timed out after 30 seconds"
98 if TEST_ENVIRONMENT:
99 print(f"WARNING: {error_msg}. Creating dummy files for testing.")
100 edges_content = (
101 "source,target,weight\nnode1,node2,0.5\nnode2,node3,0.3\n"
102 )
103 nodes_content = "id,label,x,y\nnode1,Node 1,0,0\nnode2,Node 2,1,1\nnode3,Node 3,2,0\n"
104
105 with open(os.path.join(output_dir, "edges.csv"), "w") as f:
106 f.write(edges_content)
107 with open(os.path.join(output_dir, "nodes.csv"), "w") as f:
108 f.write(nodes_content)
109 else:
110 return jsonify({"error": error_msg}), 500
111
112 except FileNotFoundError:
113 error_msg = "Bonsai tool not found. Please ensure Bonsai is installed and available."
114 if TEST_ENVIRONMENT:
115 print(f"WARNING: {error_msg}. Creating dummy files for testing.")
116 edges_content = (
117 "source,target,weight\nnode1,node2,0.5\nnode2,node3,0.3\n"
118 )
119 nodes_content = "id,label,x,y\nnode1,Node 1,0,0\nnode2,Node 2,1,1\nnode3,Node 3,2,0\n"
120
121 with open(os.path.join(output_dir, "edges.csv"), "w") as f:
122 f.write(edges_content)
123 with open(os.path.join(output_dir, "nodes.csv"), "w") as f:
124 f.write(nodes_content)
125 else:
126 return jsonify({"error": error_msg}), 500
127
128 # Read generated files and return their contents
129 files = {}
130 for filename in ["edges.csv", "nodes.csv"]:
131 filepath = os.path.join(output_dir, filename)
132 if os.path.exists(filepath):
133 with open(filepath, "r") as f:
134 files[filename] = f.read()
135
136 if not files:
137 return (
138 jsonify({"error": "No output files were generated by Bonsai"}),
139 500,
140 )
141
142 # Files are automatically cleaned up when temp_dir context exits
143 return jsonify(files)
144
145 except Exception as e:
146 return jsonify({"error": f"Unexpected error: {str(e)}"}), 500
147
148
149if __name__ == "__main__":
150 if TEST_ENVIRONMENT:
151 print("Running in TEST mode - dummy files will be generated if Bonsai fails")
152 else:
153 print("Running in PRODUCTION mode - errors will be returned if Bonsai fails")
154 app.run(debug=True, host="0.0.0.0", port=5000)
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..df3d28a
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,146 @@
1#!/bin/bash
2
3# Container registry configuration
4REGISTRY="git.d464.sh"
5NAMESPACE="diogo464"
6IMAGE_NAME="bonsai-web"
7FULL_IMAGE_NAME="${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}"
8
9# Get version from git tag or use 'latest'
10VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "latest")
11
12# Check if we should push to registry
13SHOULD_PUSH=${PUSH:-0}
14
15# Colors for output
16RED='\033[0;31m'
17GREEN='\033[0;32m'
18YELLOW='\033[1;33m'
19BLUE='\033[0;34m'
20NC='\033[0m' # No Color
21
22# Logging functions
23log_info() {
24 echo -e "${BLUE}[INFO]${NC} $1"
25}
26
27log_success() {
28 echo -e "${GREEN}[SUCCESS]${NC} $1"
29}
30
31log_warning() {
32 echo -e "${YELLOW}[WARNING]${NC} $1"
33}
34
35log_error() {
36 echo -e "${RED}[ERROR]${NC} $1"
37}
38
39# Error handler
40handle_error() {
41 log_error "Script failed at line $1"
42 exit 1
43}
44
45trap 'handle_error $LINENO' ERR
46set -e
47
48if [[ "$SHOULD_PUSH" == "1" ]]; then
49 log_info "Starting container build and push process..."
50 log_info "Registry: ${REGISTRY}"
51 log_info "Image: ${FULL_IMAGE_NAME}"
52else
53 log_info "Starting container build process (local only)..."
54 log_info "Image: ${IMAGE_NAME}"
55fi
56log_info "Version: ${VERSION}"
57
58# Check if podman is available
59if ! command -v podman &> /dev/null; then
60 log_error "podman is not installed or not in PATH"
61 exit 1
62fi
63
64# Check if we're in the right directory
65if [[ ! -f "Containerfile" ]]; then
66 log_error "Containerfile not found. Please run this script from the project root."
67 exit 1
68fi
69
70# Check if we're logged into the registry (only if pushing)
71if [[ "$SHOULD_PUSH" == "1" ]]; then
72 log_info "Checking registry authentication..."
73 if ! podman login --get-login "${REGISTRY}" &> /dev/null; then
74 log_warning "Not logged into ${REGISTRY}. Please login first:"
75 echo "podman login ${REGISTRY}"
76 exit 1
77 fi
78fi
79
80# Build the container image
81log_info "Building container image..."
82if [[ "$SHOULD_PUSH" == "1" ]]; then
83 # Build with registry tags for pushing
84 podman build \
85 -f Containerfile \
86 -t "${FULL_IMAGE_NAME}:${VERSION}" \
87 -t "${FULL_IMAGE_NAME}:latest" \
88 -t "${IMAGE_NAME}:local" \
89 .
90else
91 # Build with local tags only
92 podman build \
93 -f Containerfile \
94 -t "${IMAGE_NAME}:${VERSION}" \
95 -t "${IMAGE_NAME}:latest" \
96 -t "${IMAGE_NAME}:local" \
97 .
98fi
99
100log_success "Container image built successfully"
101
102if [[ "$SHOULD_PUSH" == "1" ]]; then
103 # Push the versioned tag
104 log_info "Pushing ${FULL_IMAGE_NAME}:${VERSION}..."
105 podman push "${FULL_IMAGE_NAME}:${VERSION}"
106 log_success "Pushed ${FULL_IMAGE_NAME}:${VERSION}"
107
108 # Push the latest tag (only if version is not 'latest')
109 if [[ "${VERSION}" != "latest" ]]; then
110 log_info "Pushing ${FULL_IMAGE_NAME}:latest..."
111 podman push "${FULL_IMAGE_NAME}:latest"
112 log_success "Pushed ${FULL_IMAGE_NAME}:latest"
113 fi
114fi
115
116# Display final information
117echo
118if [[ "$SHOULD_PUSH" == "1" ]]; then
119 log_success "Build and push completed successfully!"
120 echo
121 echo "Image details:"
122 echo " Registry: ${REGISTRY}"
123 echo " Full name: ${FULL_IMAGE_NAME}"
124 echo " Version: ${VERSION}"
125 echo
126 echo "To run the container:"
127 echo " podman run -p 5000:5000 ${FULL_IMAGE_NAME}:${VERSION}"
128 echo
129 echo "To pull on another machine:"
130 echo " podman pull ${FULL_IMAGE_NAME}:${VERSION}"
131else
132 log_success "Local build completed!"
133 echo
134 echo "Image details:"
135 echo " Name: ${IMAGE_NAME}"
136 echo " Version: ${VERSION}"
137 echo
138 echo "To run the container:"
139 echo " podman run -p 5000:5000 ${IMAGE_NAME}:${VERSION}"
140 echo
141 echo "To test the container:"
142 echo " podman run --rm -p 5000:5000 ${IMAGE_NAME}:${VERSION}"
143 echo
144 echo "To push to registry:"
145 echo " PUSH=1 $0"
146fi \ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..fff715a
--- /dev/null
+++ b/index.html
@@ -0,0 +1,116 @@
1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title>Bonsai - Network Latency Graph Generator</title>
8 <link rel="stylesheet" href="https://unpkg.com/@picocss/[email protected]/css/pico.min.css">
9 <style>
10 .logo-container {
11 display: flex;
12 align-items: center;
13 gap: 1rem;
14 margin-bottom: 2rem;
15 }
16
17 .logo {
18 height: 60px;
19 width: auto;
20 }
21
22 .file-item {
23 margin-bottom: 1rem;
24 padding: 1rem;
25 border: 1px solid var(--border-color);
26 border-radius: var(--border-radius);
27 }
28
29 .file-buttons {
30 display: flex;
31 gap: 0.5rem;
32 margin-top: 0.5rem;
33 }
34
35 .file-content {
36 margin-top: 1rem;
37 padding: 1rem;
38 background-color: var(--code-background-color);
39 border-radius: var(--border-radius);
40 white-space: pre;
41 font-family: monospace;
42 font-size: 0.9rem;
43 max-height: 300px;
44 overflow-y: auto;
45 display: none;
46 }
47
48 .error {
49 color: var(--color-red);
50 background-color: var(--color-red-background);
51 padding: 1rem;
52 border-radius: var(--border-radius);
53 margin-top: 1rem;
54 }
55
56 .loading {
57 text-align: center;
58 margin-top: 1rem;
59 }
60 </style>
61</head>
62
63<body>
64 <main class="container">
65 <header>
66 <div class="logo-container">
67 <img src="logo.png" alt="Bonsai Logo" class="logo">
68 <h1>Bonsai</h1>
69 </div>
70 </header>
71
72 <section>
73 <p>
74 Bonsai is a powerful tool for generating realistic network latency graphs.
75 This web interface provides an easy way to use the
76 <a href="https://codelab.fct.unl.pt/di/computer-systems/bonsai" target="_blank">Bonsai project</a>
77 without needing to install its dependencies locally.
78 Simply provide your network configuration in YAML format below, and click
79 "Generate" to create your network topology files.
80 </p>
81
82 <p>
83 The tool will generate two CSV files: <code>edges.csv</code> containing
84 the network connections and weights, and <code>nodes.csv</code> containing
85 the node information and positions.
86 </p>
87 </section>
88
89 <section>
90 <label for="config">Network Configuration (YAML):</label>
91 <textarea id="config" name="config" rows="10">nodes: 15
92continents:
93 EU: 5
94 AS: 5
95countries:
96 DE: 2
97 FR: 2
98 CN: 2
99 JP: 2</textarea>
100
101 <button id="generateBtn" onclick="generateFiles()">Generate Network Files</button>
102
103 <div id="loading" class="loading" style="display: none;">
104 <p aria-busy="true">Generating network files...</p>
105 </div>
106
107 <div id="error" class="error" style="display: none;"></div>
108
109 <div id="results" style="margin-top: 2rem;"></div>
110 </section>
111 </main>
112
113 <script src="script.js"></script>
114</body>
115
116</html> \ No newline at end of file
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..fb6e55d
--- /dev/null
+++ b/logo.png
Binary files differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..c4988fb
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
1Flask==2.3.3
2Werkzeug==2.3.7 \ No newline at end of file
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..5bc60b0
--- /dev/null
+++ b/script.js
@@ -0,0 +1,156 @@
1let generatedFiles = {};
2
3async function generateFiles() {
4 const configTextarea = document.getElementById('config');
5 const generateBtn = document.getElementById('generateBtn');
6 const loadingDiv = document.getElementById('loading');
7 const errorDiv = document.getElementById('error');
8 const resultsDiv = document.getElementById('results');
9
10 const config = configTextarea.value.trim();
11
12 if (!config) {
13 showError('Please enter a configuration before generating files.');
14 return;
15 }
16
17 // Reset UI state
18 hideError();
19 hideResults();
20 showLoading();
21 generateBtn.disabled = true;
22
23 try {
24 const response = await fetch('/', {
25 method: 'POST',
26 headers: {
27 'Content-Type': 'application/json',
28 },
29 body: JSON.stringify({ config: config })
30 });
31
32 const data = await response.json();
33
34 if (!response.ok) {
35 throw new Error(data.error || 'Failed to generate files');
36 }
37
38 // Store the generated files
39 generatedFiles = data;
40
41 // Display the results
42 displayResults(data);
43
44 } catch (error) {
45 console.error('Error generating files:', error);
46 showError('Error generating files: ' + error.message);
47 } finally {
48 hideLoading();
49 generateBtn.disabled = false;
50 }
51}
52
53function displayResults(files) {
54 const resultsDiv = document.getElementById('results');
55
56 if (Object.keys(files).length === 0) {
57 showError('No files were generated.');
58 return;
59 }
60
61 let html = '<h3>Generated Files</h3>';
62
63 for (const [filename, content] of Object.entries(files)) {
64 html += `
65 <div class="file-item">
66 <h4>${filename}</h4>
67 <p>Size: ${content.length} characters</p>
68 <div class="file-buttons">
69 <button onclick="toggleFileContent('${filename}')" id="view-${filename}">
70 View Content
71 </button>
72 <button onclick="downloadFile('${filename}')" class="secondary">
73 Download
74 </button>
75 </div>
76 <div id="content-${filename}" class="file-content">${escapeHtml(content)}</div>
77 </div>
78 `;
79 }
80
81 resultsDiv.innerHTML = html;
82 resultsDiv.style.display = 'block';
83}
84
85function toggleFileContent(filename) {
86 const contentDiv = document.getElementById(`content-${filename}`);
87 const viewBtn = document.getElementById(`view-${filename}`);
88
89 if (contentDiv.style.display === 'none' || contentDiv.style.display === '') {
90 contentDiv.style.display = 'block';
91 viewBtn.textContent = 'Hide Content';
92 } else {
93 contentDiv.style.display = 'none';
94 viewBtn.textContent = 'View Content';
95 }
96}
97
98function downloadFile(filename) {
99 if (!generatedFiles[filename]) {
100 showError('File not found: ' + filename);
101 return;
102 }
103
104 const content = generatedFiles[filename];
105 const blob = new Blob([content], { type: 'text/csv' });
106 const url = window.URL.createObjectURL(blob);
107
108 const a = document.createElement('a');
109 a.href = url;
110 a.download = filename;
111 document.body.appendChild(a);
112 a.click();
113
114 // Clean up
115 window.URL.revokeObjectURL(url);
116 document.body.removeChild(a);
117}
118
119function showError(message) {
120 const errorDiv = document.getElementById('error');
121 errorDiv.textContent = message;
122 errorDiv.style.display = 'block';
123}
124
125function hideError() {
126 const errorDiv = document.getElementById('error');
127 errorDiv.style.display = 'none';
128}
129
130function showLoading() {
131 const loadingDiv = document.getElementById('loading');
132 loadingDiv.style.display = 'block';
133}
134
135function hideLoading() {
136 const loadingDiv = document.getElementById('loading');
137 loadingDiv.style.display = 'none';
138}
139
140function hideResults() {
141 const resultsDiv = document.getElementById('results');
142 resultsDiv.style.display = 'none';
143}
144
145function escapeHtml(text) {
146 const div = document.createElement('div');
147 div.textContent = text;
148 return div.innerHTML;
149}
150
151// Add Enter key support for the textarea (Ctrl+Enter to generate)
152document.getElementById('config').addEventListener('keydown', function (event) {
153 if (event.ctrlKey && event.key === 'Enter') {
154 generateFiles();
155 }
156}); \ No newline at end of file