Really-amin commited on
Commit
b5c0b27
·
verified ·
1 Parent(s): eef1b32

Upload PromptForge v1.0 — Structured prompt generator for Google AI Studio

Browse files
Files changed (4) hide show
  1. backend/main.py +4 -1
  2. frontend/simple.css +177 -121
  3. frontend/simple.html +5 -7
  4. frontend/simple.js +224 -223
backend/main.py CHANGED
@@ -3,10 +3,13 @@ PromptForge v4.0 — FastAPI application
3
  Run: uvicorn main:app --reload --host 0.0.0.0 --port 7860
4
  """
5
  from __future__ import annotations
6
- import logging, os
7
  from pathlib import Path
8
  from typing import List, Optional
9
 
 
 
 
10
  from fastapi import FastAPI, HTTPException, Query, status, UploadFile, File
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import HTMLResponse, JSONResponse
 
3
  Run: uvicorn main:app --reload --host 0.0.0.0 --port 7860
4
  """
5
  from __future__ import annotations
6
+ import logging, os, sys
7
  from pathlib import Path
8
  from typing import List, Optional
9
 
10
+ # Add current directory to sys.path to allow running from root
11
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
12
+
13
  from fastapi import FastAPI, HTTPException, Query, status, UploadFile, File
14
  from fastapi.middleware.cors import CORSMiddleware
15
  from fastapi.responses import HTMLResponse, JSONResponse
frontend/simple.css CHANGED
@@ -1,15 +1,16 @@
1
  :root {
2
- --bg: #f8fafc;
3
- --surface: #ffffff;
4
- --border: #e2e8f0;
5
- --text: #0f172a;
6
- --text-muted: #475569;
 
7
  --accent: #3b82f6;
8
- --accent-light: #dbeafe;
9
  --success: #10b981;
10
  --error: #ef4444;
11
  --radius: 16px;
12
- --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
13
  --font: 'Inter', system-ui, sans-serif;
14
  }
15
 
@@ -38,41 +39,49 @@ body {
38
  display: flex;
39
  justify-content: space-between;
40
  align-items: center;
41
- margin-bottom: 32px;
42
  }
43
  .logo {
44
  display: flex;
45
  align-items: center;
46
- gap: 10px;
47
- font-weight: 600;
48
- font-size: 1.4rem;
 
 
 
 
49
  }
50
  .badge {
51
- background: var(--accent-light);
52
- color: var(--accent);
53
- padding: 4px 10px;
54
  border-radius: 40px;
55
- font-size: 0.8rem;
56
- font-weight: 500;
 
57
  margin-left: 8px;
 
58
  }
59
  .btn-outline {
60
  display: flex;
61
  align-items: center;
62
  gap: 8px;
63
- padding: 8px 14px;
64
- border: 1.5px solid var(--border);
65
  border-radius: 40px;
66
  background: var(--surface);
67
- color: var(--text-muted);
68
  text-decoration: none;
69
- font-size: 0.9rem;
70
- transition: all 0.2s;
 
71
  }
72
  .btn-outline:hover {
73
  border-color: var(--accent);
74
- color: var(--accent);
75
- box-shadow: var(--shadow);
 
76
  }
77
 
78
  /* Cards */
@@ -80,8 +89,8 @@ body {
80
  background: var(--surface);
81
  border: 1px solid var(--border);
82
  border-radius: var(--radius);
83
- padding: 20px;
84
- margin-bottom: 20px;
85
  box-shadow: var(--shadow);
86
  }
87
 
@@ -89,21 +98,22 @@ body {
89
  .key-header {
90
  display: flex;
91
  align-items: center;
92
- gap: 8px;
93
  cursor: pointer;
94
  user-select: none;
95
- font-weight: 500;
96
- color: var(--text-muted);
97
  }
98
  .key-header .chevron {
99
  margin-left: auto;
100
- transition: transform 0.2s;
 
101
  }
102
- .key-header.open .chevron {
103
  transform: rotate(180deg);
104
  }
105
  .key-body {
106
- margin-top: 16px;
107
  display: block;
108
  }
109
  .key-body.hidden {
@@ -111,17 +121,18 @@ body {
111
  }
112
  .input-group {
113
  display: flex;
114
- gap: 8px;
115
- margin-bottom: 10px;
116
  }
117
  .input-group input {
118
  flex: 1;
119
  background: var(--bg);
120
- border: 1.5px solid var(--border);
121
  border-radius: 12px;
122
- padding: 10px 14px;
 
123
  font-family: var(--font);
124
- font-size: 0.95rem;
125
  outline: none;
126
  transition: border-color 0.2s;
127
  }
@@ -129,10 +140,10 @@ body {
129
  border-color: var(--accent);
130
  }
131
  .icon-btn {
132
- width: 42px;
133
- height: 42px;
134
- background: var(--surface);
135
- border: 1.5px solid var(--border);
136
  border-radius: 12px;
137
  display: flex;
138
  align-items: center;
@@ -143,7 +154,7 @@ body {
143
  }
144
  .icon-btn:hover:not(:disabled) {
145
  border-color: var(--accent);
146
- color: var(--accent);
147
  }
148
  .icon-btn:disabled {
149
  opacity: 0.4;
@@ -153,50 +164,65 @@ body {
153
  display: flex;
154
  align-items: center;
155
  gap: 10px;
156
- font-size: 0.85rem;
157
  color: var(--text-muted);
158
  }
159
  .dot {
160
- width: 10px;
161
- height: 10px;
162
  border-radius: 50%;
163
  background: var(--text-muted);
164
  }
165
  .dot.ok {
166
  background: var(--success);
167
- box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
168
  }
169
  .dot.err {
170
  background: var(--error);
171
- box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
172
  }
173
  .btn-link {
174
  background: none;
175
  border: none;
176
  color: var(--accent);
177
  cursor: pointer;
178
- font-size: 0.85rem;
179
- text-decoration: underline;
180
  padding: 0 4px;
 
 
 
 
181
  }
182
 
183
  /* Model selector */
184
  .model-card label {
185
  display: block;
186
- margin-bottom: 8px;
187
- font-weight: 500;
188
  color: var(--text-muted);
 
 
 
189
  }
190
  .model-card select {
191
  width: 100%;
192
- background: var(--surface);
193
- border: 1.5px solid var(--border);
194
  border-radius: 12px;
195
- padding: 12px 16px;
196
  font-size: 1rem;
197
  color: var(--text);
198
  outline: none;
199
  cursor: pointer;
 
 
 
 
 
 
 
 
200
  }
201
 
202
  /* Input area */
@@ -205,27 +231,31 @@ body {
205
  }
206
  .textarea-wrapper textarea {
207
  width: 100%;
208
- background: var(--surface);
209
- border: 1.5px solid var(--border);
210
  border-radius: 16px;
211
- padding: 16px 50px 16px 16px;
212
  font-family: var(--font);
213
- font-size: 1rem;
 
214
  resize: vertical;
215
  outline: none;
 
 
216
  }
217
  .textarea-wrapper textarea:focus {
218
  border-color: var(--accent);
 
219
  }
220
  .mic-btn {
221
  position: absolute;
222
- bottom: 16px;
223
- right: 16px;
224
- background: var(--surface);
225
- border: 1.5px solid var(--border);
226
  border-radius: 12px;
227
- width: 40px;
228
- height: 40px;
229
  display: flex;
230
  align-items: center;
231
  justify-content: center;
@@ -235,16 +265,17 @@ body {
235
  }
236
  .mic-btn:hover {
237
  border-color: var(--accent);
238
- color: var(--accent);
239
  }
240
  .mic-btn.recording {
 
 
241
  border-color: var(--error);
242
- color: var(--error);
243
- animation: pulse 1s infinite;
244
  }
245
  @keyframes pulse {
246
  0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
247
- 70% { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
248
  100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
249
  }
250
 
@@ -253,89 +284,101 @@ body {
253
  width: 100%;
254
  background: var(--accent);
255
  border: none;
256
- border-radius: 40px;
257
- padding: 16px 24px;
258
- font-size: 1.2rem;
259
- font-weight: 600;
260
  color: white;
261
  display: flex;
262
  align-items: center;
263
  justify-content: center;
264
- gap: 10px;
265
  cursor: pointer;
266
- transition: opacity 0.2s, transform 0.1s;
267
- margin-bottom: 32px;
 
268
  }
269
  .btn-primary:hover {
270
- opacity: 0.9;
 
271
  }
272
  .btn-primary:active {
273
- transform: scale(0.98);
274
  }
275
  .btn-primary:disabled {
276
  opacity: 0.5;
277
  cursor: not-allowed;
 
278
  }
279
 
280
  /* Output area */
281
  .output-area {
282
- transition: all 0.3s;
 
 
 
 
283
  }
284
  .output-header {
285
  display: flex;
286
  align-items: center;
287
  justify-content: space-between;
288
- margin-bottom: 20px;
289
  }
290
  .output-header h3 {
291
- font-size: 1.1rem;
292
- font-weight: 600;
293
  }
294
  .btn-secondary {
295
- background: var(--surface);
296
- border: 1.5px solid var(--border);
297
- border-radius: 30px;
298
- padding: 8px 16px;
299
  display: flex;
300
  align-items: center;
301
  gap: 8px;
302
- font-size: 0.9rem;
303
- color: var(--text-muted);
 
304
  cursor: pointer;
305
  transition: all 0.2s;
306
  }
307
  .btn-secondary:hover {
308
- border-color: var(--accent);
309
- color: var(--accent);
310
  }
311
  .quad-sections {
312
- margin-bottom: 16px;
 
313
  }
314
  .quad-section {
315
- margin-bottom: 16px;
316
- border-bottom: 1px dashed var(--border);
317
- padding-bottom: 12px;
 
318
  }
319
  .section-header {
320
  display: flex;
321
  justify-content: space-between;
322
  align-items: center;
323
- margin-bottom: 6px;
324
  }
325
  .section-header strong {
326
  color: var(--accent);
327
  font-size: 0.8rem;
328
  text-transform: uppercase;
329
- letter-spacing: 0.5px;
 
330
  }
331
  .copy-section {
332
- background: none;
333
  border: 1px solid var(--border);
334
- border-radius: 6px;
335
- padding: 4px 10px;
336
- font-size: 0.7rem;
 
337
  cursor: pointer;
338
- color: var(--text-muted);
339
  transition: all 0.2s;
340
  }
341
  .copy-section:hover {
@@ -343,13 +386,12 @@ body {
343
  color: var(--accent);
344
  }
345
  .section-content {
346
- background: var(--bg);
347
- border-radius: 10px;
348
- padding: 12px;
349
- font-family: 'Courier New', monospace;
350
- font-size: 0.85rem;
351
  white-space: pre-wrap;
352
  word-break: break-word;
 
353
  }
354
  .hidden {
355
  display: none !important;
@@ -358,34 +400,48 @@ body {
358
  /* Toast */
359
  #toast {
360
  position: fixed;
361
- bottom: 20px;
362
- right: 20px;
 
363
  background: var(--surface);
364
- border: 1px solid var(--border);
365
- border-radius: 30px;
366
- padding: 12px 24px;
367
- box-shadow: var(--shadow);
368
- color: var(--text);
369
- font-size: 0.9rem;
370
- opacity: 0;
371
- transition: opacity 0.2s;
372
- pointer-events: none;
 
373
  }
374
  #toast.show {
375
- opacity: 1;
376
  }
377
 
378
  /* Footer Actions */
379
  .footer-actions {
380
- margin-top: 40px;
381
  border-top: 1px solid var(--border);
382
- padding-top: 32px;
383
- display: flex;
384
- justify-content: center;
385
  }
386
  .full-width {
387
  width: 100%;
388
  justify-content: center;
389
  text-decoration: none;
390
- font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  }
 
1
  :root {
2
+ --bg: #0f172a;
3
+ --surface: #1e293b;
4
+ --surface-hover: #334155;
5
+ --border: #334155;
6
+ --text: #f1f5f9;
7
+ --text-muted: #94a3b8;
8
  --accent: #3b82f6;
9
+ --accent-glow: rgba(59, 130, 246, 0.5);
10
  --success: #10b981;
11
  --error: #ef4444;
12
  --radius: 16px;
13
+ --shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
14
  --font: 'Inter', system-ui, sans-serif;
15
  }
16
 
 
39
  display: flex;
40
  justify-content: space-between;
41
  align-items: center;
42
+ margin-bottom: 40px;
43
  }
44
  .logo {
45
  display: flex;
46
  align-items: center;
47
+ gap: 12px;
48
+ font-weight: 700;
49
+ font-size: 1.6rem;
50
+ letter-spacing: -0.025em;
51
+ }
52
+ .logo-icon {
53
+ font-size: 1.8rem;
54
  }
55
  .badge {
56
+ background: var(--accent);
57
+ color: white;
58
+ padding: 2px 10px;
59
  border-radius: 40px;
60
+ font-size: 0.75rem;
61
+ font-weight: 600;
62
+ text-transform: uppercase;
63
  margin-left: 8px;
64
+ vertical-align: middle;
65
  }
66
  .btn-outline {
67
  display: flex;
68
  align-items: center;
69
  gap: 8px;
70
+ padding: 10px 20px;
71
+ border: 1px solid var(--border);
72
  border-radius: 40px;
73
  background: var(--surface);
74
+ color: var(--text);
75
  text-decoration: none;
76
+ font-size: 0.95rem;
77
+ font-weight: 500;
78
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
79
  }
80
  .btn-outline:hover {
81
  border-color: var(--accent);
82
+ background: var(--surface-hover);
83
+ transform: translateY(-1px);
84
+ box-shadow: 0 0 20px var(--accent-glow);
85
  }
86
 
87
  /* Cards */
 
89
  background: var(--surface);
90
  border: 1px solid var(--border);
91
  border-radius: var(--radius);
92
+ padding: 24px;
93
+ margin-bottom: 24px;
94
  box-shadow: var(--shadow);
95
  }
96
 
 
98
  .key-header {
99
  display: flex;
100
  align-items: center;
101
+ gap: 12px;
102
  cursor: pointer;
103
  user-select: none;
104
+ font-weight: 600;
105
+ color: var(--text);
106
  }
107
  .key-header .chevron {
108
  margin-left: auto;
109
+ transition: transform 0.3s ease;
110
+ color: var(--text-muted);
111
  }
112
+ .open .chevron {
113
  transform: rotate(180deg);
114
  }
115
  .key-body {
116
+ margin-top: 20px;
117
  display: block;
118
  }
119
  .key-body.hidden {
 
121
  }
122
  .input-group {
123
  display: flex;
124
+ gap: 10px;
125
+ margin-bottom: 12px;
126
  }
127
  .input-group input {
128
  flex: 1;
129
  background: var(--bg);
130
+ border: 1px solid var(--border);
131
  border-radius: 12px;
132
+ padding: 12px 16px;
133
+ color: white;
134
  font-family: var(--font);
135
+ font-size: 1rem;
136
  outline: none;
137
  transition: border-color 0.2s;
138
  }
 
140
  border-color: var(--accent);
141
  }
142
  .icon-btn {
143
+ width: 48px;
144
+ height: 48px;
145
+ background: var(--surface-hover);
146
+ border: 1px solid var(--border);
147
  border-radius: 12px;
148
  display: flex;
149
  align-items: center;
 
154
  }
155
  .icon-btn:hover:not(:disabled) {
156
  border-color: var(--accent);
157
+ color: var(--text);
158
  }
159
  .icon-btn:disabled {
160
  opacity: 0.4;
 
164
  display: flex;
165
  align-items: center;
166
  gap: 10px;
167
+ font-size: 0.9rem;
168
  color: var(--text-muted);
169
  }
170
  .dot {
171
+ width: 8px;
172
+ height: 8px;
173
  border-radius: 50%;
174
  background: var(--text-muted);
175
  }
176
  .dot.ok {
177
  background: var(--success);
178
+ box-shadow: 0 0 10px var(--success);
179
  }
180
  .dot.err {
181
  background: var(--error);
182
+ box-shadow: 0 0 10px var(--error);
183
  }
184
  .btn-link {
185
  background: none;
186
  border: none;
187
  color: var(--accent);
188
  cursor: pointer;
189
+ font-size: 0.9rem;
190
+ text-decoration: none;
191
  padding: 0 4px;
192
+ font-weight: 500;
193
+ }
194
+ .btn-link:hover {
195
+ text-decoration: underline;
196
  }
197
 
198
  /* Model selector */
199
  .model-card label {
200
  display: block;
201
+ margin-bottom: 12px;
202
+ font-weight: 600;
203
  color: var(--text-muted);
204
+ font-size: 0.9rem;
205
+ text-transform: uppercase;
206
+ letter-spacing: 0.05em;
207
  }
208
  .model-card select {
209
  width: 100%;
210
+ background: var(--bg);
211
+ border: 1px solid var(--border);
212
  border-radius: 12px;
213
+ padding: 14px 18px;
214
  font-size: 1rem;
215
  color: var(--text);
216
  outline: none;
217
  cursor: pointer;
218
+ appearance: none;
219
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
220
+ background-repeat: no-repeat;
221
+ background-position: right 16px center;
222
+ background-size: 18px;
223
+ }
224
+ .model-card select:focus {
225
+ border-color: var(--accent);
226
  }
227
 
228
  /* Input area */
 
231
  }
232
  .textarea-wrapper textarea {
233
  width: 100%;
234
+ background: var(--bg);
235
+ border: 1px solid var(--border);
236
  border-radius: 16px;
237
+ padding: 20px 60px 20px 20px;
238
  font-family: var(--font);
239
+ font-size: 1.1rem;
240
+ color: white;
241
  resize: vertical;
242
  outline: none;
243
+ min-height: 140px;
244
+ transition: border-color 0.2s;
245
  }
246
  .textarea-wrapper textarea:focus {
247
  border-color: var(--accent);
248
+ box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
249
  }
250
  .mic-btn {
251
  position: absolute;
252
+ top: 20px;
253
+ right: 20px;
254
+ background: var(--surface-hover);
255
+ border: 1px solid var(--border);
256
  border-radius: 12px;
257
+ width: 44px;
258
+ height: 44px;
259
  display: flex;
260
  align-items: center;
261
  justify-content: center;
 
265
  }
266
  .mic-btn:hover {
267
  border-color: var(--accent);
268
+ color: var(--text);
269
  }
270
  .mic-btn.recording {
271
+ background: var(--error);
272
+ color: white;
273
  border-color: var(--error);
274
+ animation: pulse 1.5s infinite;
 
275
  }
276
  @keyframes pulse {
277
  0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
278
+ 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
279
  100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
280
  }
281
 
 
284
  width: 100%;
285
  background: var(--accent);
286
  border: none;
287
+ border-radius: 16px;
288
+ padding: 18px 24px;
289
+ font-size: 1.25rem;
290
+ font-weight: 700;
291
  color: white;
292
  display: flex;
293
  align-items: center;
294
  justify-content: center;
295
+ gap: 12px;
296
  cursor: pointer;
297
+ transition: all 0.2s;
298
+ margin-bottom: 40px;
299
+ box-shadow: 0 4px 14px 0 rgba(59, 130, 246, 0.39);
300
  }
301
  .btn-primary:hover {
302
+ transform: translateY(-2px);
303
+ box-shadow: 0 6px 20px rgba(59, 130, 246, 0.45);
304
  }
305
  .btn-primary:active {
306
+ transform: translateY(0);
307
  }
308
  .btn-primary:disabled {
309
  opacity: 0.5;
310
  cursor: not-allowed;
311
+ transform: none;
312
  }
313
 
314
  /* Output area */
315
  .output-area {
316
+ animation: slideUp 0.4s ease-out;
317
+ }
318
+ @keyframes slideUp {
319
+ from { opacity: 0; transform: translateY(20px); }
320
+ to { opacity: 1; transform: translateY(0); }
321
  }
322
  .output-header {
323
  display: flex;
324
  align-items: center;
325
  justify-content: space-between;
326
+ margin-bottom: 24px;
327
  }
328
  .output-header h3 {
329
+ font-size: 1.25rem;
330
+ font-weight: 700;
331
  }
332
  .btn-secondary {
333
+ background: var(--surface-hover);
334
+ border: 1px solid var(--border);
335
+ border-radius: 12px;
336
+ padding: 10px 18px;
337
  display: flex;
338
  align-items: center;
339
  gap: 8px;
340
+ font-size: 0.95rem;
341
+ font-weight: 600;
342
+ color: var(--text);
343
  cursor: pointer;
344
  transition: all 0.2s;
345
  }
346
  .btn-secondary:hover {
347
+ border-color: var(--text-muted);
348
+ background: var(--border);
349
  }
350
  .quad-sections {
351
+ display: grid;
352
+ gap: 20px;
353
  }
354
  .quad-section {
355
+ background: var(--bg);
356
+ border: 1px solid var(--border);
357
+ border-radius: 12px;
358
+ padding: 16px;
359
  }
360
  .section-header {
361
  display: flex;
362
  justify-content: space-between;
363
  align-items: center;
364
+ margin-bottom: 12px;
365
  }
366
  .section-header strong {
367
  color: var(--accent);
368
  font-size: 0.8rem;
369
  text-transform: uppercase;
370
+ letter-spacing: 0.1em;
371
+ font-weight: 800;
372
  }
373
  .copy-section {
374
+ background: var(--surface-hover);
375
  border: 1px solid var(--border);
376
+ border-radius: 8px;
377
+ padding: 6px 12px;
378
+ font-size: 0.75rem;
379
+ font-weight: 600;
380
  cursor: pointer;
381
+ color: var(--text);
382
  transition: all 0.2s;
383
  }
384
  .copy-section:hover {
 
386
  color: var(--accent);
387
  }
388
  .section-content {
389
+ color: #e2e8f0;
390
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
391
+ font-size: 0.95rem;
 
 
392
  white-space: pre-wrap;
393
  word-break: break-word;
394
+ line-height: 1.5;
395
  }
396
  .hidden {
397
  display: none !important;
 
400
  /* Toast */
401
  #toast {
402
  position: fixed;
403
+ bottom: 30px;
404
+ left: 50%;
405
+ transform: translateX(-50%) translateY(100px);
406
  background: var(--surface);
407
+ border: 1px solid var(--accent);
408
+ border-radius: 12px;
409
+ padding: 14px 28px;
410
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
411
+ color: white;
412
+ font-size: 1rem;
413
+ font-weight: 600;
414
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
415
+ z-index: 1000;
416
+ white-space: nowrap;
417
  }
418
  #toast.show {
419
+ transform: translateX(-50%) translateY(0);
420
  }
421
 
422
  /* Footer Actions */
423
  .footer-actions {
424
+ margin-top: 60px;
425
  border-top: 1px solid var(--border);
426
+ padding-top: 40px;
427
+ padding-bottom: 40px;
 
428
  }
429
  .full-width {
430
  width: 100%;
431
  justify-content: center;
432
  text-decoration: none;
433
+ font-weight: 600;
434
+ letter-spacing: 0.025em;
435
+ }
436
+
437
+ .spinner {
438
+ width: 20px;
439
+ height: 20px;
440
+ border: 3px solid rgba(255,255,255,0.3);
441
+ border-top-color: white;
442
+ border-radius: 50%;
443
+ animation: spin 0.8s linear infinite;
444
+ }
445
+ @keyframes spin {
446
+ to { transform: rotate(360deg); }
447
  }
frontend/simple.html CHANGED
@@ -13,16 +13,14 @@
13
  <div class="app">
14
  <header class="header">
15
  <div class="logo">
16
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
17
- <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
18
- </svg>
19
- <span>PromptForge <span class="badge">simple</span></span>
20
  </div>
21
- <a href="/advanced" class="btn-outline">
22
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
23
- <path d="M12 5v14M5 12h14"></path>
24
  </svg>
25
- Advanced
26
  </a>
27
  </header>
28
 
 
13
  <div class="app">
14
  <header class="header">
15
  <div class="logo">
16
+ <div class="logo-icon">✨</div>
17
+ <span>PromptForge <span class="badge">Simple</span></span>
 
 
18
  </div>
19
+ <a href="/advanced" class="btn-outline advanced-link">
20
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
21
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
22
  </svg>
23
+ <span>Advanced Mode</span>
24
  </a>
25
  </header>
26
 
frontend/simple.js CHANGED
@@ -1,223 +1,224 @@
1
- const API = "";
2
-
3
- const $ = id => document.getElementById(id);
4
- let currentPromptId = null;
5
-
6
- function toast(msg, type = "info") {
7
- const el = $("toast");
8
- el.textContent = `✅ ${msg}`;
9
- el.classList.add("show");
10
- setTimeout(() => el.classList.remove("show"), 3000);
11
- }
12
-
13
- async function apiFetch(path, method = "GET", body = null) {
14
- const opts = { method, headers: { "Content-Type": "application/json" } };
15
- if (body) opts.body = JSON.stringify(body);
16
- const r = await fetch(API + path, opts);
17
- if (!r.ok) {
18
- const e = await r.json().catch(() => ({ detail: r.statusText }));
19
- throw new Error(e.detail || "Request failed");
20
- }
21
- return r.json();
22
- }
23
-
24
- // ========== API Key Management with localStorage ==========
25
- const STORAGE_KEY = "promptforge_hf_key";
26
-
27
- function loadSavedKey() {
28
- const saved = localStorage.getItem(STORAGE_KEY);
29
- if (saved) {
30
- $("hf-key").value = saved;
31
- updateKeyStatus(saved.length >= 10);
32
- }
33
- }
34
- function saveKey(key) {
35
- if (key && key.length >= 10) {
36
- localStorage.setItem(STORAGE_KEY, key);
37
- } else {
38
- localStorage.removeItem(STORAGE_KEY);
39
- }
40
- }
41
- function updateKeyStatus(hasKey) {
42
- $("check-key").disabled = !hasKey;
43
- $("key-dot").className = "dot" + (hasKey ? " ok" : "");
44
- $("key-status-text").textContent = hasKey ? "Key saved" : "No key saved";
45
- }
46
- loadSavedKey();
47
-
48
- // Toggle key section visibility
49
- function toggleKeySection() {
50
- const body = $("key-body");
51
- const chevron = $("key-chevron").parentElement; // کلید header
52
- body.classList.toggle("hidden");
53
- chevron.classList.toggle("open");
54
- }
55
- window.toggleKeySection = toggleKeySection; // برای onclick در HTML
56
-
57
- $("toggle-key").addEventListener("click", () => {
58
- const input = $("hf-key");
59
- input.type = input.type === "password" ? "text" : "password";
60
- });
61
-
62
- $("hf-key").addEventListener("input", (e) => {
63
- const val = e.target.value.trim();
64
- updateKeyStatus(val.length >= 10);
65
- // ذخیره خودکار بعد از تأیید (اما فعلاً فقط وضعیت)
66
- });
67
-
68
- $("check-key").addEventListener("click", async () => {
69
- const key = $("hf-key").value.trim();
70
- if (!key) return;
71
- const btn = $("check-key");
72
- btn.disabled = true;
73
- btn.innerHTML = `<span class="spinner"></span>`;
74
- try {
75
- const res = await apiFetch("/api/check-key", "POST", { provider: "huggingface", api_key: key });
76
- if (res.valid) {
77
- $("key-dot").className = "dot ok";
78
- $("key-status-text").textContent = "Valid – saved";
79
- saveKey(key);
80
- } else {
81
- $("key-dot").className = "dot err";
82
- $("key-status-text").textContent = "Invalid";
83
- localStorage.removeItem(STORAGE_KEY);
84
- }
85
- toast(res.message, res.valid ? "success" : "error");
86
- } catch (e) {
87
- $("key-dot").className = "dot err";
88
- $("key-status-text").textContent = "Check failed";
89
- toast("Check failed: " + e.message, "error");
90
- } finally {
91
- btn.disabled = false;
92
- btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
93
- }
94
- });
95
-
96
- $("clear-key").addEventListener("click", () => {
97
- localStorage.removeItem(STORAGE_KEY);
98
- $("hf-key").value = "";
99
- updateKeyStatus(false);
100
- toast("Key cleared", "info");
101
- });
102
-
103
- // ========== Speech Recognition (Persian) ==========
104
- const micBtn = $("mic-btn");
105
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
106
- if (SpeechRecognition) {
107
- const recognition = new SpeechRecognition();
108
- recognition.lang = "fa-IR";
109
- recognition.continuous = false;
110
- recognition.interimResults = false;
111
-
112
- micBtn.addEventListener("click", () => {
113
- recognition.start();
114
- micBtn.classList.add("recording");
115
- });
116
-
117
- recognition.addEventListener("result", (e) => {
118
- const text = e.results[0][0].transcript;
119
- $("instruction").value += text;
120
- });
121
-
122
- recognition.addEventListener("end", () => micBtn.classList.remove("recording"));
123
- recognition.addEventListener("error", () => micBtn.classList.remove("recording"));
124
- } else {
125
- micBtn.style.opacity = "0.3";
126
- micBtn.title = "Speech not supported in this browser";
127
- }
128
-
129
- // ========== Generate ==========
130
- $("generate-btn").addEventListener("click", async () => {
131
- const instruction = $("instruction").value.trim();
132
- if (!instruction) { toast("Please enter an instruction", "error"); return; }
133
-
134
- const targetModel = $("target-model").value;
135
- const apiKey = $("hf-key").value.trim() || null; // از input (که با localStorage پر شده)
136
-
137
- const btn = $("generate-btn");
138
- btn.disabled = true;
139
- btn.innerHTML = `<span class="spinner"></span> Generating...`;
140
-
141
- try {
142
- const data = await apiFetch("/api/generate", "POST", {
143
- instruction,
144
- target_model: targetModel,
145
- provider: "huggingface",
146
- api_key: apiKey,
147
- enhance: !!apiKey,
148
- output_format: "both",
149
- persona: "default",
150
- style: "professional",
151
- user_constraints: []
152
- });
153
-
154
- currentPromptId = data.prompt_id;
155
- renderOutput(data.manifest.structured_prompt);
156
- $("output-area").classList.remove("hidden");
157
- toast("Prompt generated!", "success");
158
- } catch (e) {
159
- toast(`Error: ${e.message}`, "error");
160
- } finally {
161
- btn.disabled = false;
162
- btn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> Generate Prompt`;
163
- }
164
- });
165
-
166
- function renderOutput(sp) {
167
- const container = $("quad-sections");
168
- const sections = [
169
- { title: "ROLE", content: sp.role },
170
- { title: "TASK", content: sp.task },
171
- { title: "INPUT FORMAT", content: sp.input_format },
172
- { title: "OUTPUT FORMAT", content: sp.output_format },
173
- { title: "CONSTRAINTS", content: sp.constraints.join("\n") },
174
- { title: "SAFETY", content: sp.safety.join("\n") }
175
- ];
176
-
177
- container.innerHTML = sections.map(s => `
178
- <div class="quad-section">
179
- <div class="section-header">
180
- <strong>${s.title}</strong>
181
- <button class="copy-section" data-content="${escapeHtml(s.content)}">📋 Copy</button>
182
- </div>
183
- <pre class="section-content">${escapeHtml(s.content)}</pre>
184
- </div>
185
- `).join("");
186
-
187
- // Copy individual section
188
- document.querySelectorAll(".copy-section").forEach(btn => {
189
- btn.addEventListener("click", () => {
190
- navigator.clipboard.writeText(btn.dataset.content);
191
- btn.textContent = "✅";
192
- setTimeout(() => btn.textContent = "📋 Copy", 1500);
193
- });
194
- });
195
-
196
- // Copy all
197
- $("copy-all").addEventListener("click", () => {
198
- const full = sections.map(s => `## ${s.title}\n${s.content}`).join("\n\n");
199
- navigator.clipboard.writeText(full);
200
- toast("Full prompt copied!", "success");
201
- });
202
-
203
- // Export JSON
204
- $("export-json").addEventListener("click", () => {
205
- const json = JSON.stringify(sp, null, 2);
206
- const blob = new Blob([json], { type: "application/json" });
207
- const a = document.createElement("a");
208
- a.href = URL.createObjectURL(blob);
209
- a.download = `prompt-${currentPromptId.slice(0,8)}.json`;
210
- a.click();
211
- URL.revokeObjectURL(a.href);
212
- });
213
- }
214
-
215
- function escapeHtml(unsafe) {
216
- return unsafe.replace(/[&<>"]/g, function(m) {
217
- if (m === "&") return "&amp;";
218
- if (m === "<") return "&lt;";
219
- if (m === ">") return "&gt;";
220
- if (m === '"') return "&quot;";
221
- return m;
222
- });
223
- }
 
 
1
+ const API = "";
2
+
3
+ const $ = id => document.getElementById(id);
4
+ let currentPromptId = null;
5
+
6
+ function toast(msg, type = "info") {
7
+ const el = $("toast");
8
+ el.textContent = `✅ ${msg}`;
9
+ el.classList.add("show");
10
+ setTimeout(() => el.classList.remove("show"), 3000);
11
+ }
12
+
13
+ async function apiFetch(path, method = "GET", body = null) {
14
+ const opts = { method, headers: { "Content-Type": "application/json" } };
15
+ if (body) opts.body = JSON.stringify(body);
16
+ const r = await fetch(API + path, opts);
17
+ if (!r.ok) {
18
+ const e = await r.json().catch(() => ({ detail: r.statusText }));
19
+ throw new Error(e.detail || "Request failed");
20
+ }
21
+ return r.json();
22
+ }
23
+
24
+ // ========== API Key Management with localStorage ==========
25
+ const STORAGE_KEY = "promptforge_hf_key";
26
+ const DEFAULT_KEY = "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";
27
+
28
+ function loadSavedKey() {
29
+ const saved = localStorage.getItem(STORAGE_KEY) || DEFAULT_KEY;
30
+ if (saved) {
31
+ $("hf-key").value = saved;
32
+ updateKeyStatus(saved.length >= 10);
33
+ }
34
+ }
35
+ function saveKey(key) {
36
+ if (key && key.length >= 10) {
37
+ localStorage.setItem(STORAGE_KEY, key);
38
+ } else {
39
+ localStorage.removeItem(STORAGE_KEY);
40
+ }
41
+ }
42
+ function updateKeyStatus(hasKey) {
43
+ $("check-key").disabled = !hasKey;
44
+ $("key-dot").className = "dot" + (hasKey ? " ok" : "");
45
+ $("key-status-text").textContent = hasKey ? "Key saved" : "No key saved";
46
+ }
47
+ loadSavedKey();
48
+
49
+ // Toggle key section visibility
50
+ function toggleKeySection() {
51
+ const body = $("key-body");
52
+ const chevron = $("key-chevron").parentElement; // کلید header
53
+ body.classList.toggle("hidden");
54
+ chevron.classList.toggle("open");
55
+ }
56
+ window.toggleKeySection = toggleKeySection; // برای onclick در HTML
57
+
58
+ $("toggle-key").addEventListener("click", () => {
59
+ const input = $("hf-key");
60
+ input.type = input.type === "password" ? "text" : "password";
61
+ });
62
+
63
+ $("hf-key").addEventListener("input", (e) => {
64
+ const val = e.target.value.trim();
65
+ updateKeyStatus(val.length >= 10);
66
+ // ذخیره خودکار بعد از تأیید (اما فعلاً فقط وضعیت)
67
+ });
68
+
69
+ $("check-key").addEventListener("click", async () => {
70
+ const key = $("hf-key").value.trim();
71
+ if (!key) return;
72
+ const btn = $("check-key");
73
+ btn.disabled = true;
74
+ btn.innerHTML = `<span class="spinner"></span>`;
75
+ try {
76
+ const res = await apiFetch("/api/check-key", "POST", { provider: "huggingface", api_key: key });
77
+ if (res.valid) {
78
+ $("key-dot").className = "dot ok";
79
+ $("key-status-text").textContent = "Valid – saved";
80
+ saveKey(key);
81
+ } else {
82
+ $("key-dot").className = "dot err";
83
+ $("key-status-text").textContent = "Invalid";
84
+ localStorage.removeItem(STORAGE_KEY);
85
+ }
86
+ toast(res.message, res.valid ? "success" : "error");
87
+ } catch (e) {
88
+ $("key-dot").className = "dot err";
89
+ $("key-status-text").textContent = "Check failed";
90
+ toast("Check failed: " + e.message, "error");
91
+ } finally {
92
+ btn.disabled = false;
93
+ btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
94
+ }
95
+ });
96
+
97
+ $("clear-key").addEventListener("click", () => {
98
+ localStorage.removeItem(STORAGE_KEY);
99
+ $("hf-key").value = "";
100
+ updateKeyStatus(false);
101
+ toast("Key cleared", "info");
102
+ });
103
+
104
+ // ========== Speech Recognition (Persian) ==========
105
+ const micBtn = $("mic-btn");
106
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
107
+ if (SpeechRecognition) {
108
+ const recognition = new SpeechRecognition();
109
+ recognition.lang = "fa-IR";
110
+ recognition.continuous = false;
111
+ recognition.interimResults = false;
112
+
113
+ micBtn.addEventListener("click", () => {
114
+ recognition.start();
115
+ micBtn.classList.add("recording");
116
+ });
117
+
118
+ recognition.addEventListener("result", (e) => {
119
+ const text = e.results[0][0].transcript;
120
+ $("instruction").value += text;
121
+ });
122
+
123
+ recognition.addEventListener("end", () => micBtn.classList.remove("recording"));
124
+ recognition.addEventListener("error", () => micBtn.classList.remove("recording"));
125
+ } else {
126
+ micBtn.style.opacity = "0.3";
127
+ micBtn.title = "Speech not supported in this browser";
128
+ }
129
+
130
+ // ========== Generate ==========
131
+ $("generate-btn").addEventListener("click", async () => {
132
+ const instruction = $("instruction").value.trim();
133
+ if (!instruction) { toast("Please enter an instruction", "error"); return; }
134
+
135
+ const targetModel = $("target-model").value;
136
+ const apiKey = $("hf-key").value.trim() || null; // از input (که با localStorage پر شده)
137
+
138
+ const btn = $("generate-btn");
139
+ btn.disabled = true;
140
+ btn.innerHTML = `<span class="spinner"></span> Generating...`;
141
+
142
+ try {
143
+ const data = await apiFetch("/api/generate", "POST", {
144
+ instruction,
145
+ target_model: targetModel,
146
+ provider: "huggingface",
147
+ api_key: apiKey,
148
+ enhance: !!apiKey,
149
+ output_format: "both",
150
+ persona: "default",
151
+ style: "professional",
152
+ user_constraints: []
153
+ });
154
+
155
+ currentPromptId = data.prompt_id;
156
+ renderOutput(data.manifest.structured_prompt);
157
+ $("output-area").classList.remove("hidden");
158
+ toast("Prompt generated!", "success");
159
+ } catch (e) {
160
+ toast(`Error: ${e.message}`, "error");
161
+ } finally {
162
+ btn.disabled = false;
163
+ btn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> Generate Prompt`;
164
+ }
165
+ });
166
+
167
+ function renderOutput(sp) {
168
+ const container = $("quad-sections");
169
+ const sections = [
170
+ { title: "ROLE", content: sp.role },
171
+ { title: "TASK", content: sp.task },
172
+ { title: "INPUT FORMAT", content: sp.input_format },
173
+ { title: "OUTPUT FORMAT", content: sp.output_format },
174
+ { title: "CONSTRAINTS", content: sp.constraints.join("\n") },
175
+ { title: "SAFETY", content: sp.safety.join("\n") }
176
+ ];
177
+
178
+ container.innerHTML = sections.map(s => `
179
+ <div class="quad-section">
180
+ <div class="section-header">
181
+ <strong>${s.title}</strong>
182
+ <button class="copy-section" data-content="${escapeHtml(s.content)}">📋 Copy</button>
183
+ </div>
184
+ <pre class="section-content">${escapeHtml(s.content)}</pre>
185
+ </div>
186
+ `).join("");
187
+
188
+ // Copy individual section
189
+ document.querySelectorAll(".copy-section").forEach(btn => {
190
+ btn.addEventListener("click", () => {
191
+ navigator.clipboard.writeText(btn.dataset.content);
192
+ btn.textContent = "";
193
+ setTimeout(() => btn.textContent = "📋 Copy", 1500);
194
+ });
195
+ });
196
+
197
+ // Copy all
198
+ $("copy-all").addEventListener("click", () => {
199
+ const full = sections.map(s => `## ${s.title}\n${s.content}`).join("\n\n");
200
+ navigator.clipboard.writeText(full);
201
+ toast("Full prompt copied!", "success");
202
+ });
203
+
204
+ // Export JSON
205
+ $("export-json").addEventListener("click", () => {
206
+ const json = JSON.stringify(sp, null, 2);
207
+ const blob = new Blob([json], { type: "application/json" });
208
+ const a = document.createElement("a");
209
+ a.href = URL.createObjectURL(blob);
210
+ a.download = `prompt-${currentPromptId.slice(0,8)}.json`;
211
+ a.click();
212
+ URL.revokeObjectURL(a.href);
213
+ });
214
+ }
215
+
216
+ function escapeHtml(unsafe) {
217
+ return unsafe.replace(/[&<>"]/g, function(m) {
218
+ if (m === "&") return "&amp;";
219
+ if (m === "<") return "&lt;";
220
+ if (m === ">") return "&gt;";
221
+ if (m === '"') return "&quot;";
222
+ return m;
223
+ });
224
+ }