DucLai commited on
Commit
4f29e7e
·
verified ·
1 Parent(s): 702fdef

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +0 -10
  2. requirements.txt +1 -0
  3. script.js +329 -0
  4. style.css +349 -28
README.md CHANGED
@@ -1,10 +0,0 @@
1
- ---
2
- title: Sketch2Render
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: gray
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ No Packages Require
script.js ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class SketchToRenderApp {
2
+ constructor() {
3
+ this.canvas = document.getElementById('drawingCanvas');
4
+ this.ctx = this.canvas.getContext('2d');
5
+ this.isDrawing = false;
6
+ this.drawingHistory = [];
7
+ this.currentPath = [];
8
+ this.renderedImageUrl = null;
9
+
10
+ // API Key được nhúng trực tiếp trong code
11
+ this.apiKey = 'AIzaSyCinF1_Z7XXPzu_zvMzifKyPIy7i7eUBGc'; // Thay thế bằng API key thực tế
12
+
13
+ this.initializeCanvas();
14
+ this.setupEventListeners();
15
+ this.saveCanvasState();
16
+ }
17
+
18
+ initializeCanvas() {
19
+ this.ctx.lineCap = 'round';
20
+ this.ctx.lineJoin = 'round';
21
+ this.ctx.fillStyle = 'white';
22
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
23
+ }
24
+
25
+ setupEventListeners() {
26
+ // Canvas drawing events
27
+ this.canvas.addEventListener('mousedown', (e) => this.startDrawing(e));
28
+ this.canvas.addEventListener('mousemove', (e) => this.draw(e));
29
+ this.canvas.addEventListener('mouseup', () => this.stopDrawing());
30
+ this.canvas.addEventListener('mouseout', () => this.stopDrawing());
31
+
32
+ // Touch events for mobile
33
+ this.canvas.addEventListener('touchstart', (e) => {
34
+ e.preventDefault();
35
+ const touch = e.touches[0];
36
+ const mouseEvent = new MouseEvent('mousedown', {
37
+ clientX: touch.clientX,
38
+ clientY: touch.clientY
39
+ });
40
+ this.canvas.dispatchEvent(mouseEvent);
41
+ });
42
+
43
+ this.canvas.addEventListener('touchmove', (e) => {
44
+ e.preventDefault();
45
+ const touch = e.touches[0];
46
+ const mouseEvent = new MouseEvent('mousemove', {
47
+ clientX: touch.clientX,
48
+ clientY: touch.clientY
49
+ });
50
+ this.canvas.dispatchEvent(mouseEvent);
51
+ });
52
+
53
+ this.canvas.addEventListener('touchend', (e) => {
54
+ e.preventDefault();
55
+ const mouseEvent = new MouseEvent('mouseup', {});
56
+ this.canvas.dispatchEvent(mouseEvent);
57
+ });
58
+
59
+ // Control events
60
+ document.getElementById('brushSize').addEventListener('input', (e) => {
61
+ document.getElementById('brushSizeValue').textContent = e.target.value + 'px';
62
+ });
63
+
64
+ document.getElementById('clearCanvas').addEventListener('click', () => {
65
+ this.clearCanvas();
66
+ });
67
+
68
+ document.getElementById('undoBtn').addEventListener('click', () => {
69
+ this.undo();
70
+ });
71
+
72
+ document.getElementById('generateBtn').addEventListener('click', () => {
73
+ this.generateRender();
74
+ });
75
+
76
+ document.getElementById('downloadBtn').addEventListener('click', () => {
77
+ this.downloadImage();
78
+ });
79
+ }
80
+
81
+ getMousePos(e) {
82
+ const rect = this.canvas.getBoundingClientRect();
83
+ return {
84
+ x: e.clientX - rect.left,
85
+ y: e.clientY - rect.top
86
+ };
87
+ }
88
+
89
+ startDrawing(e) {
90
+ this.isDrawing = true;
91
+ const pos = this.getMousePos(e);
92
+ this.currentPath = [pos];
93
+
94
+ this.ctx.strokeStyle = document.getElementById('brushColor').value;
95
+ this.ctx.lineWidth = document.getElementById('brushSize').value;
96
+
97
+ this.ctx.beginPath();
98
+ this.ctx.moveTo(pos.x, pos.y);
99
+ }
100
+
101
+ draw(e) {
102
+ if (!this.isDrawing) return;
103
+
104
+ const pos = this.getMousePos(e);
105
+ this.currentPath.push(pos);
106
+
107
+ this.ctx.lineTo(pos.x, pos.y);
108
+ this.ctx.stroke();
109
+ }
110
+
111
+ stopDrawing() {
112
+ if (this.isDrawing) {
113
+ this.isDrawing = false;
114
+ this.saveCanvasState();
115
+ }
116
+ }
117
+
118
+ saveCanvasState() {
119
+ this.drawingHistory.push(this.canvas.toDataURL());
120
+ if (this.drawingHistory.length > 20) {
121
+ this.drawingHistory.shift();
122
+ }
123
+ }
124
+
125
+ clearCanvas() {
126
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
127
+ this.ctx.fillStyle = 'white';
128
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
129
+ this.saveCanvasState();
130
+ }
131
+
132
+ undo() {
133
+ if (this.drawingHistory.length > 1) {
134
+ this.drawingHistory.pop();
135
+ const previousState = this.drawingHistory[this.drawingHistory.length - 1];
136
+ const img = new Image();
137
+ img.onload = () => {
138
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
139
+ this.ctx.drawImage(img, 0, 0);
140
+ };
141
+ img.src = previousState;
142
+ }
143
+ }
144
+
145
+ async generateRender() {
146
+ if (!this.apiKey || this.apiKey === 'YOUR_GEMINI_API_KEY_HERE') {
147
+ this.showError('Vui lòng cấu hình API Key trong source code');
148
+ return;
149
+ }
150
+
151
+ // Check if canvas is empty
152
+ if (this.isCanvasEmpty()) {
153
+ this.showError('Vui lòng vẽ gì đó trước khi tạo render');
154
+ return;
155
+ }
156
+
157
+ this.showLoading(true);
158
+ this.hideError();
159
+
160
+ try {
161
+ // Step 1: Analyze sketch with Gemini
162
+ const analysisResult = await this.analyzeSketchWithGemini();
163
+ console.log('Analysis result:', analysisResult);
164
+
165
+ // Step 2: Generate image using the description from Gemini
166
+ const imageUrl = await this.generateImageFromDescription(analysisResult.description);
167
+
168
+ // Step 3: Display results
169
+ this.displayResults(imageUrl, analysisResult.score, analysisResult.prediction);
170
+
171
+ } catch (error) {
172
+ console.error('Error:', error);
173
+ this.showError(`Lỗi: ${error.message}`);
174
+ } finally {
175
+ this.showLoading(false);
176
+ }
177
+ }
178
+
179
+ isCanvasEmpty() {
180
+ const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
181
+ return imageData.data.every((pixel, index) => {
182
+ if ((index + 1) % 4 === 0) return true; // Alpha channel
183
+ return pixel === 255; // White pixels
184
+ });
185
+ }
186
+
187
+ async analyzeSketchWithGemini() {
188
+ const canvasDataURL = this.canvas.toDataURL('image/png');
189
+ const base64Data = canvasDataURL.split(',')[1];
190
+
191
+ const prompt = `Phân tích bức vẽ sketch này và cung cấp:
192
+
193
+ 1. ĐIỂM SỐ: Đánh giá chất lượng và độ rõ ràng từ 1–10, với tiêu chuẩn thoải mái (hình được vẽ bởi người bình thường bằng bút đen trên canvas, không yêu cầu đường nét hoàn hảo).
194
+ 2. DỰ ĐOÁN: Tên ngắn gọn của sự vật được vẽ (VD: "Mèo", "Ngôi nhà", "Hoa")
195
+ 3. MÔ TẢ: Mô tả ngắn gọn để tạo ảnh render (viết bằng tiếng Anh, phong cách artistic)
196
+
197
+ Định dạng trả lời:
198
+ ĐIỂM: [số]
199
+ DỰ ĐOÁN: [tên sự vật]
200
+ MÔ TẢ: [mô tả ngắn gọn bằng tiếng Anh]`;
201
+
202
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${this.apiKey}`, {
203
+ method: 'POST',
204
+ headers: {
205
+ 'Content-Type': 'application/json',
206
+ },
207
+ body: JSON.stringify({
208
+ contents: [{
209
+ parts: [
210
+ { text: prompt },
211
+ {
212
+ inline_data: {
213
+ mime_type: "image/png",
214
+ data: base64Data
215
+ }
216
+ }
217
+ ]
218
+ }]
219
+ })
220
+ });
221
+
222
+ if (!response.ok) {
223
+ const errorData = await response.json();
224
+ throw new Error(`Gemini API Error: ${errorData.error?.message || 'Unknown error'}`);
225
+ }
226
+
227
+ const data = await response.json();
228
+
229
+ if (data.candidates && data.candidates.length > 0) {
230
+ const result = data.candidates[0].content.parts[0].text;
231
+ return this.parseGeminiResponse(result);
232
+ } else {
233
+ throw new Error('Không có kết quả từ Gemini API');
234
+ }
235
+ }
236
+
237
+ parseGeminiResponse(text) {
238
+ console.log('Gemini response:', text);
239
+
240
+ // Extract score
241
+ const scoreMatch = text.match(/ĐIỂM[:\s]*(\d+)/i) || text.match(/score[:\s]*(\d+)/i);
242
+ const score = scoreMatch ? parseInt(scoreMatch[1]) : 7;
243
+
244
+ // Extract prediction
245
+ const predictionMatch = text.match(/DỰ ĐOÁN[:\s]*([^\n]+)/i) || text.match(/prediction[:\s]*([^\n]+)/i);
246
+ const prediction = predictionMatch ? predictionMatch[1].trim() : 'Không xác định';
247
+
248
+ // Extract description
249
+ const descriptionMatch = text.match(/MÔ TẢ[:\s]*([^]*)/i) || text.match(/description[:\s]*([^]*)/i);
250
+ let description = descriptionMatch ? descriptionMatch[1].trim() : text;
251
+
252
+ // If no specific description found, use the whole response
253
+ if (!descriptionMatch) {
254
+ // Try to extract the most descriptive part
255
+ const lines = text.split('\n').filter(line => line.trim().length > 20);
256
+ description = lines.length > 0 ? lines[lines.length - 1] : text;
257
+ }
258
+
259
+ return {
260
+ score: Math.min(Math.max(score, 1), 10), // Ensure score is between 1-10
261
+ prediction: prediction.replace(/^["']|["']$/g, ''), // Remove quotes
262
+ description: description.replace(/^["']|["']$/g, '') // Remove quotes
263
+ };
264
+ }
265
+
266
+ async generateImageFromDescription(description) {
267
+ // Using Pollinations AI (free image generation API)
268
+ // This creates an image based on the description from Gemini
269
+ const enhancedPrompt = `${description}, high quality, detailed artwork, professional illustration`;
270
+ const encodedPrompt = encodeURIComponent(enhancedPrompt);
271
+
272
+ // Generate a unique seed to avoid caching
273
+ const seed = Math.floor(Math.random() * 1000000);
274
+ const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?seed=${seed}&width=512&height=512&model=flux`;
275
+
276
+ // Verify the image loads
277
+ return new Promise((resolve, reject) => {
278
+ const img = new Image();
279
+ img.onload = () => resolve(imageUrl);
280
+ img.onerror = () => reject(new Error('Không thể tạo ảnh render'));
281
+ img.src = imageUrl;
282
+ });
283
+ }
284
+
285
+ displayResults(imageUrl, score, prediction) {
286
+ // Display rendered image
287
+ const imageContainer = document.getElementById('imageContainer');
288
+ imageContainer.innerHTML = `<img src="${imageUrl}" alt="Rendered Image" class="rendered-image">`;
289
+
290
+ this.renderedImageUrl = imageUrl;
291
+ document.getElementById('downloadBtn').style.display = 'block';
292
+
293
+ // Display results
294
+ document.getElementById('scoreDisplay').textContent = `${score}/10`;
295
+ document.getElementById('predictionText').textContent = prediction;
296
+
297
+ // Show results
298
+ document.getElementById('resultContent').style.display = 'block';
299
+ }
300
+
301
+ downloadImage() {
302
+ if (this.renderedImageUrl) {
303
+ const link = document.createElement('a');
304
+ link.download = `sketch-render-${Date.now()}.png`;
305
+ link.href = this.renderedImageUrl;
306
+ link.click();
307
+ }
308
+ }
309
+
310
+ showLoading(show) {
311
+ document.getElementById('loading').style.display = show ? 'block' : 'none';
312
+ document.getElementById('resultContent').style.display = show ? 'none' : 'block';
313
+ }
314
+
315
+ showError(message) {
316
+ const errorElement = document.getElementById('errorMessage');
317
+ errorElement.textContent = message;
318
+ errorElement.style.display = 'block';
319
+ }
320
+
321
+ hideError() {
322
+ document.getElementById('errorMessage').style.display = 'none';
323
+ }
324
+ }
325
+
326
+ // Initialize the app when the page loads
327
+ document.addEventListener('DOMContentLoaded', () => {
328
+ new SketchToRenderApp();
329
+ });
style.css CHANGED
@@ -1,28 +1,349 @@
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
+ * {
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: 1600px;
16
+ margin: 0 auto;
17
+ background: white;
18
+ border-radius: 20px;
19
+ box-shadow: 0 20px 60px rgba(0,0,0,0.1);
20
+ overflow: hidden;
21
+ }
22
+
23
+ .header {
24
+ background: linear-gradient(135deg, #4285f4 0%, #34a853 50%, #fbbc05 75%, #ea4335 100%);
25
+ color: white;
26
+ padding: 30px;
27
+ text-align: center;
28
+ }
29
+
30
+ .header h1 {
31
+ font-size: 2.5rem;
32
+ margin-bottom: 10px;
33
+ font-weight: 700;
34
+ }
35
+
36
+ .header p {
37
+ font-size: 1.1rem;
38
+ opacity: 0.9;
39
+ }
40
+
41
+ .main-content {
42
+ display: grid;
43
+ grid-template-columns: 500px 1fr;
44
+ gap: 30px;
45
+ padding: 30px;
46
+ }
47
+
48
+ .drawing-section {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: 20px;
52
+ }
53
+
54
+ .canvas-container {
55
+ position: relative;
56
+ border: 3px solid #e0e0e0;
57
+ border-radius: 15px;
58
+ overflow: hidden;
59
+ background: #fafafa;
60
+ box-shadow: inset 0 2px 10px rgba(0,0,0,0.05);
61
+ }
62
+
63
+ #drawingCanvas {
64
+ display: block;
65
+ cursor: crosshair;
66
+ background: white;
67
+ }
68
+
69
+ .controls {
70
+ display: flex;
71
+ gap: 15px;
72
+ flex-wrap: wrap;
73
+ align-items: center;
74
+ padding: 20px;
75
+ background: #f8f9fa;
76
+ border-radius: 15px;
77
+ border: 1px solid #e9ecef;
78
+ }
79
+
80
+ .control-group {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 10px;
84
+ }
85
+
86
+ .control-group label {
87
+ font-weight: 600;
88
+ color: #495057;
89
+ font-size: 0.9rem;
90
+ }
91
+
92
+ input[type="range"] {
93
+ width: 100px;
94
+ accent-color: #4285f4;
95
+ }
96
+
97
+ input[type="color"] {
98
+ width: 40px;
99
+ height: 40px;
100
+ border: none;
101
+ border-radius: 8px;
102
+ cursor: pointer;
103
+ }
104
+
105
+ .btn {
106
+ padding: 12px 24px;
107
+ border: none;
108
+ border-radius: 10px;
109
+ font-weight: 600;
110
+ cursor: pointer;
111
+ transition: all 0.3s ease;
112
+ font-size: 0.9rem;
113
+ }
114
+
115
+ .btn-primary {
116
+ background: linear-gradient(135deg, #4285f4 0%, #34a853 100%);
117
+ color: white;
118
+ }
119
+
120
+ .btn-primary:hover {
121
+ transform: translateY(-2px);
122
+ box-shadow: 0 8px 25px rgba(66, 133, 244, 0.4);
123
+ }
124
+
125
+ .btn-secondary {
126
+ background: #6c757d;
127
+ color: white;
128
+ }
129
+
130
+ .btn-secondary:hover {
131
+ background: #5a6268;
132
+ transform: translateY(-2px);
133
+ }
134
+
135
+ .results-section {
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: 20px;
139
+ }
140
+
141
+ .generate-section {
142
+ background: #f8f9fa;
143
+ padding: 20px;
144
+ border-radius: 15px;
145
+ border: 1px solid #e9ecef;
146
+ text-align: center;
147
+ }
148
+
149
+ .generate-section h3 {
150
+ color: #495057;
151
+ margin-bottom: 15px;
152
+ font-size: 1.2rem;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ gap: 10px;
157
+ }
158
+
159
+ .gemini-logo {
160
+ font-size: 1.5rem;
161
+ }
162
+
163
+ .render-results {
164
+ background: #f8f9fa;
165
+ padding: 20px;
166
+ border-radius: 15px;
167
+ border: 1px solid #e9ecef;
168
+ min-height: 600px;
169
+ }
170
+
171
+ .render-results h3 {
172
+ color: #495057;
173
+ margin-bottom: 20px;
174
+ font-size: 1.2rem;
175
+ }
176
+
177
+ .loading {
178
+ display: none;
179
+ text-align: center;
180
+ padding: 40px;
181
+ }
182
+
183
+ .loading-spinner {
184
+ width: 50px;
185
+ height: 50px;
186
+ border: 4px solid #e9ecef;
187
+ border-top: 4px solid #4285f4;
188
+ border-radius: 50%;
189
+ animation: spin 1s linear infinite;
190
+ margin: 0 auto 20px;
191
+ }
192
+
193
+ @keyframes spin {
194
+ 0% { transform: rotate(0deg); }
195
+ 100% { transform: rotate(360deg); }
196
+ }
197
+
198
+ .result-content {
199
+ display: none;
200
+ }
201
+
202
+ .result-grid {
203
+ display: grid;
204
+ grid-template-columns: 1fr 300px;
205
+ gap: 20px;
206
+ }
207
+
208
+ .rendered-image-container {
209
+ background: white;
210
+ padding: 20px;
211
+ border-radius: 15px;
212
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
213
+ text-align: center;
214
+ }
215
+
216
+ .rendered-image-container h4 {
217
+ color: #495057;
218
+ margin-bottom: 15px;
219
+ font-size: 1.1rem;
220
+ }
221
+
222
+ .rendered-image {
223
+ max-width: 100%;
224
+ max-height: 400px;
225
+ border-radius: 10px;
226
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
227
+ margin-bottom: 15px;
228
+ }
229
+
230
+ .image-placeholder {
231
+ width: 100%;
232
+ height: 300px;
233
+ background: #f8f9fa;
234
+ border: 2px dashed #dee2e6;
235
+ border-radius: 10px;
236
+ display: flex;
237
+ align-items: center;
238
+ justify-content: center;
239
+ color: #6c757d;
240
+ font-size: 1.1rem;
241
+ text-align: center;
242
+ padding: 20px;
243
+ }
244
+
245
+ .result-info {
246
+ display: flex;
247
+ flex-direction: column;
248
+ gap: 15px;
249
+ }
250
+
251
+ .info-card {
252
+ background: white;
253
+ padding: 20px;
254
+ border-radius: 15px;
255
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
256
+ }
257
+
258
+ .info-card h4 {
259
+ color: #495057;
260
+ margin-bottom: 10px;
261
+ font-size: 1.1rem;
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 8px;
265
+ }
266
+
267
+ .score-display {
268
+ font-size: 2.5rem;
269
+ font-weight: bold;
270
+ color: #4285f4;
271
+ text-align: center;
272
+ padding: 20px;
273
+ background: linear-gradient(135deg, rgba(66, 133, 244, 0.1) 0%, rgba(52, 168, 83, 0.1) 100%);
274
+ border-radius: 10px;
275
+ }
276
+
277
+ .prediction-text {
278
+ font-size: 1.2rem;
279
+ color: #495057;
280
+ line-height: 1.6;
281
+ background: #f8f9fa;
282
+ padding: 15px;
283
+ border-radius: 8px;
284
+ border-left: 4px solid #4285f4;
285
+ text-align: center;
286
+ font-weight: 600;
287
+ }
288
+
289
+ .error-message {
290
+ color: #d93025;
291
+ background: #fce8e6;
292
+ padding: 15px;
293
+ border-radius: 8px;
294
+ border: 1px solid #f5c6cb;
295
+ margin: 10px 0;
296
+ }
297
+
298
+ .canvas-size-info {
299
+ font-size: 0.8rem;
300
+ color: #6c757d;
301
+ margin-top: 5px;
302
+ }
303
+
304
+ .download-btn {
305
+ background: #34a853;
306
+ color: white;
307
+ padding: 10px 20px;
308
+ border: none;
309
+ border-radius: 8px;
310
+ font-weight: 600;
311
+ cursor: pointer;
312
+ margin-top: 10px;
313
+ transition: all 0.3s ease;
314
+ }
315
+
316
+ .download-btn:hover {
317
+ background: #137333;
318
+ transform: translateY(-2px);
319
+ }
320
+
321
+ @media (max-width: 1200px) {
322
+ .main-content {
323
+ grid-template-columns: 1fr;
324
+ gap: 20px;
325
+ }
326
+
327
+ .result-grid {
328
+ grid-template-columns: 1fr;
329
+ }
330
+ }
331
+
332
+ @media (max-width: 768px) {
333
+ .main-content {
334
+ padding: 20px;
335
+ }
336
+
337
+ .header h1 {
338
+ font-size: 2rem;
339
+ }
340
+
341
+ .controls {
342
+ flex-direction: column;
343
+ align-items: stretch;
344
+ }
345
+
346
+ .control-group {
347
+ justify-content: space-between;
348
+ }
349
+ }