Spaces:
Running
Running
Ryan Christian D. Deniega commited on
Commit ·
a98615e
1
Parent(s): f934e95
chore: add HF Spaces deployment config and fix frontend API URL
Browse files- Dockerfile +5 -5
- README.md +10 -0
- frontend/.env.production.example +5 -0
- frontend/src/api.js +5 -2
- frontend/src/pages/VerifyPage.jsx +134 -128
Dockerfile
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
# ── PhilVerify API — Cloud Run
|
| 2 |
# Build: docker build -t philverify-api .
|
| 3 |
-
# Run: docker run -p
|
| 4 |
|
| 5 |
FROM python:3.12-slim
|
| 6 |
|
|
@@ -56,12 +56,12 @@ print('All HuggingFace models cached.'); \
|
|
| 56 |
" || true
|
| 57 |
|
| 58 |
# ── Runtime ───────────────────────────────────────────────────────────────────
|
| 59 |
-
#
|
| 60 |
-
ENV PORT=
|
| 61 |
ENV APP_ENV=production
|
| 62 |
ENV DEBUG=false
|
| 63 |
|
| 64 |
-
EXPOSE
|
| 65 |
|
| 66 |
# Use exec form so signals (SIGTERM) reach uvicorn directly
|
| 67 |
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT} --workers 1 --timeout-keep-alive 75"]
|
|
|
|
| 1 |
+
# ── PhilVerify API — Dockerfile (Cloud Run + Hugging Face Spaces) ─────────────
|
| 2 |
# Build: docker build -t philverify-api .
|
| 3 |
+
# Run: docker run -p 7860:7860 --env-file .env philverify-api
|
| 4 |
|
| 5 |
FROM python:3.12-slim
|
| 6 |
|
|
|
|
| 56 |
" || true
|
| 57 |
|
| 58 |
# ── Runtime ───────────────────────────────────────────────────────────────────
|
| 59 |
+
# HF Spaces uses port 7860 by default. Cloud Run overrides PORT via env var.
|
| 60 |
+
ENV PORT=7860
|
| 61 |
ENV APP_ENV=production
|
| 62 |
ENV DEBUG=false
|
| 63 |
|
| 64 |
+
EXPOSE 7860
|
| 65 |
|
| 66 |
# Use exec form so signals (SIGTERM) reach uvicorn directly
|
| 67 |
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT} --workers 1 --timeout-keep-alive 75"]
|
README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<p align="center">
|
| 2 |
<img src="frontend/public/logo.svg" alt="PhilVerify Logo" width="150">
|
| 3 |
</p>
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: PhilVerify API
|
| 3 |
+
emoji: 🇵🇭
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
<p align="center">
|
| 12 |
<img src="frontend/public/logo.svg" alt="PhilVerify Logo" width="150">
|
| 13 |
</p>
|
frontend/.env.production.example
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy this to .env.production and fill in your actual HF Space URL
|
| 2 |
+
# HF Space URL format: https://<your-hf-username>-philverify-api.hf.space
|
| 3 |
+
# The /api suffix is required — it must match your FastAPI router prefix
|
| 4 |
+
|
| 5 |
+
VITE_API_BASE_URL=https://YOUR_HF_USERNAME-philverify-api.hf.space/api
|
frontend/src/api.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
| 1 |
-
/** PhilVerify API client
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
function _detailToString(detail, status) {
|
| 5 |
if (!detail) return `HTTP ${status}`
|
|
|
|
| 1 |
+
/** PhilVerify API client
|
| 2 |
+
* Dev: Vite proxies /api → http://localhost:8000
|
| 3 |
+
* Production: Set VITE_API_BASE_URL to your backend URL (e.g. HF Space)
|
| 4 |
+
*/
|
| 5 |
+
const BASE = (import.meta.env.VITE_API_BASE_URL ?? '').replace(/\/$/, '') || '/api'
|
| 6 |
|
| 7 |
function _detailToString(detail, status) {
|
| 8 |
if (!detail) return `HTTP ${status}`
|
frontend/src/pages/VerifyPage.jsx
CHANGED
|
@@ -237,6 +237,7 @@ export default function VerifyPage() {
|
|
| 237 |
const [tab, setTab] = useState(persisted?.tab ?? 'text')
|
| 238 |
const [input, setInput] = useState(persisted?.input ?? '')
|
| 239 |
const [file, setFile] = useState(null)
|
|
|
|
| 240 |
const [dragOver, setDragOver] = useState(false)
|
| 241 |
const [loading, setLoading] = useState(false)
|
| 242 |
const [result, setResult] = useState(persisted?.result ?? null)
|
|
@@ -257,6 +258,14 @@ export default function VerifyPage() {
|
|
| 257 |
}
|
| 258 |
}, [submittedInput])
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
/* Persist result + input to sessionStorage so state survives navigation/refresh */
|
| 261 |
useEffect(() => {
|
| 262 |
if (result) {
|
|
@@ -321,12 +330,13 @@ export default function VerifyPage() {
|
|
| 321 |
}
|
| 322 |
|
| 323 |
function handleTabChange(id) {
|
| 324 |
-
setTab(id); setInput(''); setFile(null); setResult(null); setError(null); setSubmittedInput(null); setUrlPreview(null)
|
| 325 |
sessionStorage.removeItem(STORAGE_KEY)
|
| 326 |
}
|
| 327 |
|
| 328 |
function handleVerifyAgain() {
|
| 329 |
setResult(null); setError(null); setExtractedTextOpen(false)
|
|
|
|
| 330 |
sessionStorage.removeItem(STORAGE_KEY)
|
| 331 |
// Smooth-scroll back to the input panel
|
| 332 |
requestAnimationFrame(() => {
|
|
@@ -482,11 +492,45 @@ export default function VerifyPage() {
|
|
| 482 |
name={tab === 'image' ? 'media-image' : 'media-video'}
|
| 483 |
accept={tab === 'image' ? 'image/*' : 'video/*,audio/*'}
|
| 484 |
onChange={e => setFile(e.target.files[0])} />
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
<p className="text-sm" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }}>
|
| 491 |
Drop or click to upload {tab === 'image' ? 'image' : 'video / audio'}
|
| 492 |
</p>
|
|
@@ -494,7 +538,7 @@ export default function VerifyPage() {
|
|
| 494 |
or press <kbd style={{ background: 'var(--bg-hover)', border: '1px solid var(--border)', borderRadius: 2, padding: '1px 5px', fontFamily: 'var(--font-mono)', fontSize: 10 }}>Ctrl+V</kbd> to paste from clipboard
|
| 495 |
</p>
|
| 496 |
</>
|
| 497 |
-
}
|
| 498 |
</div>
|
| 499 |
</div>
|
| 500 |
)}
|
|
@@ -525,13 +569,33 @@ export default function VerifyPage() {
|
|
| 525 |
<div className="card p-4 fade-up" style={{ borderLeft: '3px solid var(--accent-red)' }}>
|
| 526 |
<p className="text-xs font-semibold uppercase tracking-widest mb-2"
|
| 527 |
style={{ fontFamily: 'var(--font-display)', color: 'var(--text-muted)', letterSpacing: '0.15em' }}>
|
| 528 |
-
|
|
|
|
|
|
|
|
|
|
| 529 |
</p>
|
| 530 |
{submittedInput.type === 'url' && (
|
| 531 |
<div className="space-y-2">
|
| 532 |
-
{
|
| 533 |
-
|
| 534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
: (
|
| 536 |
<a href={submittedInput.text} target="_blank" rel="noreferrer"
|
| 537 |
className="flex items-center gap-2 text-sm"
|
|
@@ -560,45 +624,67 @@ export default function VerifyPage() {
|
|
| 560 |
</p>
|
| 561 |
)}
|
| 562 |
{submittedInput.type === 'image' && (
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
<
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
<
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
<
|
| 585 |
-
|
| 586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
)}
|
| 588 |
{submittedInput.type === 'video' && (
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
)}
|
| 603 |
</div>
|
| 604 |
)}
|
|
@@ -726,86 +812,6 @@ export default function VerifyPage() {
|
|
| 726 |
</p>
|
| 727 |
</div>
|
| 728 |
|
| 729 |
-
{/* Row 2½: Extracted Text (collapsible) */}
|
| 730 |
-
{result.extracted_text && (
|
| 731 |
-
<div className="card fade-up-3" style={{ overflow: 'hidden' }}>
|
| 732 |
-
<button
|
| 733 |
-
onClick={() => setExtractedTextOpen(o => !o)}
|
| 734 |
-
className="w-full flex items-center justify-between px-5 py-3 transition-colors"
|
| 735 |
-
style={{
|
| 736 |
-
background: 'none',
|
| 737 |
-
border: 'none',
|
| 738 |
-
cursor: 'pointer',
|
| 739 |
-
borderBottom: extractedTextOpen ? '1px solid var(--border)' : 'none',
|
| 740 |
-
}}
|
| 741 |
-
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
| 742 |
-
onMouseLeave={e => e.currentTarget.style.background = 'none'}
|
| 743 |
-
aria-expanded={extractedTextOpen}
|
| 744 |
-
aria-controls="extracted-text-panel"
|
| 745 |
-
>
|
| 746 |
-
<div className="flex items-center gap-2">
|
| 747 |
-
<span className="text-xs font-semibold uppercase tracking-widest"
|
| 748 |
-
style={{ fontFamily: 'var(--font-display)', color: 'var(--text-muted)', letterSpacing: '0.15em' }}>
|
| 749 |
-
{result.input_type === 'image' ? 'OCR Extracted Text'
|
| 750 |
-
: result.input_type === 'video' ? 'Transcribed Text'
|
| 751 |
-
: result.input_type === 'url' ? 'Scraped Text'
|
| 752 |
-
: 'Analyzed Text'}
|
| 753 |
-
</span>
|
| 754 |
-
<span className="text-xs tabular"
|
| 755 |
-
style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)', fontSize: 10 }}>
|
| 756 |
-
{result.extracted_text.length} chars
|
| 757 |
-
</span>
|
| 758 |
-
</div>
|
| 759 |
-
<ChevronRight size={13}
|
| 760 |
-
style={{
|
| 761 |
-
color: 'var(--text-muted)',
|
| 762 |
-
transform: extractedTextOpen ? 'rotate(90deg)' : 'rotate(0deg)',
|
| 763 |
-
transition: 'transform 200ms ease',
|
| 764 |
-
flexShrink: 0,
|
| 765 |
-
}}
|
| 766 |
-
aria-hidden="true"
|
| 767 |
-
/>
|
| 768 |
-
</button>
|
| 769 |
-
{extractedTextOpen && (
|
| 770 |
-
<div id="extracted-text-panel" className="px-5 py-4">
|
| 771 |
-
{result.input_type === 'url' && (
|
| 772 |
-
<p className="text-xs mb-3" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)', lineHeight: 1.5 }}>
|
| 773 |
-
This is the text our scraper extracted from the URL. If it looks wrong or incomplete, the page may have been partially blocked.
|
| 774 |
-
</p>
|
| 775 |
-
)}
|
| 776 |
-
{result.input_type === 'image' && (
|
| 777 |
-
<p className="text-xs mb-3" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)', lineHeight: 1.5 }}>
|
| 778 |
-
This is the text read from your image using OCR. Poor image quality may cause errors.
|
| 779 |
-
</p>
|
| 780 |
-
)}
|
| 781 |
-
{result.input_type === 'video' && (
|
| 782 |
-
<p className="text-xs mb-3" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)', lineHeight: 1.5 }}>
|
| 783 |
-
This is the speech transcribed from your video/audio file.
|
| 784 |
-
</p>
|
| 785 |
-
)}
|
| 786 |
-
<pre
|
| 787 |
-
style={{
|
| 788 |
-
whiteSpace: 'pre-wrap',
|
| 789 |
-
wordBreak: 'break-word',
|
| 790 |
-
fontFamily: 'var(--font-mono)',
|
| 791 |
-
fontSize: 12,
|
| 792 |
-
lineHeight: 1.7,
|
| 793 |
-
color: 'var(--text-secondary)',
|
| 794 |
-
background: 'var(--bg-elevated)',
|
| 795 |
-
border: '1px solid var(--border)',
|
| 796 |
-
borderRadius: 2,
|
| 797 |
-
padding: '12px 14px',
|
| 798 |
-
maxHeight: 280,
|
| 799 |
-
overflowY: 'auto',
|
| 800 |
-
}}
|
| 801 |
-
>
|
| 802 |
-
{result.extracted_text}
|
| 803 |
-
</pre>
|
| 804 |
-
</div>
|
| 805 |
-
)}
|
| 806 |
-
</div>
|
| 807 |
-
)}
|
| 808 |
-
|
| 809 |
{/* Row 3: Layer cards (2 col, collapses to 1 on mobile) */}
|
| 810 |
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 fade-up-4">
|
| 811 |
{/* Layer 1 */}
|
|
|
|
| 237 |
const [tab, setTab] = useState(persisted?.tab ?? 'text')
|
| 238 |
const [input, setInput] = useState(persisted?.input ?? '')
|
| 239 |
const [file, setFile] = useState(null)
|
| 240 |
+
const [fileObjectUrl, setFileObjectUrl] = useState(null)
|
| 241 |
const [dragOver, setDragOver] = useState(false)
|
| 242 |
const [loading, setLoading] = useState(false)
|
| 243 |
const [result, setResult] = useState(persisted?.result ?? null)
|
|
|
|
| 258 |
}
|
| 259 |
}, [submittedInput])
|
| 260 |
|
| 261 |
+
/* Create/revoke object URL for in-form file preview */
|
| 262 |
+
useEffect(() => {
|
| 263 |
+
if (!file) { setFileObjectUrl(null); return }
|
| 264 |
+
const url = URL.createObjectURL(file)
|
| 265 |
+
setFileObjectUrl(url)
|
| 266 |
+
return () => URL.revokeObjectURL(url)
|
| 267 |
+
}, [file])
|
| 268 |
+
|
| 269 |
/* Persist result + input to sessionStorage so state survives navigation/refresh */
|
| 270 |
useEffect(() => {
|
| 271 |
if (result) {
|
|
|
|
| 330 |
}
|
| 331 |
|
| 332 |
function handleTabChange(id) {
|
| 333 |
+
setTab(id); setInput(''); setFile(null); setFileObjectUrl(null); setResult(null); setError(null); setSubmittedInput(null); setUrlPreview(null)
|
| 334 |
sessionStorage.removeItem(STORAGE_KEY)
|
| 335 |
}
|
| 336 |
|
| 337 |
function handleVerifyAgain() {
|
| 338 |
setResult(null); setError(null); setExtractedTextOpen(false)
|
| 339 |
+
setFile(null); setFileObjectUrl(null); setUrlPreview(null); setSubmittedInput(null)
|
| 340 |
sessionStorage.removeItem(STORAGE_KEY)
|
| 341 |
// Smooth-scroll back to the input panel
|
| 342 |
requestAnimationFrame(() => {
|
|
|
|
| 492 |
name={tab === 'image' ? 'media-image' : 'media-video'}
|
| 493 |
accept={tab === 'image' ? 'image/*' : 'video/*,audio/*'}
|
| 494 |
onChange={e => setFile(e.target.files[0])} />
|
| 495 |
+
{file && fileObjectUrl && tab === 'image' ? (
|
| 496 |
+
<div className="flex flex-col items-center gap-2">
|
| 497 |
+
<img
|
| 498 |
+
src={fileObjectUrl}
|
| 499 |
+
alt={file.name}
|
| 500 |
+
style={{
|
| 501 |
+
maxHeight: 200,
|
| 502 |
+
maxWidth: '100%',
|
| 503 |
+
borderRadius: 2,
|
| 504 |
+
border: '1px solid var(--border)',
|
| 505 |
+
objectFit: 'contain',
|
| 506 |
+
}}
|
| 507 |
+
/>
|
| 508 |
+
<p className="text-xs" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
|
| 509 |
+
{file.name} · {(file.size / 1024).toFixed(1)} KB
|
| 510 |
+
</p>
|
| 511 |
+
</div>
|
| 512 |
+
) : file && fileObjectUrl && tab === 'video' ? (
|
| 513 |
+
<div className="flex flex-col items-center gap-2">
|
| 514 |
+
<video
|
| 515 |
+
src={fileObjectUrl}
|
| 516 |
+
controls
|
| 517 |
+
onClick={e => e.stopPropagation()}
|
| 518 |
+
style={{
|
| 519 |
+
maxHeight: 200,
|
| 520 |
+
maxWidth: '100%',
|
| 521 |
+
borderRadius: 2,
|
| 522 |
+
border: '1px solid var(--border)',
|
| 523 |
+
background: '#000',
|
| 524 |
+
}}
|
| 525 |
+
/>
|
| 526 |
+
<p className="text-xs" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
|
| 527 |
+
{file.name} · {(file.size / (1024 * 1024)).toFixed(2)} MB
|
| 528 |
+
</p>
|
| 529 |
+
</div>
|
| 530 |
+
) : (
|
| 531 |
+
<>
|
| 532 |
+
<Upload size={18} aria-hidden="true"
|
| 533 |
+
style={{ margin: '0 auto 8px', color: 'var(--text-muted)' }} />
|
| 534 |
<p className="text-sm" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }}>
|
| 535 |
Drop or click to upload {tab === 'image' ? 'image' : 'video / audio'}
|
| 536 |
</p>
|
|
|
|
| 538 |
or press <kbd style={{ background: 'var(--bg-hover)', border: '1px solid var(--border)', borderRadius: 2, padding: '1px 5px', fontFamily: 'var(--font-mono)', fontSize: 10 }}>Ctrl+V</kbd> to paste from clipboard
|
| 539 |
</p>
|
| 540 |
</>
|
| 541 |
+
)}
|
| 542 |
</div>
|
| 543 |
</div>
|
| 544 |
)}
|
|
|
|
| 569 |
<div className="card p-4 fade-up" style={{ borderLeft: '3px solid var(--accent-red)' }}>
|
| 570 |
<p className="text-xs font-semibold uppercase tracking-widest mb-2"
|
| 571 |
style={{ fontFamily: 'var(--font-display)', color: 'var(--text-muted)', letterSpacing: '0.15em' }}>
|
| 572 |
+
{submittedInput.type === 'url' ? 'Scraped Text'
|
| 573 |
+
: submittedInput.type === 'image' ? 'OCR Extracted Text'
|
| 574 |
+
: submittedInput.type === 'video' ? 'Transcribed Text'
|
| 575 |
+
: 'Verified Input'}
|
| 576 |
</p>
|
| 577 |
{submittedInput.type === 'url' && (
|
| 578 |
<div className="space-y-2">
|
| 579 |
+
{result?.extracted_text
|
| 580 |
+
? (
|
| 581 |
+
<pre style={{
|
| 582 |
+
whiteSpace: 'pre-wrap',
|
| 583 |
+
wordBreak: 'break-word',
|
| 584 |
+
fontFamily: 'var(--font-mono)',
|
| 585 |
+
fontSize: 12,
|
| 586 |
+
lineHeight: 1.7,
|
| 587 |
+
color: 'var(--text-secondary)',
|
| 588 |
+
background: 'var(--bg-elevated)',
|
| 589 |
+
border: '1px solid var(--border)',
|
| 590 |
+
borderRadius: 2,
|
| 591 |
+
padding: '12px 14px',
|
| 592 |
+
maxHeight: 280,
|
| 593 |
+
overflowY: 'auto',
|
| 594 |
+
margin: 0,
|
| 595 |
+
}}>
|
| 596 |
+
{result.extracted_text}
|
| 597 |
+
</pre>
|
| 598 |
+
)
|
| 599 |
: (
|
| 600 |
<a href={submittedInput.text} target="_blank" rel="noreferrer"
|
| 601 |
className="flex items-center gap-2 text-sm"
|
|
|
|
| 624 |
</p>
|
| 625 |
)}
|
| 626 |
{submittedInput.type === 'image' && (
|
| 627 |
+
result?.extracted_text
|
| 628 |
+
? (
|
| 629 |
+
<pre style={{
|
| 630 |
+
whiteSpace: 'pre-wrap',
|
| 631 |
+
wordBreak: 'break-word',
|
| 632 |
+
fontFamily: 'var(--font-mono)',
|
| 633 |
+
fontSize: 12,
|
| 634 |
+
lineHeight: 1.7,
|
| 635 |
+
color: 'var(--text-secondary)',
|
| 636 |
+
background: 'var(--bg-elevated)',
|
| 637 |
+
border: '1px solid var(--border)',
|
| 638 |
+
borderRadius: 2,
|
| 639 |
+
padding: '12px 14px',
|
| 640 |
+
maxHeight: 280,
|
| 641 |
+
overflowY: 'auto',
|
| 642 |
+
margin: 0,
|
| 643 |
+
}}>
|
| 644 |
+
{result.extracted_text}
|
| 645 |
+
</pre>
|
| 646 |
+
)
|
| 647 |
+
: (
|
| 648 |
+
<div className="flex items-center gap-3">
|
| 649 |
+
{submittedInput.fileUrl && (
|
| 650 |
+
<img src={submittedInput.fileUrl} alt=""
|
| 651 |
+
style={{ width: 48, height: 48, objectFit: 'cover', borderRadius: 2, border: '1px solid var(--border)', flexShrink: 0 }} />
|
| 652 |
+
)}
|
| 653 |
+
<p className="text-xs" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
|
| 654 |
+
{submittedInput.file?.name}
|
| 655 |
+
</p>
|
| 656 |
+
</div>
|
| 657 |
+
)
|
| 658 |
)}
|
| 659 |
{submittedInput.type === 'video' && (
|
| 660 |
+
result?.extracted_text
|
| 661 |
+
? (
|
| 662 |
+
<pre style={{
|
| 663 |
+
whiteSpace: 'pre-wrap',
|
| 664 |
+
wordBreak: 'break-word',
|
| 665 |
+
fontFamily: 'var(--font-mono)',
|
| 666 |
+
fontSize: 12,
|
| 667 |
+
lineHeight: 1.7,
|
| 668 |
+
color: 'var(--text-secondary)',
|
| 669 |
+
background: 'var(--bg-elevated)',
|
| 670 |
+
border: '1px solid var(--border)',
|
| 671 |
+
borderRadius: 2,
|
| 672 |
+
padding: '12px 14px',
|
| 673 |
+
maxHeight: 280,
|
| 674 |
+
overflowY: 'auto',
|
| 675 |
+
margin: 0,
|
| 676 |
+
}}>
|
| 677 |
+
{result.extracted_text}
|
| 678 |
+
</pre>
|
| 679 |
+
)
|
| 680 |
+
: (
|
| 681 |
+
<div className="flex items-center gap-2">
|
| 682 |
+
<Video size={15} style={{ color: 'var(--text-muted)', flexShrink: 0 }} aria-hidden="true" />
|
| 683 |
+
<p className="text-xs" style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-mono)' }}>
|
| 684 |
+
{submittedInput.file?.name}
|
| 685 |
+
</p>
|
| 686 |
+
</div>
|
| 687 |
+
)
|
| 688 |
)}
|
| 689 |
</div>
|
| 690 |
)}
|
|
|
|
| 812 |
</p>
|
| 813 |
</div>
|
| 814 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 815 |
{/* Row 3: Layer cards (2 col, collapses to 1 on mobile) */}
|
| 816 |
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 fade-up-4">
|
| 817 |
{/* Layer 1 */}
|