HusainHG commited on
Commit
0481514
·
verified ·
1 Parent(s): 6d4104d

Upload 8 files

Browse files
Files changed (8) hide show
  1. .gitignore +24 -0
  2. Dockerfile +13 -0
  3. README.md +180 -10
  4. app.py +84 -0
  5. requirements.txt +5 -0
  6. static/app.js +101 -0
  7. static/index.html +60 -0
  8. static/style.css +211 -0
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ venv/
8
+ env/
9
+ ENV/
10
+ .venv
11
+
12
+ # Hugging Face
13
+ .cache/
14
+ flagged/
15
+
16
+ # IDE
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+ .DS_Store
22
+
23
+ # Misc
24
+ *.log
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY app.py .
9
+ COPY static ./static
10
+
11
+ EXPOSE 7860
12
+
13
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,180 @@
1
- ---
2
- title: Mistral Api
3
- emoji: 🚀
4
- colorFrom: pink
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Mistral Fine-tuned Model
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ ---
9
+
10
+ # 🤖 Mistral Fine-tuned Model
11
+
12
+ Flask API with separate HTML/CSS/JS frontend for `KASHH-4/mistral_fine-tuned` model.
13
+
14
+ ## 🚀 What This Is
15
+
16
+ A **Flask API server** with **separate frontend files**:
17
+ - Backend: Python Flask with CORS
18
+ - Frontend: HTML + CSS + JavaScript
19
+ - Clean separation of concerns
20
+ - API-first design
21
+
22
+ ## 📁 Project Structure
23
+
24
+ ```
25
+ e:\EDI\hf-node-app\
26
+ ├── app.py # Main Gradio application
27
+ ├── requirements.txt # Python dependencies
28
+ ├── README.md # This file
29
+ └── .gitignore # Git ignore rules
30
+ ```
31
+
32
+ ## 🔧 Deploy to Hugging Face Spaces
33
+
34
+ ### Step 1: Create a Space
35
+
36
+ 1. Go to https://huggingface.co/spaces
37
+ 2. Click **"Create new Space"**
38
+ 3. Configure:
39
+ - **Owner:** KASHH-4 (or your account)
40
+ - **Space name:** `mistral-api` (or any name)
41
+ - **SDK:** Gradio
42
+ - **Hardware:** CPU basic (Free)
43
+ - **Visibility:** Public
44
+ 4. Click **"Create Space"**
45
+
46
+ ### Step 2: Upload Files
47
+
48
+ Upload these 3 files to your Space:
49
+ - `app.py`
50
+ - `requirements.txt`
51
+ - `README.md` (optional)
52
+
53
+ **Via Web UI:**
54
+ 1. Click "Files" tab
55
+ 2. Click "Add file" → "Upload files"
56
+ 3. Drag and drop the files
57
+ 4. Commit changes
58
+
59
+ **Via Git:**
60
+ ```bash
61
+ git init
62
+ git remote add origin https://huggingface.co/spaces/KASHH-4/mistral-api
63
+ git add app.py requirements.txt README.md .gitignore
64
+ git commit -m "Initial deployment"
65
+ git push origin main
66
+ ```
67
+
68
+ ### Step 3: Wait for Deployment
69
+
70
+ - First build takes 5-10 minutes
71
+ - Watch the logs for "Running on..."
72
+ - Your Space will be live at: `https://kashh-4-mistral-api.hf.space`
73
+
74
+ ## 🧪 Test Your Space
75
+
76
+ ### Web Interface
77
+ Visit: `https://huggingface.co/spaces/KASHH-4/mistral-api`
78
+
79
+ ### API Endpoint
80
+ ```bash
81
+ curl -X POST "https://kashh-4-mistral-api.hf.space/api/predict" \
82
+ -H "Content-Type: application/json" \
83
+ -d '{"data":["Hello, how are you?"]}'
84
+ ```
85
+
86
+ ### From JavaScript/Node.js
87
+ ```javascript
88
+ const response = await fetch('https://kashh-4-mistral-api.hf.space/api/predict', {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ data: ["Your prompt here"] })
92
+ });
93
+
94
+ const result = await response.json();
95
+ console.log(result.data[0]); // Generated text
96
+ ```
97
+
98
+ ### From Python
99
+ ```python
100
+ import requests
101
+
102
+ response = requests.post(
103
+ 'https://kashh-4-mistral-api.hf.space/api/predict',
104
+ json={'data': ['Your prompt here']}
105
+ )
106
+
107
+ print(response.json()['data'][0])
108
+ ```
109
+
110
+ ## 💰 Cost
111
+
112
+ **100% FREE** on HF Spaces:
113
+ - Free CPU tier (slower, ~10-30 sec per request)
114
+ - Sleeps after 48h inactivity (30 sec wake-up)
115
+ - Perfect for demos, personal projects, testing
116
+
117
+ **Optional Upgrades:**
118
+ - GPU T4 Small: $0.60/hour (much faster, 2-5 sec)
119
+ - GPU A10G: $3.15/hour (very fast, 1-2 sec)
120
+
121
+ Upgrade in: Space Settings → Hardware
122
+
123
+ ## 🔧 Local Testing (Optional)
124
+
125
+ If you have Python installed and want to test locally before deploying:
126
+
127
+ ```bash
128
+ # Install dependencies
129
+ pip install -r requirements.txt
130
+
131
+ # Run locally
132
+ python app.py
133
+
134
+ # Visit: http://localhost:7860
135
+ ```
136
+
137
+ **Requirements:**
138
+ - Python 3.9+
139
+ - 16GB+ RAM (for model loading)
140
+ - GPU recommended but not required
141
+
142
+ ## 📋 Model Configuration
143
+
144
+ The app is configured for `KASHH-4/mistral_fine-tuned`. To use a different model, edit `app.py`:
145
+
146
+ ```python
147
+ MODEL_NAME = "your-org/your-model"
148
+ ```
149
+
150
+ ## 🆘 Troubleshooting
151
+
152
+ **Space stuck on "Building":**
153
+ - Check logs for errors
154
+ - Model might be too large for free CPU
155
+ - Try: Restart Space in Settings
156
+
157
+ **Space shows "Runtime Error":**
158
+ - Check if model exists and is public
159
+ - Verify model format is compatible with transformers
160
+ - Try smaller model first to test
161
+
162
+ **Slow responses:**
163
+ - Normal on free CPU tier
164
+ - Upgrade to GPU for faster inference
165
+ - Or use smaller model
166
+
167
+ ## 📞 Support
168
+
169
+ Issues? Check the deployment guide in `huggingface-space/DEPLOYMENT-GUIDE.md`
170
+
171
+ ---
172
+
173
+ ## 🗑️ Cleanup Old Files
174
+
175
+ If you followed earlier Node.js instructions, delete unnecessary files:
176
+
177
+ See `CLEANUP.md` for full list of files to remove.
178
+
179
+ ## License
180
+ MIT
app.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ from flask_cors import CORS
3
+ from transformers import AutoModelForCausalLM, AutoTokenizer
4
+ import torch
5
+ import os
6
+
7
+ app = Flask(__name__, static_folder='static')
8
+ CORS(app)
9
+
10
+ MODEL_NAME = "KASHH-4/mistral_fine-tuned"
11
+ print(f"Loading model: {MODEL_NAME}")
12
+
13
+ print("Loading tokenizer...")
14
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
15
+
16
+ if tokenizer.pad_token is None:
17
+ tokenizer.pad_token = tokenizer.eos_token
18
+
19
+ print("Tokenizer loaded successfully!")
20
+
21
+ print("Loading model weights...")
22
+ model = AutoModelForCausalLM.from_pretrained(
23
+ MODEL_NAME,
24
+ torch_dtype=torch.float16,
25
+ device_map="auto",
26
+ low_cpu_mem_usage=True
27
+ )
28
+ print("Model loaded successfully!")
29
+
30
+
31
+ @app.route('/')
32
+ def index():
33
+ return send_from_directory('static', 'index.html')
34
+
35
+
36
+ @app.route('/api/generate', methods=['POST'])
37
+ def generate():
38
+ try:
39
+ data = request.json
40
+
41
+ if not data or 'prompt' not in data:
42
+ return jsonify({'error': 'Missing prompt in request body'}), 400
43
+
44
+ prompt = data['prompt']
45
+ max_new_tokens = data.get('max_new_tokens', 256)
46
+ temperature = data.get('temperature', 0.7)
47
+ top_p = data.get('top_p', 0.9)
48
+
49
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
50
+
51
+ with torch.no_grad():
52
+ outputs = model.generate(
53
+ **inputs,
54
+ max_new_tokens=max_new_tokens,
55
+ temperature=temperature,
56
+ top_p=top_p,
57
+ do_sample=True,
58
+ pad_token_id=tokenizer.eos_token_id
59
+ )
60
+
61
+ generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
62
+
63
+ return jsonify({
64
+ 'generated_text': generated_text,
65
+ 'prompt': prompt
66
+ })
67
+
68
+ except Exception as e:
69
+ print(f"Error during generation: {e}")
70
+ return jsonify({'error': str(e)}), 500
71
+
72
+
73
+ @app.route('/api/health', methods=['GET'])
74
+ def health():
75
+ return jsonify({
76
+ 'status': 'ok',
77
+ 'model': MODEL_NAME,
78
+ 'device': str(model.device)
79
+ })
80
+
81
+
82
+ if __name__ == '__main__':
83
+ port = int(os.environ.get('PORT', 7860))
84
+ app.run(host='0.0.0.0', port=port, debug=False)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flask==3.0.0
2
+ flask-cors==4.0.0
3
+ transformers==4.36.2
4
+ torch==2.1.2
5
+ accelerate==0.25.0
static/app.js ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API Configuration
2
+ const API_URL = window.location.origin; // Use same origin (works locally and on HF Spaces)
3
+
4
+ // DOM Elements
5
+ const promptEl = document.getElementById('prompt');
6
+ const generateBtn = document.getElementById('generateBtn');
7
+ const statusEl = document.getElementById('status');
8
+ const outputEl = document.getElementById('output');
9
+
10
+ const maxTokensEl = document.getElementById('maxTokens');
11
+ const temperatureEl = document.getElementById('temperature');
12
+ const topPEl = document.getElementById('topP');
13
+
14
+ const maxTokensValueEl = document.getElementById('maxTokensValue');
15
+ const temperatureValueEl = document.getElementById('temperatureValue');
16
+ const topPValueEl = document.getElementById('topPValue');
17
+
18
+ // Update slider value displays
19
+ maxTokensEl.addEventListener('input', (e) => {
20
+ maxTokensValueEl.textContent = e.target.value;
21
+ });
22
+
23
+ temperatureEl.addEventListener('input', (e) => {
24
+ temperatureValueEl.textContent = parseFloat(e.target.value).toFixed(1);
25
+ });
26
+
27
+ topPEl.addEventListener('input', (e) => {
28
+ topPValueEl.textContent = parseFloat(e.target.value).toFixed(2);
29
+ });
30
+
31
+ // Generate button click handler
32
+ generateBtn.addEventListener('click', async () => {
33
+ const prompt = promptEl.value.trim();
34
+
35
+ if (!prompt) {
36
+ outputEl.textContent = 'Please enter a prompt';
37
+ outputEl.className = 'output error';
38
+ return;
39
+ }
40
+
41
+ // Disable button and show loading
42
+ generateBtn.disabled = true;
43
+ statusEl.textContent = '⏳';
44
+ outputEl.textContent = 'Generating...';
45
+ outputEl.className = 'output';
46
+
47
+ try {
48
+ const response = await fetch(`${API_URL}/api/generate`, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ body: JSON.stringify({
54
+ prompt: prompt,
55
+ max_new_tokens: parseInt(maxTokensEl.value),
56
+ temperature: parseFloat(temperatureEl.value),
57
+ top_p: parseFloat(topPEl.value)
58
+ })
59
+ });
60
+
61
+ const data = await response.json();
62
+
63
+ if (!response.ok) {
64
+ throw new Error(data.error || `HTTP ${response.status}`);
65
+ }
66
+
67
+ // Display result
68
+ outputEl.textContent = data.generated_text;
69
+ outputEl.className = 'output';
70
+ statusEl.textContent = '✅';
71
+
72
+ } catch (error) {
73
+ console.error('Error:', error);
74
+ outputEl.textContent = `Error: ${error.message}`;
75
+ outputEl.className = 'output error';
76
+ statusEl.textContent = '❌';
77
+ } finally {
78
+ generateBtn.disabled = false;
79
+ }
80
+ });
81
+
82
+ // Allow Enter key to trigger generation (Ctrl+Enter)
83
+ promptEl.addEventListener('keydown', (e) => {
84
+ if (e.ctrlKey && e.key === 'Enter') {
85
+ generateBtn.click();
86
+ }
87
+ });
88
+
89
+ // Health check on load
90
+ async function checkHealth() {
91
+ try {
92
+ const response = await fetch(`${API_URL}/api/health`);
93
+ const data = await response.json();
94
+ console.log('API Health:', data);
95
+ } catch (error) {
96
+ console.warn('API health check failed:', error);
97
+ }
98
+ }
99
+
100
+ // Run health check when page loads
101
+ checkHealth();
static/index.html ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mistral Fine-tuned Model</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <header>
12
+ <h1>🤖 Mistral Fine-tuned Model</h1>
13
+ <p>Model: <code>KASHH-4/mistral_fine-tuned</code></p>
14
+ </header>
15
+
16
+ <main>
17
+ <div class="prompt-section">
18
+ <label for="prompt">Enter your prompt:</label>
19
+ <textarea id="prompt" rows="6" placeholder="Write a short story about a robot learning to paint..."></textarea>
20
+ </div>
21
+
22
+ <div class="settings-section">
23
+ <details>
24
+ <summary>⚙️ Advanced Settings</summary>
25
+ <div class="settings-grid">
26
+ <div class="setting">
27
+ <label for="maxTokens">Max Tokens: <span id="maxTokensValue">256</span></label>
28
+ <input type="range" id="maxTokens" min="50" max="512" value="256">
29
+ </div>
30
+ <div class="setting">
31
+ <label for="temperature">Temperature: <span id="temperatureValue">0.7</span></label>
32
+ <input type="range" id="temperature" min="0.1" max="2.0" step="0.1" value="0.7">
33
+ </div>
34
+ <div class="setting">
35
+ <label for="topP">Top P: <span id="topPValue">0.9</span></label>
36
+ <input type="range" id="topP" min="0.1" max="1.0" step="0.05" value="0.9">
37
+ </div>
38
+ </div>
39
+ </details>
40
+ </div>
41
+
42
+ <div class="button-section">
43
+ <button id="generateBtn" class="generate-btn">✨ Generate</button>
44
+ <span id="status" class="status"></span>
45
+ </div>
46
+
47
+ <div class="output-section">
48
+ <h3>Generated Output:</h3>
49
+ <div id="output" class="output"></div>
50
+ </div>
51
+ </main>
52
+
53
+ <footer>
54
+ <p>API Endpoints: <code>POST /api/generate</code> | <code>GET /api/health</code></p>
55
+ </footer>
56
+ </div>
57
+
58
+ <script src="app.js"></script>
59
+ </body>
60
+ </html>
static/style.css ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ padding: 20px;
12
+ }
13
+
14
+ .container {
15
+ max-width: 900px;
16
+ margin: 0 auto;
17
+ background: white;
18
+ border-radius: 16px;
19
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
20
+ overflow: hidden;
21
+ }
22
+
23
+ header {
24
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
25
+ color: white;
26
+ padding: 30px;
27
+ text-align: center;
28
+ }
29
+
30
+ header h1 {
31
+ font-size: 2.5em;
32
+ margin-bottom: 10px;
33
+ }
34
+
35
+ header p {
36
+ opacity: 0.9;
37
+ font-size: 1.1em;
38
+ }
39
+
40
+ header code {
41
+ background: rgba(255, 255, 255, 0.2);
42
+ padding: 4px 8px;
43
+ border-radius: 4px;
44
+ }
45
+
46
+ main {
47
+ padding: 30px;
48
+ }
49
+
50
+ .prompt-section {
51
+ margin-bottom: 20px;
52
+ }
53
+
54
+ .prompt-section label {
55
+ display: block;
56
+ font-weight: 600;
57
+ margin-bottom: 8px;
58
+ color: #333;
59
+ }
60
+
61
+ #prompt {
62
+ width: 100%;
63
+ padding: 15px;
64
+ border: 2px solid #e0e0e0;
65
+ border-radius: 8px;
66
+ font-size: 1em;
67
+ font-family: inherit;
68
+ resize: vertical;
69
+ transition: border-color 0.3s;
70
+ }
71
+
72
+ #prompt:focus {
73
+ outline: none;
74
+ border-color: #667eea;
75
+ }
76
+
77
+ .settings-section {
78
+ margin-bottom: 20px;
79
+ }
80
+
81
+ details {
82
+ border: 1px solid #e0e0e0;
83
+ border-radius: 8px;
84
+ padding: 15px;
85
+ }
86
+
87
+ summary {
88
+ cursor: pointer;
89
+ font-weight: 600;
90
+ color: #667eea;
91
+ user-select: none;
92
+ }
93
+
94
+ summary:hover {
95
+ color: #764ba2;
96
+ }
97
+
98
+ .settings-grid {
99
+ display: grid;
100
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
101
+ gap: 20px;
102
+ margin-top: 15px;
103
+ }
104
+
105
+ .setting label {
106
+ display: block;
107
+ margin-bottom: 8px;
108
+ font-weight: 500;
109
+ color: #555;
110
+ }
111
+
112
+ .setting input[type="range"] {
113
+ width: 100%;
114
+ cursor: pointer;
115
+ }
116
+
117
+ .button-section {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 15px;
121
+ margin-bottom: 30px;
122
+ }
123
+
124
+ .generate-btn {
125
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
126
+ color: white;
127
+ border: none;
128
+ padding: 15px 40px;
129
+ font-size: 1.1em;
130
+ font-weight: 600;
131
+ border-radius: 8px;
132
+ cursor: pointer;
133
+ transition: transform 0.2s, box-shadow 0.2s;
134
+ }
135
+
136
+ .generate-btn:hover {
137
+ transform: translateY(-2px);
138
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
139
+ }
140
+
141
+ .generate-btn:active {
142
+ transform: translateY(0);
143
+ }
144
+
145
+ .generate-btn:disabled {
146
+ opacity: 0.6;
147
+ cursor: not-allowed;
148
+ }
149
+
150
+ .status {
151
+ font-size: 1.5em;
152
+ }
153
+
154
+ .output-section h3 {
155
+ color: #333;
156
+ margin-bottom: 15px;
157
+ }
158
+
159
+ .output {
160
+ background: #f8f9fa;
161
+ border: 2px solid #e0e0e0;
162
+ border-radius: 8px;
163
+ padding: 20px;
164
+ min-height: 150px;
165
+ font-family: 'Courier New', monospace;
166
+ white-space: pre-wrap;
167
+ word-wrap: break-word;
168
+ line-height: 1.6;
169
+ color: #333;
170
+ }
171
+
172
+ .output.empty {
173
+ color: #999;
174
+ font-style: italic;
175
+ }
176
+
177
+ .output.error {
178
+ color: #dc3545;
179
+ background: #fff5f5;
180
+ border-color: #dc3545;
181
+ }
182
+
183
+ footer {
184
+ background: #f8f9fa;
185
+ padding: 20px;
186
+ text-align: center;
187
+ color: #666;
188
+ font-size: 0.9em;
189
+ }
190
+
191
+ footer code {
192
+ background: white;
193
+ padding: 4px 8px;
194
+ border-radius: 4px;
195
+ border: 1px solid #e0e0e0;
196
+ }
197
+
198
+ @media (max-width: 768px) {
199
+ header h1 {
200
+ font-size: 2em;
201
+ }
202
+
203
+ .settings-grid {
204
+ grid-template-columns: 1fr;
205
+ }
206
+
207
+ .button-section {
208
+ flex-direction: column;
209
+ align-items: stretch;
210
+ }
211
+ }