pradit/pages/Playground.tsx
2025-11-20 17:04:57 +07:00

330 lines
15 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
UploadCloud,
FileText,
Database,
Search,
CheckCircle2,
ArrowRight,
Loader2,
Lock,
Cpu,
File,
Terminal,
} from "lucide-react";
import { useLanguage } from "../App";
import { TerminalBlock } from "../components/TerminalBlock";
export const Playground: React.FC = () => {
const { language } = useLanguage();
const [step, setStep] = useState<"upload" | "processing" | "ready">("upload");
const [processingStage, setProcessingStage] = useState(0); // 0: Parsing, 1: Chunking, 2: Embedding, 3: Indexing
const [query, setQuery] = useState("");
const [showResult, setShowResult] = useState(false);
const content = {
en: {
title: "Enterprise Knowledge Engine",
subtitle:
"Experience our secure RAG pipeline architecture. Turn unstructured documents into queryable intelligence.",
upload_title: "Data Ingestion",
upload_desc:
"Drag and drop your internal PDFs, DOCX, or Wiki dumps here. We simulate the ingestion process secure enterprises use.",
drop_label: "Drop corporate assets here",
processing_title: "Processing Pipeline",
stages: [
"Optical Character Recognition (OCR)",
"Semantic Chunking",
"Vector Embedding (text-embedding-3-large)",
"Pinecone Indexing",
],
query_placeholder: "Ask a question about the uploaded documents...",
query_btn: "Retrieve Insight",
result_title: "Synthesized Answer",
source_title: "Source Attribution",
value_prop:
"Why this matters: Most enterprise knowledge is trapped in PDFs. We build pipelines that unlock this data while respecting RBAC (Role-Based Access Control).",
},
th: {
title: "เครื่องยนต์ความรู้องค์กร",
subtitle:
"สัมผัสสถาปัตยกรรม RAG ที่ปลอดภัยของเรา เปลี่ยนเอกสารที่ไม่มีโครงสร้างให้เป็นข้อมูลอัจฉริยะที่ค้นหาได้",
upload_title: "การนำเข้าข้อมูล",
upload_desc:
"ลากและวาง PDF, DOCX หรือ Wiki ภายในองค์กรที่นี่ เราจำลองกระบวนการนำเข้าข้อมูลที่องค์กรระดับสูงใช้",
drop_label: "วางไฟล์ข้อมูลองค์กรที่นี่",
processing_title: "ท่อประมวลผลข้อมูล",
stages: [
"การแปลงภาพเป็นข้อความ (OCR)",
"การแบ่งส่วนความหมาย (Semantic Chunking)",
"การสร้าง Vector (text-embedding-3-large)",
"การจัดทำดัชนี Pinecone",
],
query_placeholder: "ถามคำถามเกี่ยวกับเอกสารที่อัปโหลด...",
query_btn: "ค้นหาข้อมูลเชิงลึก",
result_title: "คำตอบที่สังเคราะห์แล้ว",
source_title: "การอ้างอิงแหล่งที่มา",
value_prop:
"ทำไมสิ่งนี้ถึงสำคัญ: ความรู้องค์กรส่วนใหญ่ติดอยู่ใน PDF เราสร้างระบบที่ปลดล็อกข้อมูลนี้โดยยังคงรักษาความปลอดภัยตามสิทธิ์การเข้าถึง (RBAC)",
},
};
const t = content[language];
const handleSimulateUpload = () => {
setStep("processing");
setProcessingStage(0);
// Simulate pipeline stages
const interval = setInterval(() => {
setProcessingStage((prev) => {
if (prev >= 3) {
clearInterval(interval);
setTimeout(() => setStep("ready"), 800);
return 3;
}
return prev + 1;
});
}, 1500);
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (!query) return;
setShowResult(true);
};
return (
<div className="bg-paper min-h-screen pt-24 pb-24">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<header className="mb-16 text-center max-w-3xl mx-auto">
<h1 className="font-serif text-4xl md:text-5xl text-ink leading-tight mb-6">
{t.title}
</h1>
<p className="font-serif text-xl text-subtle leading-relaxed">
{t.subtitle}
</p>
</header>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
{/* Left: Demo Interface */}
<div className="lg:col-span-7 space-y-8">
{/* Step 1: Upload */}
<div
className={`border-2 border-dashed rounded-lg transition-all duration-500 ${step === "upload" ? "border-ink bg-gray-50 p-12" : "border-gray-200 bg-white p-6 opacity-50"}`}
>
<div className="flex flex-col items-center text-center">
<div
className={`p-4 rounded-full mb-4 ${step === "upload" ? "bg-white shadow-md text-ink" : "bg-gray-100 text-gray-400"}`}
>
<UploadCloud size={step === "upload" ? 32 : 24} />
</div>
<h3 className="font-sans font-bold uppercase tracking-widest text-sm mb-2">
{t.upload_title}
</h3>
{step === "upload" && (
<>
<p className="font-serif text-subtle text-sm mb-8 max-w-md">
{t.upload_desc}
</p>
<button
onClick={handleSimulateUpload}
className="bg-ink text-white px-8 py-3 rounded-sm font-bold text-xs uppercase tracking-widest hover:bg-gray-800 transition-colors shadow-lg"
>
{t.drop_label}
</button>
<div className="mt-6 flex gap-4 text-xs text-gray-400 font-mono">
<span className="flex items-center">
<File size={12} className="mr-1" /> policy_v2.pdf
</span>
<span className="flex items-center">
<File size={12} className="mr-1" /> technical_specs.docx
</span>
</div>
</>
)}
</div>
</div>
{/* Step 2: Processing Pipeline */}
{(step === "processing" || step === "ready") && (
<div className="bg-white border border-gray-200 rounded-lg p-8 shadow-sm animate-in fade-in slide-in-from-bottom-4">
<h3 className="font-sans font-bold uppercase tracking-widest text-sm mb-6 flex items-center gap-2">
<Cpu size={16} className="text-accent" /> {t.processing_title}
</h3>
<div className="space-y-6">
{t.stages.map((stage, index) => (
<div
key={index}
className="relative pl-8 border-l-2 border-gray-100"
>
<div
className={`absolute -left-[9px] top-0 w-4 h-4 rounded-full border-2 transition-colors duration-500 ${
processingStage > index
? "bg-green-500 border-green-500"
: processingStage === index
? "bg-white border-accent animate-pulse"
: "bg-white border-gray-200"
}`}
></div>
<div
className={`transition-opacity duration-500 ${processingStage >= index ? "opacity-100" : "opacity-30"}`}
>
<p className="font-mono text-sm font-bold text-ink">
{stage}
</p>
{processingStage === index && (
<p className="text-xs text-accent mt-1 font-mono animate-pulse">
Processing...
</p>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Step 3: Query Interface */}
{step === "ready" && (
<div className="bg-white border border-gray-200 rounded-lg p-8 shadow-lg animate-in fade-in slide-in-from-bottom-4 relative overflow-hidden">
<div className="absolute top-0 right-0 p-4">
<div className="flex items-center gap-1 px-2 py-1 bg-green-50 text-green-700 rounded text-[10px] font-bold uppercase tracking-wide">
<Lock size={10} /> Secure Context
</div>
</div>
<form onSubmit={handleSearch} className="relative mb-8">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full bg-gray-50 border border-gray-200 p-4 pr-12 rounded-sm text-ink focus:outline-none focus:border-ink focus:ring-1 focus:ring-ink font-serif placeholder:text-gray-400"
placeholder={t.query_placeholder}
/>
<button
type="submit"
className="absolute right-2 top-2 bottom-2 bg-white text-ink p-2 rounded-sm hover:bg-gray-100 border border-gray-200 transition-colors"
>
<Search size={20} />
</button>
</form>
{showResult && (
<div className="animate-in fade-in">
<div className="mb-6">
<h4 className="font-sans font-bold text-xs uppercase tracking-widest text-subtle mb-2">
{t.result_title}
</h4>
<p className="font-serif text-lg leading-relaxed text-ink">
Based on the{" "}
<span className="bg-blue-100 px-1 font-mono text-sm">
technical_specs.docx
</span>
, the maximum concurrent connection limit for the
websocket gateway is{" "}
<strong className="text-ink">10,000 users</strong> per
shard. To increase this, you must scale the Redis
cluster horizontally.
</p>
</div>
<div className="bg-gray-50 p-4 rounded border border-gray-200">
<h4 className="font-sans font-bold text-[10px] uppercase tracking-widest text-gray-400 mb-2 flex items-center gap-2">
<Database size={10} /> {t.source_title}
</h4>
<div className="font-mono text-xs text-gray-500 p-2 bg-white border border-gray-100 border-l-4 border-l-accent">
"...gateway.max_connections = 10000 # Hard limit per
shard. See Redis scaling section..."
<div className="mt-1 text-[10px] text-gray-300 text-right">
Score: 0.89
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
{/* Right: Architecture Context */}
<div className="lg:col-span-5">
<div className="sticky top-32">
<div className="bg-[#1e1e1e] text-gray-300 rounded-lg overflow-hidden shadow-2xl border border-gray-800 mb-8">
<div className="bg-[#252526] px-4 py-2 border-b border-gray-700 flex items-center gap-2">
<Terminal size={12} />
<span className="text-xs font-mono">pipeline_logs.txt</span>
</div>
<div className="p-6 font-mono text-xs space-y-2 h-[300px] overflow-y-auto">
<div className="text-green-400">
$ init_ingestion_job --secure
</div>
{step === "processing" && (
<>
<div className="text-gray-400">
[INFO] File uploaded: technical_specs.docx (2.4MB)
</div>
<div className="text-gray-400">
[INFO] Parsing PDF structure...
</div>
{processingStage >= 1 && (
<div className="text-blue-400">
[PROC] Chunking strategy: RecursiveCharacter (512tk)
</div>
)}
{processingStage >= 2 && (
<div className="text-purple-400">
[EMBD] Batch embedding 142 chunks...
</div>
)}
{processingStage >= 3 && (
<div className="text-yellow-400">
[INDX] Upserting to namespace 'corp-knowledge'...
</div>
)}
</>
)}
{step === "ready" && (
<>
<div className="text-green-400">
[DONE] Indexing complete. Latency: 4.2s
</div>
<div className="text-gray-400">
----------------------------------------
</div>
<div className="text-white animate-pulse">
$ waiting_for_query...
</div>
{showResult && (
<>
<div className="text-green-400">$ query_received</div>
<div className="text-gray-400">
[RETR] Semantic search k=3
</div>
<div className="text-gray-400">
[GEN] LLM Context window filled (1200 tokens)
</div>
</>
)}
</>
)}
</div>
</div>
<div className="p-6 bg-blue-50 border border-blue-100 rounded-lg">
<h4 className="font-serif font-bold text-accent text-lg mb-2">
Business Value
</h4>
<p className="font-serif text-sm text-ink leading-relaxed">
{t.value_prop}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};