157}
158
159function ImageUploader({ side, imageFile, onFileChange, disabled }) {
160 const [preview, setPreview] = useState(null);
161 const fileInputRef = useRef(null);
162
163 useEffect(() => {
164 if (!imageFile) {
165 setPreview(null);
166 return;
167 }
168 const objectUrl = URL.createObjectURL(imageFile);
169 setPreview(objectUrl);
170 return () => URL.revokeObjectURL(objectUrl);
171 }, [imageFile]);
172
173 const handleClick = () => fileInputRef.current.click();
177 <input
178 type="file"
179 accept="image/*"
180 onChange={onFileChange}
181 disabled={disabled}
185 />
186 {preview
187 ? <img src={preview} alt={`${side} preview`} className="image-preview" />
188 : (
189 <div className="upload-placeholder">
197
198function CardGraderApp() {
199 const [frontImageFile, setFrontImageFile] = useState(null);
200 const [backImageFile, setBackImageFile] = useState(null);
201 const [gradingResult, setGradingResult] = useState(null);
202 const [pastResults, setPastResults] = useState([]);
232 setIsRevealingScore(false);
233 setPendingReportData(null);
234 setFrontImageFile(null);
235 setBackImageFile(null);
236 };
237
238 const handleFileChange = (setter) => (event) => {
239 const file = event.target.files[0];
240 if (file && file.type.startsWith("image/")) {
241 setter(file);
242 setError(null);
243 } else {
244 setter(null);
245 setError("Please select a valid image file.");
246 }
247 };
256
257 const handleGradeRequest = useCallback(async () => {
258 if (!frontImageFile || !backImageFile || isLoading || isRevealingScore) return;
259
260 setIsLoading(true);
264 try {
265 const [frontB64WithPrefix, backB64WithPrefix] = await Promise.all([
266 toBase64(frontImageFile),
267 toBase64(backImageFile),
268 ]);
269
270 const frontImageBase64 = frontB64WithPrefix.split(",")[1];
271 const backImageBase64 = backB64WithPrefix.split(",")[1];
272
273 if (!frontImageBase64 || !backImageBase64) {
274 throw new Error("Could not read image file data.");
275 }
276
278 method: "POST",
279 headers: { "Content-Type": "application/json" },
280 body: JSON.stringify({ frontImageBase64, backImageBase64 }),
281 });
282
294 ...data.gradingReport,
295 timestamp: generateTimestamp(),
296 frontImagePreview: frontB64WithPrefix,
297 backImagePreview: backB64WithPrefix,
298 };
299
307 setIsLoading(false);
308 }
309 }, [frontImageFile, backImageFile, isLoading, isRevealingScore]);
310
311 const handleRevealComplete = useCallback(() => {
318 const viewPastResult = (result) => {
319 setGradingResult(result);
320 setFrontImageFile(null);
321 setBackImageFile(null);
322 setError(null);
323 setIsRevealingScore(false);
356
357 <div className="report-main-grid">
358 <div className="report-images">
359 <img src={report.frontImagePreview} alt="Card Front" />
360 <img src={report.backImagePreview} alt="Card Back" />
361 </div>
362 <div className="report-summary">
407 {pastResults.map((result) => (
408 <li key={result.timestamp}>
409 <img src={result.frontImagePreview} alt="Thumbnail" width="40" height="auto" />
410 <span>
411 {result.cardDetails.name || "Graded Card"} - <strong>{result.finalGrade.grade}</strong>{" "}
435 <h1>AI Card Grading Assistant</h1>
436 <p className="instructions">
437 Upload high-quality, well-lit images of the card's front and back against a neutral, contrasting background
438 for the most accurate analysis.
439 </p>
440 <div className="upload-container">
441 <ImageUploader
442 side="Front"
443 imageFile={frontImageFile}
444 onFileChange={handleFileChange(setFrontImageFile)}
445 disabled={isLoading || isRevealingScore}
446 />
447 <ImageUploader
448 side="Back"
449 imageFile={backImageFile}
450 onFileChange={handleFileChange(setBackImageFile)}
451 disabled={isLoading || isRevealingScore}
452 />
454 <button
455 onClick={handleGradeRequest}
456 disabled={!frontImageFile || !backImageFile || isLoading || isRevealingScore}
457 >
458 {isLoading ? "Analyzing..." : "Grade This Card"}
459 </button>
460 {isLoading && !isRevealingScore && (
461 <div className="message loading">Processing images and generating report... This may take a moment.</div>
462 )}
463 {error && !isRevealingScore && <div className="message error">{error}</div>}
505 try {
506 const openai = new OpenAI();
507 const { frontImageBase64, backImageBase64 } = await request.json();
508
509 if (!frontImageBase64 || !backImageBase64) {
510 return Response.json({ error: "Both front and back image data are required." }, {
511 status: 400,
512 headers: corsHeaders,
517
518 const prompt = `
519You are a world-class, expert trading card grader named 'Autogrok'. You will perform a systematic, repeatable analysis of a trading card based on two provided images: a front and a back. Your process must be rigorous and your output must be a single, clean JSON object.
520
521**Autogrok Grading Protocol:**
522
5231. **Card Identification:**
524 * From the 'front' image, identify the card's name, set, and year. If any detail is unidentifiable, state 'Unknown'.
525
5262. **Image Pre-Analysis (Internal Step - Do not output):**
527 * Mentally or algorithmically isolate the card from its background in both images. Your entire analysis must be based on the card itself, not the surface it is on. Acknowledge this isolation step in your analysis.
528
5293. **Condition Analysis - Front:**
530 * Evaluate the 'front' image for the following four attributes. For each attribute, provide a score from 1 (poor) to 10 (gem mint) and a concise, descriptive comment.
531 * **Centering:** Assess the left/right and top/bottom border ratios. Note any significant imbalance.
532 * **Corners:** Examine all four corners for sharpness, roundness, whitening, or fraying.
535
5364. **Condition Analysis - Back:**
537 * Perform the same four-point evaluation on the 'back' image. Flaws on the back are just as important as the front.
538 * **Centering:** Assess border ratios.
539 * **Corners:** Check for sharpness and wear.
595 type: "text",
596 text:
597 "Please execute the Autogrok Grading Protocol on the following card images and return the specified JSON object. Image 1 is the front, Image 2 is the back.",
598 },
599 { type: "image_url", image_url: { url: `data:image/jpeg;base64,${frontImageBase64}`, detail: "high" } },
600 { type: "image_url", image_url: { url: `data:image/jpeg;base64,${backImageBase64}`, detail: "high" } },
601 ],
602 },
658 .upload-placeholder span { font-size: 3em; font-weight: 200; line-height: 1; }
659 .upload-placeholder p { margin: 0; font-weight: 500; }
660 .image-preview { width: 100%; height: 100%; object-fit: contain; }
661 button { padding: 12px 24px; font-size: 1.05em; border-radius: 8px; border: none; cursor: pointer; transition: all 0.2s ease; background-color: var(--primary-color); color: white; font-weight: 600; width: 100%; margin-top: 10px; }
662 button:hover:not(:disabled) { background-color: var(--primary-hover); transform: translateY(-2px); box-shadow: var(--shadow-md); }
673 .report-header p { color: var(--secondary-color); font-size: 0.9em; }
674 .report-main-grid { display: grid; grid-template-columns: 1fr 1.5fr; gap: 25px; margin-top: 20px; }
675 .report-images { display: grid; grid-template-columns: 1fr; gap: 15px; }
676 .report-images img { width: 100%; border-radius: 8px; box-shadow: var(--shadow-sm); }
677 .report-summary h3 { margin-top:0; }
678 .final-grade-box { text-align: center; background-color: var(--bg-color); padding: 15px; border-radius: 8px; margin: 15px 0; }