330 lines
15 KiB
TypeScript
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>
|
|
);
|
|
};
|