AntDX316 commited on
Commit
9ab3421
·
1 Parent(s): c122703
Files changed (14) hide show
  1. .dockerignore +14 -0
  2. .env.example +9 -0
  3. .gitignore +47 -0
  4. Dockerfile +20 -0
  5. HF-Grok +1 -0
  6. README.md +60 -5
  7. app.py +201 -0
  8. index.html +88 -11
  9. requirements.txt +6 -0
  10. script.js +323 -0
  11. static/index.html +98 -0
  12. static/script.js +263 -0
  13. static/style.css +369 -0
  14. style.css +267 -16
.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitattributes
3
+ .env
4
+ *.md
5
+ !README.md
6
+ __pycache__
7
+ *.pyc
8
+ *.pyo
9
+ *.pyd
10
+ .Python
11
+ env/
12
+ venv/
13
+ .venv/
14
+ *.log
.env.example ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Port for the Flask app
2
+ PORT=7860
3
+
4
+ # Host for the Flask app (0.0.0.0 for Docker)
5
+ HOST=0.0.0.0
6
+
7
+ # Default model names (change if model names differ)
8
+ GROK_IMAGE_MODEL=grok-2-image-1212
9
+ GROK_VISION_MODEL=grok-2-vision-1212
.gitignore ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ .env
23
+
24
+ # Virtual Environment
25
+ venv/
26
+ ENV/
27
+ .venv/
28
+
29
+ # Logs
30
+ logs
31
+ *.log
32
+ npm-debug.log*
33
+ yarn-debug.log*
34
+ yarn-error.log*
35
+
36
+ # Runtime data
37
+ pids
38
+ *.pid
39
+ *.seed
40
+ *.pid.lock
41
+
42
+ # Editors
43
+ .idea/
44
+ .vscode/
45
+ *.swp
46
+ *.swo
47
+ *~
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy requirements first for better caching
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy the rest of the application
10
+ COPY . .
11
+
12
+ # Environment variables
13
+ ENV PORT=7860
14
+ ENV HOST=0.0.0.0
15
+
16
+ # Expose the port
17
+ EXPOSE 7860
18
+
19
+ # Run the application
20
+ CMD ["python", "app.py"]
HF-Grok ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit c1227034d867658f0611206dc85cd619138a4953
README.md CHANGED
@@ -1,10 +1,65 @@
1
  ---
2
- title: HF Grok
3
- emoji: 📉
4
- colorFrom: green
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Grok-2 AI Interface
3
+ emoji: 🤖
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
+ # Grok-2 AI Interface
11
+
12
+ A Docker-based web interface for interacting with Grok-2 AI models. This application provides a secure way to use Grok-2 models for image generation and vision analysis through a server-side implementation.
13
+
14
+ ## Features
15
+
16
+ - **Image Generation**: Create images from text descriptions using Grok-2-Image-1212
17
+ - **Vision Analysis**: Upload images for AI analysis using Grok-2-Vision-1212
18
+ - **Secure API Key Handling**: Your API key is stored in your browser and only sent to our backend (not third parties)
19
+ - **Server-Side API Calls**: All requests to the Grok-2 API are made securely from the server
20
+
21
+ ## Requirements
22
+
23
+ - Docker
24
+ - Grok-2 API key from xAI
25
+
26
+ ## Running with Docker
27
+
28
+ 1. Build the Docker image:
29
+ ```
30
+ docker build -t grok2-interface .
31
+ ```
32
+
33
+ 2. Run the container:
34
+ ```
35
+ docker run -p 7860:7860 grok2-interface
36
+ ```
37
+
38
+ 3. Open your browser and go to: `http://localhost:7860`
39
+
40
+ ## Using the Application
41
+
42
+ 1. On the first tab, enter your Grok-2 API key and save it
43
+ 2. Navigate to either the Image Generation or Vision Analysis tab
44
+ 3. For Image Generation:
45
+ - Enter a detailed text prompt
46
+ - Click "Generate Image"
47
+ 4. For Vision Analysis:
48
+ - Upload an image
49
+ - Optionally enter a question about the image
50
+ - Click "Analyze Image"
51
+
52
+ ## Deploying to Hugging Face Spaces
53
+
54
+ To deploy this application to Hugging Face Spaces:
55
+
56
+ 1. Create a new Space with Docker SDK
57
+ 2. Push this code to the Space's repository
58
+ 3. The Space will automatically build and deploy the Docker container
59
+
60
+ ## Security Notes
61
+
62
+ - Your API key is stored in your browser's localStorage
63
+ - The key is only sent to our backend server to make API calls
64
+ - Our server doesn't log or store your API key
65
+ - All communication with the Grok-2 API happens server-side for security
app.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import requests
4
+ from flask import Flask, request, jsonify, send_from_directory
5
+ from flask_cors import CORS
6
+ from PIL import Image
7
+ import io
8
+ from dotenv import load_dotenv
9
+
10
+ # Load environment variables from .env file if present
11
+ load_dotenv()
12
+
13
+ # Get environment variables
14
+ PORT = int(os.environ.get('PORT', 7860))
15
+ HOST = os.environ.get('HOST', '0.0.0.0')
16
+ GROK_IMAGE_MODEL = os.environ.get('GROK_IMAGE_MODEL', 'grok-2-image-1212')
17
+ GROK_VISION_MODEL = os.environ.get('GROK_VISION_MODEL', 'grok-2-vision-1212')
18
+
19
+ app = Flask(__name__, static_folder='static')
20
+ CORS(app)
21
+
22
+ # Route to serve the frontend
23
+ @app.route('/', defaults={'path': ''})
24
+ @app.route('/<path:path>')
25
+ def serve(path):
26
+ if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
27
+ return send_from_directory(app.static_folder, path)
28
+ else:
29
+ return send_from_directory(app.static_folder, 'index.html')
30
+
31
+ # Image generation API
32
+ @app.route('/api/generate-image', methods=['POST'])
33
+ def generate_image():
34
+ data = request.json
35
+ api_key = data.get('api_key')
36
+ prompt = data.get('prompt')
37
+ use_openai_format = data.get('use_openai_format', False)
38
+
39
+ if not api_key or not prompt:
40
+ return jsonify({"error": "Missing API key or prompt"}), 400
41
+
42
+ try:
43
+ headers = {
44
+ "Content-Type": "application/json",
45
+ "Authorization": f"Bearer {api_key}"
46
+ }
47
+
48
+ # OpenAI SDK-style format or original format - both use the same endpoint
49
+ json_data = {
50
+ "model": "grok-2-image-1212", # Use the specific Grok-2 image model
51
+ "prompt": prompt
52
+ # Simplified request: removed n and size parameters
53
+ }
54
+
55
+ print(f"Sending request to x.ai API with data: {json_data}")
56
+ print(f"Using authorization header: Bearer {api_key[:10]}...")
57
+
58
+ # The correct endpoint for x.ai image generation
59
+ response = requests.post(
60
+ "https://api.x.ai/v1/images/generations",
61
+ headers=headers,
62
+ json=json_data
63
+ )
64
+
65
+ if not response.ok:
66
+ # Print detailed error information
67
+ print(f"X.AI API Error Status: {response.status_code}")
68
+ print(f"X.AI API Error Response: {response.text}")
69
+ try:
70
+ error_data = response.json()
71
+ print(f"X.AI API Error JSON: {error_data}")
72
+ return jsonify({"error": error_data.get('error', {}).get('message', f"API error: {response.status_code}")}), response.status_code
73
+ except:
74
+ return jsonify({"error": f"API error: {response.status_code}, Response: {response.text}"}), response.status_code
75
+
76
+ result = response.json()
77
+ print(f"X.AI API Success Response: {result}")
78
+
79
+ # Handle different possible response formats
80
+ image_url = None
81
+ caption = None
82
+
83
+ if 'data' in result and len(result['data']) > 0:
84
+ data_item = result['data'][0]
85
+ if 'url' in data_item:
86
+ image_url = data_item['url']
87
+ if 'revised_prompt' in data_item:
88
+ caption = data_item['revised_prompt']
89
+ elif 'images' in result and len(result['images']) > 0:
90
+ image_url = result['images'][0]
91
+ elif 'url' in result:
92
+ image_url = result['url']
93
+
94
+ if not image_url:
95
+ print(f"No image URL found in response: {result}")
96
+ return jsonify({"error": "Unexpected API response format", "details": result}), 500
97
+
98
+ response_data = {"image_url": image_url}
99
+ if caption:
100
+ response_data["caption"] = caption
101
+
102
+ return jsonify(response_data)
103
+
104
+ except requests.exceptions.RequestException as e:
105
+ # If the API returns an error response
106
+ print(f"Request exception: {str(e)}")
107
+ if hasattr(e, 'response') and e.response is not None:
108
+ try:
109
+ error_data = e.response.json()
110
+ print(f"Error response JSON: {error_data}")
111
+ return jsonify({"error": error_data.get('error', {}).get('message', str(e))}), e.response.status_code
112
+ except:
113
+ print(f"Could not parse error response: {e.response.text}")
114
+ return jsonify({"error": f"API error: {e.response.status_code}"}), e.response.status_code
115
+ return jsonify({"error": str(e)}), 500
116
+
117
+ # Image analysis API
118
+ @app.route('/api/analyze-image', methods=['POST'])
119
+ def analyze_image():
120
+ api_key = request.form.get('api_key')
121
+ prompt = request.form.get('prompt', 'Describe this image in detail')
122
+
123
+ if not api_key or 'image' not in request.files:
124
+ return jsonify({"error": "Missing API key or image"}), 400
125
+
126
+ try:
127
+ # Process the uploaded image
128
+ image_file = request.files['image']
129
+ img = Image.open(image_file)
130
+
131
+ # Convert to base64
132
+ buffered = io.BytesIO()
133
+ img.save(buffered, format=img.format if img.format else 'JPEG')
134
+ img_str = base64.b64encode(buffered.getvalue()).decode('utf-8')
135
+
136
+ # Determine image mime type
137
+ format_mapping = {
138
+ 'JPEG': 'image/jpeg',
139
+ 'PNG': 'image/png',
140
+ 'GIF': 'image/gif',
141
+ 'WEBP': 'image/webp'
142
+ }
143
+ mime_type = format_mapping.get(img.format, 'image/jpeg')
144
+
145
+ # Call the Grok Vision API
146
+ response = requests.post(
147
+ "https://api.x.ai/v1/chat/completions",
148
+ headers={
149
+ "Content-Type": "application/json",
150
+ "Authorization": f"Bearer {api_key}"
151
+ },
152
+ json={
153
+ "model": GROK_VISION_MODEL,
154
+ "messages": [
155
+ {
156
+ "role": "user",
157
+ "content": [
158
+ {"type": "text", "text": prompt},
159
+ {
160
+ "type": "image_url",
161
+ "image_url": {
162
+ "url": f"data:{mime_type};base64,{img_str}"
163
+ }
164
+ }
165
+ ]
166
+ }
167
+ ],
168
+ "max_tokens": 1000
169
+ }
170
+ )
171
+
172
+ response.raise_for_status()
173
+ result = response.json()
174
+
175
+ # Handle different response formats
176
+ if 'choices' in result and len(result['choices']) > 0 and 'message' in result['choices'][0]:
177
+ analysis = result['choices'][0]['message']['content']
178
+ elif 'response' in result:
179
+ analysis = result['response']
180
+ else:
181
+ return jsonify({"error": "Unexpected API response format", "details": result}), 500
182
+
183
+ return jsonify({"analysis": analysis})
184
+
185
+ except requests.exceptions.RequestException as e:
186
+ # If the API returns an error response
187
+ if hasattr(e, 'response') and e.response is not None:
188
+ try:
189
+ error_data = e.response.json()
190
+ return jsonify({"error": error_data.get('error', {}).get('message', str(e))}), e.response.status_code
191
+ except:
192
+ return jsonify({"error": f"API error: {e.response.status_code}"}), e.response.status_code
193
+ return jsonify({"error": str(e)}), 500
194
+ except Exception as e:
195
+ return jsonify({"error": str(e)}), 500
196
+
197
+ if __name__ == '__main__':
198
+ print(f"Starting Grok-2 API interface on {HOST}:{PORT}")
199
+ print(f"Using image model: {GROK_IMAGE_MODEL}")
200
+ print(f"Using vision model: {GROK_VISION_MODEL}")
201
+ app.run(host=HOST, port=PORT)
index.html CHANGED
@@ -1,19 +1,96 @@
1
  <!doctype html>
2
- <html>
3
  <head>
4
  <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
  <link rel="stylesheet" href="style.css" />
8
  </head>
9
  <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  </body>
19
  </html>
 
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>Grok-2 AI Interface</title>
7
  <link rel="stylesheet" href="style.css" />
8
  </head>
9
  <body>
10
+ <header>
11
+ <div class="container">
12
+ <h1 class="logo">Grok-2 AI Interface</h1>
13
+ <nav>
14
+ <ul>
15
+ <li><a href="#" class="active" data-section="api-key">API Key</a></li>
16
+ <li><a href="#" data-section="image-gen">Image Generation</a></li>
17
+ <li><a href="#" data-section="vision">Vision Analysis</a></li>
18
+ </ul>
19
+ </nav>
20
+ </div>
21
+ </header>
22
+
23
+ <main>
24
+ <div class="container">
25
+ <section id="api-key" class="section active">
26
+ <div class="api-key-container">
27
+ <h2>API Key Management</h2>
28
+ <p>Enter your Grok-2 API key below. The key will be stored locally on your device.</p>
29
+
30
+ <div class="input-group">
31
+ <input type="password" id="api-key-input" placeholder="Enter your Grok-2 API key">
32
+ <button id="save-api-key" class="btn">Save API Key</button>
33
+ <button id="clear-api-key" class="btn btn-secondary">Clear Saved Key</button>
34
+ </div>
35
+
36
+ <div id="api-key-status"></div>
37
+ </div>
38
+ </section>
39
+
40
+ <section id="image-gen" class="section">
41
+ <div class="model-card">
42
+ <div class="model-info">
43
+ <h2>Grok-2-Image-1212</h2>
44
+ <p>Image Generation Model</p>
45
+ <p>Our latest image generation model, capable of creating high-quality, detailed images from text prompts with enhanced creativity and precision.</p>
46
+ </div>
47
+
48
+ <div class="input-container">
49
+ <textarea id="image-prompt" placeholder="Enter a detailed description of the image you want to generate..."></textarea>
50
+ <button id="generate-image" class="btn">Generate Image</button>
51
+ </div>
52
+
53
+ <div class="result-container">
54
+ <div id="image-result" class="result-box"></div>
55
+ <div id="generation-status" class="status"></div>
56
+ </div>
57
+ </div>
58
+ </section>
59
+
60
+ <section id="vision" class="section">
61
+ <div class="model-card">
62
+ <div class="model-info">
63
+ <h2>Grok-2-Vision-1212</h2>
64
+ <p>Image Analysis Model</p>
65
+ <p>Our latest image understanding model with increased context window that can process a wide variety of visual information, including documents, diagrams, charts, screenshots, and photographs.</p>
66
+ </div>
67
+
68
+ <div class="input-container">
69
+ <div class="upload-area" id="upload-area">
70
+ <p>Drag and drop an image or click to upload</p>
71
+ <input type="file" id="image-upload" accept="image/*" hidden>
72
+ </div>
73
+ <textarea id="vision-prompt" placeholder="Optional: Enter a question about the image..."></textarea>
74
+ <button id="analyze-image" class="btn" disabled>Analyze Image</button>
75
+ </div>
76
+
77
+ <div class="result-container">
78
+ <div id="uploaded-image" class="uploaded-image"></div>
79
+ <div id="vision-result" class="result-box"></div>
80
+ <div id="vision-status" class="status"></div>
81
+ </div>
82
+ </div>
83
+ </section>
84
+ </div>
85
+ </main>
86
+
87
+ <footer>
88
+ <div class="container">
89
+ <p>This is an unofficial interface. Grok-2 is a product of xAI.</p>
90
+ <p>Your API key is stored locally in your browser and is never sent to our servers.</p>
91
+ </div>
92
+ </footer>
93
+
94
+ <script src="script.js"></script>
95
  </body>
96
  </html>
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask==2.0.1
2
+ requests==2.28.1
3
+ python-dotenv==0.20.0
4
+ gunicorn==20.1.0
5
+ Pillow==9.1.0
6
+ Flask-Cors==3.0.10
script.js ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Wait for the DOM to be fully loaded
2
+ document.addEventListener('DOMContentLoaded', function() {
3
+ // API key storage in localStorage
4
+ const API_KEY_STORAGE_KEY = 'grok2_api_key';
5
+ const apiKeyInput = document.getElementById('api-key-input');
6
+ const saveApiKeyBtn = document.getElementById('save-api-key');
7
+ const clearApiKeyBtn = document.getElementById('clear-api-key');
8
+ const apiKeyStatus = document.getElementById('api-key-status');
9
+
10
+ // Navigation
11
+ const navLinks = document.querySelectorAll('nav ul li a');
12
+ const sections = document.querySelectorAll('.section');
13
+
14
+ // Image Generation
15
+ const imagePromptInput = document.getElementById('image-prompt');
16
+ const generateImageBtn = document.getElementById('generate-image');
17
+ const imageResult = document.getElementById('image-result');
18
+ const generationStatus = document.getElementById('generation-status');
19
+
20
+ // Vision Analysis
21
+ const uploadArea = document.getElementById('upload-area');
22
+ const imageUploadInput = document.getElementById('image-upload');
23
+ const visionPromptInput = document.getElementById('vision-prompt');
24
+ const analyzeImageBtn = document.getElementById('analyze-image');
25
+ const uploadedImageContainer = document.getElementById('uploaded-image');
26
+ const visionResult = document.getElementById('vision-result');
27
+ const visionStatus = document.getElementById('vision-status');
28
+
29
+ let uploadedImage = null;
30
+
31
+ // Check if API key is stored on load
32
+ checkStoredApiKey();
33
+
34
+ // API Key Management
35
+ function checkStoredApiKey() {
36
+ const storedApiKey = localStorage.getItem(API_KEY_STORAGE_KEY);
37
+ if (storedApiKey) {
38
+ apiKeyStatus.textContent = '✅ API key is stored';
39
+ apiKeyStatus.style.color = '#27ae60';
40
+ // Mask the input for security
41
+ apiKeyInput.value = '•'.repeat(12);
42
+ } else {
43
+ apiKeyStatus.textContent = '❌ No API key stored';
44
+ apiKeyStatus.style.color = '#e74c3c';
45
+ apiKeyInput.value = '';
46
+ }
47
+ }
48
+
49
+ saveApiKeyBtn.addEventListener('click', function() {
50
+ const apiKey = apiKeyInput.value.trim();
51
+ if (apiKey && apiKey !== '•'.repeat(12)) {
52
+ localStorage.setItem(API_KEY_STORAGE_KEY, apiKey);
53
+ apiKeyStatus.textContent = '✅ API key saved successfully';
54
+ apiKeyStatus.style.color = '#27ae60';
55
+ // Mask the input for security
56
+ apiKeyInput.value = '•'.repeat(12);
57
+ } else if (apiKey === '•'.repeat(12)) {
58
+ apiKeyStatus.textContent = '❓ Please enter a new API key';
59
+ apiKeyStatus.style.color = '#f39c12';
60
+ } else {
61
+ apiKeyStatus.textContent = '❌ Please enter a valid API key';
62
+ apiKeyStatus.style.color = '#e74c3c';
63
+ }
64
+ });
65
+
66
+ clearApiKeyBtn.addEventListener('click', function() {
67
+ localStorage.removeItem(API_KEY_STORAGE_KEY);
68
+ apiKeyInput.value = '';
69
+ apiKeyStatus.textContent = '🗑️ API key cleared';
70
+ apiKeyStatus.style.color = '#e74c3c';
71
+ });
72
+
73
+ // Navigation between sections
74
+ navLinks.forEach(link => {
75
+ link.addEventListener('click', function(e) {
76
+ e.preventDefault();
77
+
78
+ // Remove active class from all links and add to current
79
+ navLinks.forEach(l => l.classList.remove('active'));
80
+ this.classList.add('active');
81
+
82
+ // Show the selected section
83
+ const targetSection = this.getAttribute('data-section');
84
+ sections.forEach(section => {
85
+ section.classList.remove('active');
86
+ if (section.id === targetSection) {
87
+ section.classList.add('active');
88
+ }
89
+ });
90
+ });
91
+ });
92
+
93
+ // Image Upload Handling
94
+ uploadArea.addEventListener('click', function() {
95
+ imageUploadInput.click();
96
+ });
97
+
98
+ uploadArea.addEventListener('dragover', function(e) {
99
+ e.preventDefault();
100
+ this.classList.add('dragover');
101
+ });
102
+
103
+ uploadArea.addEventListener('dragleave', function() {
104
+ this.classList.remove('dragover');
105
+ });
106
+
107
+ uploadArea.addEventListener('drop', function(e) {
108
+ e.preventDefault();
109
+ this.classList.remove('dragover');
110
+
111
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
112
+ handleImageUpload(e.dataTransfer.files[0]);
113
+ }
114
+ });
115
+
116
+ imageUploadInput.addEventListener('change', function() {
117
+ if (this.files && this.files[0]) {
118
+ handleImageUpload(this.files[0]);
119
+ }
120
+ });
121
+
122
+ function handleImageUpload(file) {
123
+ if (!file.type.match('image.*')) {
124
+ visionStatus.textContent = '❌ Please upload an image file';
125
+ return;
126
+ }
127
+
128
+ const reader = new FileReader();
129
+ reader.onload = function(e) {
130
+ const img = document.createElement('img');
131
+ img.src = e.target.result;
132
+ uploadedImageContainer.innerHTML = '';
133
+ uploadedImageContainer.appendChild(img);
134
+ uploadedImage = file;
135
+ analyzeImageBtn.disabled = false;
136
+ visionStatus.textContent = '✅ Image uploaded successfully';
137
+ };
138
+ reader.readAsDataURL(file);
139
+ }
140
+
141
+ // Image Generation
142
+ generateImageBtn.addEventListener('click', function() {
143
+ const prompt = imagePromptInput.value.trim();
144
+ if (!prompt) {
145
+ generationStatus.textContent = '❌ Please enter a prompt';
146
+ return;
147
+ }
148
+
149
+ const apiKey = localStorage.getItem(API_KEY_STORAGE_KEY);
150
+ if (!apiKey) {
151
+ generationStatus.textContent = '❌ API key is required. Please add your API key in the API Key section.';
152
+ return;
153
+ }
154
+
155
+ // Show loading state
156
+ generationStatus.textContent = '⏳ Generating image...';
157
+ generateImageBtn.disabled = true;
158
+
159
+ // Call the Grok-2 Image API
160
+ callGrokImageApi(prompt, apiKey)
161
+ .then(imageUrl => {
162
+ // Display the generated image
163
+ imageResult.innerHTML = `<img src="${imageUrl}" alt="Generated image">`;
164
+ generationStatus.textContent = '✅ Image generated successfully';
165
+ })
166
+ .catch(error => {
167
+ generationStatus.textContent = `❌ Error: ${error.message}`;
168
+ console.error('Image generation error:', error);
169
+ })
170
+ .finally(() => {
171
+ generateImageBtn.disabled = false;
172
+ });
173
+ });
174
+
175
+ // Vision Analysis
176
+ analyzeImageBtn.addEventListener('click', function() {
177
+ if (!uploadedImage) {
178
+ visionStatus.textContent = '❌ Please upload an image first';
179
+ return;
180
+ }
181
+
182
+ const prompt = visionPromptInput.value.trim() || 'Describe this image in detail';
183
+ const apiKey = localStorage.getItem(API_KEY_STORAGE_KEY);
184
+ if (!apiKey) {
185
+ visionStatus.textContent = '❌ API key is required. Please add your API key in the API Key section.';
186
+ return;
187
+ }
188
+
189
+ // Show loading state
190
+ visionStatus.textContent = '⏳ Analyzing image...';
191
+ analyzeImageBtn.disabled = true;
192
+
193
+ // Call the Grok-2 Vision API
194
+ callGrokVisionApi(uploadedImage, prompt, apiKey)
195
+ .then(analysisResult => {
196
+ // Display the analysis result
197
+ visionResult.textContent = analysisResult;
198
+ visionStatus.textContent = '✅ Analysis completed';
199
+ })
200
+ .catch(error => {
201
+ visionStatus.textContent = `❌ Error: ${error.message}`;
202
+ console.error('Vision analysis error:', error);
203
+ })
204
+ .finally(() => {
205
+ analyzeImageBtn.disabled = false;
206
+ });
207
+ });
208
+
209
+ // API Calls
210
+ async function callGrokImageApi(prompt, apiKey) {
211
+ generationStatus.textContent = '⏳ Calling Grok-2 Image API...';
212
+
213
+ try {
214
+ const response = await fetch('https://api.x.ai/v1/images/generations', {
215
+ method: 'POST',
216
+ headers: {
217
+ 'Content-Type': 'application/json',
218
+ 'Authorization': `Bearer ${apiKey}`
219
+ },
220
+ body: JSON.stringify({
221
+ model: "grok-2-image-1212",
222
+ prompt: prompt,
223
+ n: 1,
224
+ size: "1024x1024"
225
+ })
226
+ });
227
+
228
+ if (!response.ok) {
229
+ const errorData = await response.json().catch(() => ({ error: { message: `Status code: ${response.status}` } }));
230
+ console.log("API Error Response:", errorData);
231
+ throw new Error(errorData.error?.message || `API error: ${response.status}`);
232
+ }
233
+
234
+ const data = await response.json();
235
+ console.log("API Success Response:", data);
236
+
237
+ // Handle various response formats
238
+ if (data.data && data.data[0] && data.data[0].url) {
239
+ return data.data[0].url; // OpenAI format
240
+ } else if (data.images && data.images[0]) {
241
+ return data.images[0]; // Alternate format
242
+ } else if (data.url) {
243
+ return data.url; // Simple format
244
+ } else {
245
+ console.log("Unexpected response format:", data);
246
+ throw new Error("Unexpected response format from API");
247
+ }
248
+ } catch (error) {
249
+ console.error('Image generation API error:', error);
250
+ throw new Error(`Failed to generate image: ${error.message}`);
251
+ }
252
+ }
253
+
254
+ async function callGrokVisionApi(imageFile, prompt, apiKey) {
255
+ visionStatus.textContent = '⏳ Calling Grok-2 Vision API...';
256
+
257
+ try {
258
+ // Convert image to base64
259
+ const base64Image = await fileToBase64(imageFile);
260
+
261
+ const response = await fetch('https://api.x.ai/v1/chat/completions', {
262
+ method: 'POST',
263
+ headers: {
264
+ 'Content-Type': 'application/json',
265
+ 'Authorization': `Bearer ${apiKey}`
266
+ },
267
+ body: JSON.stringify({
268
+ model: "grok-2-vision-1212",
269
+ messages: [
270
+ {
271
+ role: "user",
272
+ content: [
273
+ { type: "text", text: prompt },
274
+ {
275
+ type: "image_url",
276
+ image_url: {
277
+ url: `data:image/${imageFile.type};base64,${base64Image}`
278
+ }
279
+ }
280
+ ]
281
+ }
282
+ ],
283
+ max_tokens: 1000
284
+ })
285
+ });
286
+
287
+ if (!response.ok) {
288
+ const errorData = await response.json().catch(() => ({ error: { message: `Status code: ${response.status}` } }));
289
+ console.log("Vision API Error Response:", errorData);
290
+ throw new Error(errorData.error?.message || `API error: ${response.status}`);
291
+ }
292
+
293
+ const data = await response.json();
294
+ console.log("Vision API Success Response:", data);
295
+
296
+ // Handle different response formats
297
+ if (data.choices && data.choices[0] && data.choices[0].message) {
298
+ return data.choices[0].message.content;
299
+ } else if (data.response) {
300
+ return data.response;
301
+ } else {
302
+ console.log("Unexpected vision response format:", data);
303
+ return "Received a response from the API, but in an unexpected format.";
304
+ }
305
+ } catch (error) {
306
+ console.error('Vision API error:', error);
307
+ throw new Error(`Failed to analyze image: ${error.message}`);
308
+ }
309
+ }
310
+
311
+ // Helper function to convert File to base64
312
+ function fileToBase64(file) {
313
+ return new Promise((resolve, reject) => {
314
+ const reader = new FileReader();
315
+ reader.readAsDataURL(file);
316
+ reader.onload = () => {
317
+ const base64String = reader.result.split(',')[1];
318
+ resolve(base64String);
319
+ };
320
+ reader.onerror = error => reject(error);
321
+ });
322
+ }
323
+ });
static/index.html ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Grok-2 AI Interface</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <div class="container">
13
+ <h1 class="logo">Grok-2 AI Interface</h1>
14
+ <nav>
15
+ <ul>
16
+ <li><a href="#" class="active" data-section="api-key">API Key</a></li>
17
+ <li><a href="#" data-section="image-gen">Image Generation</a></li>
18
+ <li><a href="#" data-section="vision">Vision Analysis</a></li>
19
+ </ul>
20
+ </nav>
21
+ </div>
22
+ </header>
23
+
24
+ <main>
25
+ <div class="container">
26
+ <section id="api-key" class="section active">
27
+ <div class="api-key-container">
28
+ <h2>API Key Management</h2>
29
+ <p>Enter your Grok-2 API key below. The key will be stored locally on your device.</p>
30
+
31
+ <div class="input-group">
32
+ <input type="password" id="api-key-input" placeholder="Enter your Grok-2 API key">
33
+ <button id="save-api-key" class="btn">Save API Key</button>
34
+ <button id="clear-api-key" class="btn btn-secondary">Clear Saved Key</button>
35
+ </div>
36
+
37
+ <div id="api-key-status"></div>
38
+ </div>
39
+ </section>
40
+
41
+ <section id="image-gen" class="section">
42
+ <div class="model-card">
43
+ <div class="model-info">
44
+ <h2>Grok-2-Image-1212</h2>
45
+ <p>Image Generation Model</p>
46
+ <p>Our latest image generation model, capable of creating high-quality, detailed images from text prompts with enhanced creativity and precision.</p>
47
+ </div>
48
+
49
+ <div class="input-container">
50
+ <textarea id="image-prompt" placeholder="Enter a detailed description of the image you want to generate..."></textarea>
51
+ <button id="generate-image" class="btn">Generate Image</button>
52
+ </div>
53
+
54
+ <div class="result-container">
55
+ <div id="image-result" class="result-box"></div>
56
+ <div id="generation-status" class="status"></div>
57
+ </div>
58
+ </div>
59
+ </section>
60
+
61
+ <section id="vision" class="section">
62
+ <div class="model-card">
63
+ <div class="model-info">
64
+ <h2>Grok-2-Vision-1212</h2>
65
+ <p>Image Analysis Model</p>
66
+ <p>Our latest image understanding model with increased context window that can process a wide variety of visual information, including documents, diagrams, charts, screenshots, and photographs.</p>
67
+ </div>
68
+
69
+ <div class="input-container">
70
+ <div class="upload-area" id="upload-area">
71
+ <p>Drag and drop an image or click to upload</p>
72
+ <input type="file" id="image-upload" accept="image/*" hidden>
73
+ </div>
74
+ <textarea id="vision-prompt" placeholder="Optional: Enter a question about the image..."></textarea>
75
+ <button id="analyze-image" class="btn" disabled>Analyze Image</button>
76
+ </div>
77
+
78
+ <div class="result-container">
79
+ <div id="uploaded-image" class="uploaded-image"></div>
80
+ <div id="vision-result" class="result-box"></div>
81
+ <div id="vision-status" class="status"></div>
82
+ </div>
83
+ </div>
84
+ </section>
85
+ </div>
86
+ </main>
87
+
88
+ <footer>
89
+ <div class="container">
90
+ <p>This is an unofficial interface. Grok-2 is a product of xAI.</p>
91
+ <p>Your API key is stored locally in your browser and is never sent to any third-party servers.</p>
92
+ <p>The application is running in a Docker container with a secure backend to protect your API key.</p>
93
+ </div>
94
+ </footer>
95
+
96
+ <script src="script.js"></script>
97
+ </body>
98
+ </html>
static/script.js ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Wait for the DOM to be fully loaded
2
+ document.addEventListener('DOMContentLoaded', function() {
3
+ // Configure marked.js
4
+ marked.setOptions({
5
+ breaks: true,
6
+ gfm: true,
7
+ smartLists: true
8
+ });
9
+
10
+ // API key storage in localStorage
11
+ const API_KEY_STORAGE_KEY = 'grok2_api_key';
12
+ const apiKeyInput = document.getElementById('api-key-input');
13
+ const saveApiKeyBtn = document.getElementById('save-api-key');
14
+ const clearApiKeyBtn = document.getElementById('clear-api-key');
15
+ const apiKeyStatus = document.getElementById('api-key-status');
16
+
17
+ // Navigation
18
+ const navLinks = document.querySelectorAll('nav ul li a');
19
+ const sections = document.querySelectorAll('.section');
20
+
21
+ // Image Generation
22
+ const imagePromptInput = document.getElementById('image-prompt');
23
+ const generateImageBtn = document.getElementById('generate-image');
24
+ const imageResult = document.getElementById('image-result');
25
+ const generationStatus = document.getElementById('generation-status');
26
+
27
+ // Vision Analysis
28
+ const uploadArea = document.getElementById('upload-area');
29
+ const imageUploadInput = document.getElementById('image-upload');
30
+ const visionPromptInput = document.getElementById('vision-prompt');
31
+ const analyzeImageBtn = document.getElementById('analyze-image');
32
+ const uploadedImageContainer = document.getElementById('uploaded-image');
33
+ const visionResult = document.getElementById('vision-result');
34
+ const visionStatus = document.getElementById('vision-status');
35
+
36
+ let uploadedImage = null;
37
+
38
+ // Check if API key is stored on load
39
+ checkStoredApiKey();
40
+
41
+ // API Key Management
42
+ function checkStoredApiKey() {
43
+ const storedApiKey = localStorage.getItem(API_KEY_STORAGE_KEY);
44
+ if (storedApiKey) {
45
+ apiKeyStatus.textContent = '✅ API key is stored';
46
+ apiKeyStatus.style.color = '#27ae60';
47
+ // Mask the input for security
48
+ apiKeyInput.value = '•'.repeat(12);
49
+ } else {
50
+ apiKeyStatus.textContent = '❌ No API key stored';
51
+ apiKeyStatus.style.color = '#e74c3c';
52
+ apiKeyInput.value = '';
53
+ }
54
+ }
55
+
56
+ saveApiKeyBtn.addEventListener('click', function() {
57
+ const apiKey = apiKeyInput.value.trim();
58
+ if (apiKey && apiKey !== '•'.repeat(12)) {
59
+ localStorage.setItem(API_KEY_STORAGE_KEY, apiKey);
60
+ apiKeyStatus.textContent = '✅ API key saved successfully';
61
+ apiKeyStatus.style.color = '#27ae60';
62
+ // Mask the input for security
63
+ apiKeyInput.value = '•'.repeat(12);
64
+ } else if (apiKey === '•'.repeat(12)) {
65
+ apiKeyStatus.textContent = '❓ Please enter a new API key';
66
+ apiKeyStatus.style.color = '#f39c12';
67
+ } else {
68
+ apiKeyStatus.textContent = '❌ Please enter a valid API key';
69
+ apiKeyStatus.style.color = '#e74c3c';
70
+ }
71
+ });
72
+
73
+ clearApiKeyBtn.addEventListener('click', function() {
74
+ localStorage.removeItem(API_KEY_STORAGE_KEY);
75
+ apiKeyInput.value = '';
76
+ apiKeyStatus.textContent = '🗑️ API key cleared';
77
+ apiKeyStatus.style.color = '#e74c3c';
78
+ });
79
+
80
+ // Navigation between sections
81
+ navLinks.forEach(link => {
82
+ link.addEventListener('click', function(e) {
83
+ e.preventDefault();
84
+
85
+ // Remove active class from all links and add to current
86
+ navLinks.forEach(l => l.classList.remove('active'));
87
+ this.classList.add('active');
88
+
89
+ // Show the selected section
90
+ const targetSection = this.getAttribute('data-section');
91
+ sections.forEach(section => {
92
+ section.classList.remove('active');
93
+ if (section.id === targetSection) {
94
+ section.classList.add('active');
95
+ }
96
+ });
97
+ });
98
+ });
99
+
100
+ // Image Upload Handling
101
+ uploadArea.addEventListener('click', function() {
102
+ imageUploadInput.click();
103
+ });
104
+
105
+ uploadArea.addEventListener('dragover', function(e) {
106
+ e.preventDefault();
107
+ this.classList.add('dragover');
108
+ });
109
+
110
+ uploadArea.addEventListener('dragleave', function() {
111
+ this.classList.remove('dragover');
112
+ });
113
+
114
+ uploadArea.addEventListener('drop', function(e) {
115
+ e.preventDefault();
116
+ this.classList.remove('dragover');
117
+
118
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
119
+ handleImageUpload(e.dataTransfer.files[0]);
120
+ }
121
+ });
122
+
123
+ imageUploadInput.addEventListener('change', function() {
124
+ if (this.files && this.files[0]) {
125
+ handleImageUpload(this.files[0]);
126
+ }
127
+ });
128
+
129
+ function handleImageUpload(file) {
130
+ if (!file.type.match('image.*')) {
131
+ visionStatus.textContent = '❌ Please upload an image file';
132
+ return;
133
+ }
134
+
135
+ const reader = new FileReader();
136
+ reader.onload = function(e) {
137
+ const img = document.createElement('img');
138
+ img.src = e.target.result;
139
+ uploadedImageContainer.innerHTML = '';
140
+ uploadedImageContainer.appendChild(img);
141
+ uploadedImage = file;
142
+ analyzeImageBtn.disabled = false;
143
+ visionStatus.textContent = '✅ Image uploaded successfully';
144
+ };
145
+ reader.readAsDataURL(file);
146
+ }
147
+
148
+ // Image Generation
149
+ generateImageBtn.addEventListener('click', function() {
150
+ const prompt = imagePromptInput.value.trim();
151
+ if (!prompt) {
152
+ generationStatus.textContent = '❌ Please enter a prompt';
153
+ return;
154
+ }
155
+
156
+ const apiKey = localStorage.getItem(API_KEY_STORAGE_KEY);
157
+ if (!apiKey) {
158
+ generationStatus.textContent = '❌ API key is required. Please add your API key in the API Key section.';
159
+ return;
160
+ }
161
+
162
+ // Show loading state
163
+ generationStatus.innerHTML = '<span class="loading"></span> Generating image...';
164
+ generateImageBtn.disabled = true;
165
+ imageResult.innerHTML = '';
166
+
167
+ // Call the backend API with OpenAI-style format
168
+ fetch('/api/generate-image', {
169
+ method: 'POST',
170
+ headers: {
171
+ 'Content-Type': 'application/json'
172
+ },
173
+ body: JSON.stringify({
174
+ api_key: apiKey,
175
+ prompt: prompt,
176
+ use_openai_format: true
177
+ })
178
+ })
179
+ .then(response => {
180
+ if (!response.ok) {
181
+ return response.json().then(err => { throw err; });
182
+ }
183
+ return response.json();
184
+ })
185
+ .then(data => {
186
+ if (data.image_url) {
187
+ // Display the generated image
188
+ let imageHTML = `<img src="${data.image_url}" alt="Generated image">`;
189
+
190
+ // Add caption if available
191
+ if (data.caption) {
192
+ imageHTML += `<div class="image-caption">${data.caption}</div>`;
193
+ }
194
+
195
+ imageResult.innerHTML = imageHTML;
196
+ generationStatus.textContent = '✅ Image generated successfully';
197
+ } else {
198
+ throw new Error('No image URL in response');
199
+ }
200
+ })
201
+ .catch(error => {
202
+ generationStatus.textContent = `❌ Error: ${error.error || error.message || 'Unknown error'}`;
203
+ console.error('Image generation error:', error);
204
+ })
205
+ .finally(() => {
206
+ generateImageBtn.disabled = false;
207
+ });
208
+ });
209
+
210
+ // Vision Analysis
211
+ analyzeImageBtn.addEventListener('click', function() {
212
+ if (!uploadedImage) {
213
+ visionStatus.textContent = '❌ Please upload an image first';
214
+ return;
215
+ }
216
+
217
+ const prompt = visionPromptInput.value.trim() || 'Describe this image in detail';
218
+ const apiKey = localStorage.getItem(API_KEY_STORAGE_KEY);
219
+ if (!apiKey) {
220
+ visionStatus.textContent = '❌ API key is required. Please add your API key in the API Key section.';
221
+ return;
222
+ }
223
+
224
+ // Show loading state
225
+ visionStatus.innerHTML = '<span class="loading"></span> Analyzing image...';
226
+ analyzeImageBtn.disabled = true;
227
+ visionResult.textContent = '';
228
+
229
+ // Create form data for file upload
230
+ const formData = new FormData();
231
+ formData.append('image', uploadedImage);
232
+ formData.append('api_key', apiKey);
233
+ formData.append('prompt', prompt);
234
+
235
+ // Call the backend API
236
+ fetch('/api/analyze-image', {
237
+ method: 'POST',
238
+ body: formData
239
+ })
240
+ .then(response => {
241
+ if (!response.ok) {
242
+ return response.json().then(err => { throw err; });
243
+ }
244
+ return response.json();
245
+ })
246
+ .then(data => {
247
+ if (data.analysis) {
248
+ // Display the analysis result with Markdown support
249
+ visionResult.innerHTML = marked.parse(data.analysis);
250
+ visionStatus.textContent = '✅ Analysis completed';
251
+ } else {
252
+ throw new Error('No analysis in response');
253
+ }
254
+ })
255
+ .catch(error => {
256
+ visionStatus.textContent = `❌ Error: ${error.error || error.message || 'Unknown error'}`;
257
+ console.error('Vision analysis error:', error);
258
+ })
259
+ .finally(() => {
260
+ analyzeImageBtn.disabled = false;
261
+ });
262
+ });
263
+ });
static/style.css ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Global styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ background-color: #f8f9fa;
13
+ }
14
+
15
+ .container {
16
+ width: 95%;
17
+ max-width: 1200px;
18
+ margin: 0 auto;
19
+ padding: 0 20px;
20
+ }
21
+
22
+ a {
23
+ text-decoration: none;
24
+ color: #3498db;
25
+ }
26
+
27
+ /* Header styles */
28
+ header {
29
+ background-color: #1a1a1a;
30
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
31
+ padding: 20px 0;
32
+ position: sticky;
33
+ top: 0;
34
+ z-index: 100;
35
+ }
36
+
37
+ header .container {
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
+ }
42
+
43
+ .logo {
44
+ font-size: 1.5rem;
45
+ font-weight: 700;
46
+ color: #ffffff;
47
+ }
48
+
49
+ nav ul {
50
+ display: flex;
51
+ list-style: none;
52
+ }
53
+
54
+ nav ul li {
55
+ margin-left: 30px;
56
+ }
57
+
58
+ nav ul li a {
59
+ color: #ffffff;
60
+ font-weight: 500;
61
+ transition: color 0.3s;
62
+ opacity: 0.8;
63
+ }
64
+
65
+ nav ul li a:hover,
66
+ nav ul li a.active {
67
+ color: #3498db;
68
+ opacity: 1;
69
+ }
70
+
71
+ /* Main section styles */
72
+ main {
73
+ padding: 40px 0;
74
+ }
75
+
76
+ .section {
77
+ display: none;
78
+ padding: 20px 0;
79
+ }
80
+
81
+ .section.active {
82
+ display: block;
83
+ }
84
+
85
+ /* API Key Section */
86
+ .api-key-container {
87
+ background-color: #fff;
88
+ border-radius: 10px;
89
+ padding: 30px;
90
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
91
+ max-width: 800px;
92
+ margin: 0 auto;
93
+ }
94
+
95
+ .input-group {
96
+ margin: 20px 0;
97
+ display: flex;
98
+ flex-wrap: wrap;
99
+ gap: 10px;
100
+ }
101
+
102
+ input[type="password"] {
103
+ flex: 1;
104
+ padding: 12px 15px;
105
+ border: 1px solid #ddd;
106
+ border-radius: 5px;
107
+ font-size: 16px;
108
+ min-width: 250px;
109
+ }
110
+
111
+ .btn {
112
+ display: inline-block;
113
+ background-color: #3498db;
114
+ color: white;
115
+ padding: 12px 20px;
116
+ border-radius: 5px;
117
+ font-weight: 500;
118
+ transition: background-color 0.3s;
119
+ border: none;
120
+ cursor: pointer;
121
+ }
122
+
123
+ .btn:hover {
124
+ background-color: #2980b9;
125
+ }
126
+
127
+ .btn-secondary {
128
+ background-color: #e74c3c;
129
+ }
130
+
131
+ .btn-secondary:hover {
132
+ background-color: #c0392b;
133
+ }
134
+
135
+ #api-key-status {
136
+ margin-top: 15px;
137
+ font-weight: 500;
138
+ }
139
+
140
+ /* Model Cards */
141
+ .model-card {
142
+ background-color: #fff;
143
+ border-radius: 10px;
144
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
145
+ overflow: hidden;
146
+ margin-bottom: 30px;
147
+ }
148
+
149
+ .model-info {
150
+ padding: 25px;
151
+ background-color: #2c3e50;
152
+ color: #fff;
153
+ }
154
+
155
+ .model-info h2 {
156
+ margin-bottom: 10px;
157
+ font-size: 1.8rem;
158
+ }
159
+
160
+ .model-info p {
161
+ color: rgba(255, 255, 255, 0.8);
162
+ margin-bottom: 10px;
163
+ }
164
+
165
+ .model-info p:first-of-type {
166
+ font-weight: 600;
167
+ color: #3498db;
168
+ }
169
+
170
+ /* Input Container */
171
+ .input-container {
172
+ padding: 25px;
173
+ border-bottom: 1px solid #eee;
174
+ }
175
+
176
+ textarea {
177
+ width: 100%;
178
+ padding: 15px;
179
+ border: 1px solid #ddd;
180
+ border-radius: 5px;
181
+ font-size: 16px;
182
+ min-height: 120px;
183
+ margin-bottom: 15px;
184
+ resize: vertical;
185
+ }
186
+
187
+ /* Result Container */
188
+ .result-container {
189
+ padding: 25px;
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 20px;
193
+ }
194
+
195
+ .result-box {
196
+ min-height: 100px;
197
+ padding: 15px;
198
+ border: 1px solid #ddd;
199
+ border-radius: 5px;
200
+ background-color: #f9f9f9;
201
+ }
202
+
203
+ .status {
204
+ font-style: italic;
205
+ color: #7f8c8d;
206
+ }
207
+
208
+ /* Image Upload */
209
+ .upload-area {
210
+ border: 2px dashed #3498db;
211
+ border-radius: 5px;
212
+ padding: 40px 20px;
213
+ text-align: center;
214
+ margin-bottom: 15px;
215
+ cursor: pointer;
216
+ transition: background-color 0.3s;
217
+ }
218
+
219
+ .upload-area:hover {
220
+ background-color: rgba(52, 152, 219, 0.1);
221
+ }
222
+
223
+ .upload-area.dragover {
224
+ background-color: rgba(52, 152, 219, 0.2);
225
+ border-color: #2980b9;
226
+ }
227
+
228
+ .uploaded-image {
229
+ max-width: 100%;
230
+ margin-bottom: 20px;
231
+ display: flex;
232
+ justify-content: center;
233
+ }
234
+
235
+ .uploaded-image img {
236
+ max-width: 100%;
237
+ max-height: 400px;
238
+ border-radius: 5px;
239
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
240
+ }
241
+
242
+ /* Footer */
243
+ footer {
244
+ background-color: #2c3e50;
245
+ color: #ecf0f1;
246
+ padding: 30px 0;
247
+ text-align: center;
248
+ }
249
+
250
+ footer p {
251
+ margin-bottom: 10px;
252
+ opacity: 0.8;
253
+ }
254
+
255
+ /* Loading spinner */
256
+ .loading {
257
+ display: inline-block;
258
+ width: 20px;
259
+ height: 20px;
260
+ border: 3px solid rgba(255,255,255,.3);
261
+ border-radius: 50%;
262
+ border-top-color: #3498db;
263
+ animation: spin 1s ease-in-out infinite;
264
+ }
265
+
266
+ @keyframes spin {
267
+ to { transform: rotate(360deg); }
268
+ }
269
+
270
+ /* Responsive design */
271
+ @media (max-width: 768px) {
272
+ header .container {
273
+ flex-direction: column;
274
+ }
275
+
276
+ nav ul {
277
+ margin-top: 20px;
278
+ justify-content: center;
279
+ flex-wrap: wrap;
280
+ }
281
+
282
+ nav ul li {
283
+ margin: 5px 15px;
284
+ }
285
+
286
+ .input-group {
287
+ flex-direction: column;
288
+ }
289
+
290
+ .input-group input,
291
+ .input-group button {
292
+ width: 100%;
293
+ }
294
+ }
295
+
296
+ /* Markdown styling */
297
+ #vision-result {
298
+ line-height: 1.2;
299
+ padding: 12px;
300
+ background-color: #f9f9f9;
301
+ border-radius: 8px;
302
+ overflow-wrap: break-word;
303
+ white-space: pre-line;
304
+ }
305
+
306
+ #vision-result h1,
307
+ #vision-result h2,
308
+ #vision-result h3,
309
+ #vision-result h4 {
310
+ margin-top: 0.4em;
311
+ margin-bottom: 0.2em;
312
+ color: #2a3990;
313
+ }
314
+
315
+ #vision-result p {
316
+ margin-bottom: 0.3em;
317
+ white-space: pre-line;
318
+ }
319
+
320
+ #vision-result ul,
321
+ #vision-result ol {
322
+ margin-left: 15px;
323
+ margin-bottom: 0.3em;
324
+ margin-top: 0.2em;
325
+ }
326
+
327
+ #vision-result li {
328
+ margin-bottom: 0em;
329
+ line-height: 1.2;
330
+ padding-bottom: 0.1em;
331
+ }
332
+
333
+ #vision-result code {
334
+ background-color: #eaeaea;
335
+ padding: 2px 4px;
336
+ border-radius: 3px;
337
+ font-family: monospace;
338
+ }
339
+
340
+ #vision-result pre {
341
+ background-color: #eaeaea;
342
+ padding: 10px;
343
+ border-radius: 5px;
344
+ overflow-x: auto;
345
+ }
346
+
347
+ #vision-result blockquote {
348
+ border-left: 4px solid #2a3990;
349
+ padding-left: 15px;
350
+ margin-left: 0;
351
+ color: #555;
352
+ }
353
+
354
+ #vision-result img {
355
+ max-width: 100%;
356
+ height: auto;
357
+ }
358
+
359
+ /* Image caption styling */
360
+ .image-caption {
361
+ margin-top: 10px;
362
+ padding: 10px;
363
+ background-color: #f5f5f5;
364
+ border-radius: 5px;
365
+ font-size: 0.9em;
366
+ line-height: 1.4;
367
+ color: #333;
368
+ border-left: 3px solid #2a3990;
369
+ }
style.css CHANGED
@@ -1,28 +1,279 @@
 
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  font-size: 16px;
8
- margin-top: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
1
+ /* Global styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
  body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ background-color: #f8f9fa;
13
+ }
14
+
15
+ .container {
16
+ width: 95%;
17
+ max-width: 1200px;
18
+ margin: 0 auto;
19
+ padding: 0 20px;
20
+ }
21
+
22
+ a {
23
+ text-decoration: none;
24
+ color: #3498db;
25
+ }
26
+
27
+ /* Header styles */
28
+ header {
29
+ background-color: #1a1a1a;
30
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
31
+ padding: 20px 0;
32
+ position: sticky;
33
+ top: 0;
34
+ z-index: 100;
35
+ }
36
+
37
+ header .container {
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
+ }
42
+
43
+ .logo {
44
+ font-size: 1.5rem;
45
+ font-weight: 700;
46
+ color: #ffffff;
47
+ }
48
+
49
+ nav ul {
50
+ display: flex;
51
+ list-style: none;
52
+ }
53
+
54
+ nav ul li {
55
+ margin-left: 30px;
56
+ }
57
+
58
+ nav ul li a {
59
+ color: #ffffff;
60
+ font-weight: 500;
61
+ transition: color 0.3s;
62
+ opacity: 0.8;
63
  }
64
 
65
+ nav ul li a:hover,
66
+ nav ul li a.active {
67
+ color: #3498db;
68
+ opacity: 1;
69
+ }
70
+
71
+ /* Main section styles */
72
+ main {
73
+ padding: 40px 0;
74
+ }
75
+
76
+ .section {
77
+ display: none;
78
+ padding: 20px 0;
79
+ }
80
+
81
+ .section.active {
82
+ display: block;
83
+ }
84
+
85
+ /* API Key Section */
86
+ .api-key-container {
87
+ background-color: #fff;
88
+ border-radius: 10px;
89
+ padding: 30px;
90
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
91
+ max-width: 800px;
92
+ margin: 0 auto;
93
+ }
94
+
95
+ .input-group {
96
+ margin: 20px 0;
97
+ display: flex;
98
+ flex-wrap: wrap;
99
+ gap: 10px;
100
+ }
101
+
102
+ input[type="password"] {
103
+ flex: 1;
104
+ padding: 12px 15px;
105
+ border: 1px solid #ddd;
106
+ border-radius: 5px;
107
  font-size: 16px;
108
+ min-width: 250px;
109
+ }
110
+
111
+ .btn {
112
+ display: inline-block;
113
+ background-color: #3498db;
114
+ color: white;
115
+ padding: 12px 20px;
116
+ border-radius: 5px;
117
+ font-weight: 500;
118
+ transition: background-color 0.3s;
119
+ border: none;
120
+ cursor: pointer;
121
+ }
122
+
123
+ .btn:hover {
124
+ background-color: #2980b9;
125
+ }
126
+
127
+ .btn-secondary {
128
+ background-color: #e74c3c;
129
  }
130
 
131
+ .btn-secondary:hover {
132
+ background-color: #c0392b;
133
+ }
134
+
135
+ #api-key-status {
136
+ margin-top: 15px;
137
+ font-weight: 500;
138
+ }
139
+
140
+ /* Model Cards */
141
+ .model-card {
142
+ background-color: #fff;
143
+ border-radius: 10px;
144
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
145
+ overflow: hidden;
146
+ margin-bottom: 30px;
147
+ }
148
+
149
+ .model-info {
150
+ padding: 25px;
151
+ background-color: #2c3e50;
152
+ color: #fff;
153
+ }
154
+
155
+ .model-info h2 {
156
  margin-bottom: 10px;
157
+ font-size: 1.8rem;
158
  }
159
 
160
+ .model-info p {
161
+ color: rgba(255, 255, 255, 0.8);
162
+ margin-bottom: 10px;
163
+ }
164
+
165
+ .model-info p:first-of-type {
166
+ font-weight: 600;
167
+ color: #3498db;
168
+ }
169
+
170
+ /* Input Container */
171
+ .input-container {
172
+ padding: 25px;
173
+ border-bottom: 1px solid #eee;
174
+ }
175
+
176
+ textarea {
177
+ width: 100%;
178
+ padding: 15px;
179
+ border: 1px solid #ddd;
180
+ border-radius: 5px;
181
+ font-size: 16px;
182
+ min-height: 120px;
183
+ margin-bottom: 15px;
184
+ resize: vertical;
185
+ }
186
+
187
+ /* Result Container */
188
+ .result-container {
189
+ padding: 25px;
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 20px;
193
+ }
194
+
195
+ .result-box {
196
+ min-height: 100px;
197
+ padding: 15px;
198
+ border: 1px solid #ddd;
199
+ border-radius: 5px;
200
+ background-color: #f9f9f9;
201
+ }
202
+
203
+ .status {
204
+ font-style: italic;
205
+ color: #7f8c8d;
206
+ }
207
+
208
+ /* Image Upload */
209
+ .upload-area {
210
+ border: 2px dashed #3498db;
211
+ border-radius: 5px;
212
+ padding: 40px 20px;
213
+ text-align: center;
214
+ margin-bottom: 15px;
215
+ cursor: pointer;
216
+ transition: background-color 0.3s;
217
+ }
218
+
219
+ .upload-area:hover {
220
+ background-color: rgba(52, 152, 219, 0.1);
221
+ }
222
+
223
+ .upload-area.dragover {
224
+ background-color: rgba(52, 152, 219, 0.2);
225
+ border-color: #2980b9;
226
+ }
227
+
228
+ .uploaded-image {
229
+ max-width: 100%;
230
+ margin-bottom: 20px;
231
+ display: flex;
232
+ justify-content: center;
233
+ }
234
+
235
+ .uploaded-image img {
236
+ max-width: 100%;
237
+ max-height: 400px;
238
+ border-radius: 5px;
239
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
240
+ }
241
+
242
+ /* Footer */
243
+ footer {
244
+ background-color: #2c3e50;
245
+ color: #ecf0f1;
246
+ padding: 30px 0;
247
+ text-align: center;
248
+ }
249
+
250
+ footer p {
251
+ margin-bottom: 10px;
252
+ opacity: 0.8;
253
  }
254
 
255
+ /* Responsive design */
256
+ @media (max-width: 768px) {
257
+ header .container {
258
+ flex-direction: column;
259
+ }
260
+
261
+ nav ul {
262
+ margin-top: 20px;
263
+ justify-content: center;
264
+ flex-wrap: wrap;
265
+ }
266
+
267
+ nav ul li {
268
+ margin: 5px 15px;
269
+ }
270
+
271
+ .input-group {
272
+ flex-direction: column;
273
+ }
274
+
275
+ .input-group input,
276
+ .input-group button {
277
+ width: 100%;
278
+ }
279
  }