initial commit

This commit is contained in:
sirin.ph 2025-11-20 17:04:57 +07:00
commit 6f9defe29a
20 changed files with 4100 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

267
App.tsx Normal file
View File

@ -0,0 +1,267 @@
import React, { useContext, useState } from "react";
import { HashRouter, Routes, Route, Link, useLocation } from "react-router-dom";
import { Home } from "./pages/Home";
import { BlogPost } from "./pages/BlogPost";
import { Research } from "./pages/Research";
import { Services } from "./pages/Services";
import { HireUs } from "./pages/HireUs";
import { Playground } from "./pages/Playground";
import { ChatWidget } from "./components/ChatWidget";
import { Cpu, Menu, X, Globe, Terminal } from "lucide-react";
// --- Language Context ---
type Language = "en" | "th";
interface LanguageContextType {
language: Language;
toggleLanguage: () => void;
}
export const LanguageContext = React.createContext<LanguageContextType>({
language: "en",
toggleLanguage: () => {},
});
export const useLanguage = () => useContext(LanguageContext);
// --- Navigation ---
const Navigation: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const location = useLocation();
const { language, toggleLanguage } = useLanguage();
const isActive = (path: string) => {
if (path === "/research" && location.pathname.startsWith("/research")) {
return "text-ink font-semibold";
}
return location.pathname === path
? "text-ink font-semibold"
: "text-subtle hover:text-ink transition-colors";
};
return (
<nav className="sticky top-0 z-50 bg-paper/95 backdrop-blur-sm border-b border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-20">
<div className="flex items-center">
<Link to="/" className="flex items-center space-x-2">
<Cpu className="h-6 w-6 text-ink" />
<span className="font-sans tracking-widest text-sm font-bold uppercase text-ink">
Pradit
</span>
</Link>
</div>
{/* Desktop Nav */}
<div className="hidden md:flex items-center space-x-8 font-sans text-sm">
<Link to="/" className={isActive("/")}>
{language === "en" ? "Work" : "ผลงาน"}
</Link>
<Link to="/services" className={isActive("/services")}>
{language === "en" ? "Services" : "บริการ"}
</Link>
<Link to="/research" className={isActive("/research")}>
{language === "en" ? "Research" : "วิจัย"}
</Link>
<Link
to="/playground"
className={`${isActive("/playground")} flex items-center gap-1`}
>
<Terminal size={14} />
{language === "en" ? "Playground" : "ลองเล่น"}
</Link>
<Link
to="/hire-us"
className="bg-ink text-white px-4 py-2 rounded-sm hover:bg-gray-800 transition-colors"
>
{language === "en" ? "Hire Us" : "ติดต่อเรา"}
</Link>
{/* Language Toggle */}
<button
onClick={toggleLanguage}
className="flex items-center space-x-1 text-xs font-bold border border-gray-200 rounded-full px-3 py-1 hover:bg-gray-50 transition-colors"
>
<Globe size={14} className="text-subtle" />
<span className={language === "en" ? "text-ink" : "text-subtle"}>
EN
</span>
<span className="text-gray-300">|</span>
<span className={language === "th" ? "text-ink" : "text-subtle"}>
TH
</span>
</button>
</div>
{/* Mobile button */}
<div className="md:hidden flex items-center gap-4">
<button
onClick={toggleLanguage}
className="flex items-center space-x-1 text-xs font-bold border border-gray-200 rounded-full px-3 py-1"
>
<span className={language === "en" ? "text-ink" : "text-subtle"}>
EN
</span>
<span className="text-gray-300">|</span>
<span className={language === "th" ? "text-ink" : "text-subtle"}>
TH
</span>
</button>
<button onClick={() => setIsOpen(!isOpen)} className="text-ink p-2">
{isOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
</div>
</div>
{/* Mobile Menu */}
{isOpen && (
<div className="md:hidden bg-white border-b border-gray-100">
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<Link
to="/"
onClick={() => setIsOpen(false)}
className="block px-3 py-2 text-base font-medium text-ink"
>
Work
</Link>
<Link
to="/services"
onClick={() => setIsOpen(false)}
className="block px-3 py-2 text-base font-medium text-subtle"
>
Services
</Link>
<Link
to="/research"
onClick={() => setIsOpen(false)}
className="block px-3 py-2 text-base font-medium text-subtle"
>
Research
</Link>
<Link
to="/playground"
onClick={() => setIsOpen(false)}
className="block px-3 py-2 text-base font-medium text-subtle"
>
Playground
</Link>
<Link
to="/hire-us"
onClick={() => setIsOpen(false)}
className="block px-3 py-2 text-base font-medium text-ink font-bold"
>
Hire Us
</Link>
</div>
</div>
)}
</nav>
);
};
const Footer: React.FC = () => {
const { language } = useLanguage();
return (
<footer className="bg-white border-t border-gray-100 py-12 mt-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="col-span-1 md:col-span-2">
<span className="font-sans tracking-widest text-xs font-bold uppercase text-ink">
Pradit
</span>
<p className="mt-4 text-subtle font-serif text-sm max-w-md">
{language === "en"
? "We build production-grade AI systems. No hype, just engineering. Specializing in LLM integration, custom evaluations, and scalable architecture."
: "เราสร้างระบบ AI ระดับใช้งานจริง เน้นวิศวกรรมไม่เน้นโฆษณา เชี่ยวชาญด้านการรวมระบบ LLM, การประเมินผล และสถาปัตยกรรมที่ปรับขนาดได้"}
</p>
</div>
<div>
<h3 className="font-sans font-bold text-xs uppercase tracking-wider mb-4">
Offerings
</h3>
<ul className="space-y-2 text-sm text-subtle font-serif">
<li>
<Link
to="/services"
className="hover:text-ink hover:underline decoration-1 underline-offset-2"
>
MVP Development
</Link>
</li>
<li>
<Link
to="/services"
className="hover:text-ink hover:underline decoration-1 underline-offset-2"
>
System Audits
</Link>
</li>
<li>
<Link
to="/services"
className="hover:text-ink hover:underline decoration-1 underline-offset-2"
>
Custom Integration
</Link>
</li>
</ul>
</div>
<div>
<h3 className="font-sans font-bold text-xs uppercase tracking-wider mb-4">
Connect
</h3>
<ul className="space-y-2 text-sm text-subtle font-serif">
<li>
<a href="#" className="hover:text-ink">
GitHub
</a>
</li>
<li>
<Link to="/playground" className="hover:text-ink">
Playground
</Link>
</li>
<li>
<Link to="/hire-us" className="hover:text-ink">
Hire Us
</Link>
</li>
</ul>
</div>
</div>
</footer>
);
};
export default function App() {
const [language, setLanguage] = useState<Language>("en");
const toggleLanguage = () => {
setLanguage((prev) => (prev === "en" ? "th" : "en"));
};
return (
<LanguageContext.Provider value={{ language, toggleLanguage }}>
<HashRouter>
<div className="min-h-screen bg-paper flex flex-col relative">
<Navigation />
<main className="flex-grow">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/research" element={<Research />} />
<Route
path="/research/lora-without-regret"
element={<BlogPost />}
/>
<Route path="/services" element={<Services />} />
<Route path="/playground" element={<Playground />} />
<Route path="/hire-us" element={<HireUs />} />
</Routes>
</main>
<Footer />
<ChatWidget />
</div>
</HashRouter>
</LanguageContext.Provider>
);
}

20
README.md Normal file
View File

@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1lSdt9DucwKsU3WO83AD5ieLoPx_QLmVF
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

79
STYLE_GUIDE.md Normal file
View File

@ -0,0 +1,79 @@
# Pradit (ประดิษฐ์) Design System
**Philosophy:** "Engineer First, Sales Second."
The aesthetic of Pradit is minimal, precise, and reminiscent of technical documentation or a high-end code editor. It avoids gradients, heavy drop shadows, and stock photography in favor of clean typography, terminal-inspired visual elements, and high information density.
---
## 1. Color Palette
We use a strict semantic color system.
| Token | Hex | Usage |
| :---------- | :-------- | :------------------------------------------------------------------------------- |
| **Paper** | `#fdfdfd` | Main background. Slightly off-white to reduce eye strain. |
| **Ink** | `#111111` | Primary text, strong headers, active states. |
| **Subtle** | `#666666` | Secondary text, metadata, descriptions. |
| **Accent** | `#2563eb` | Primary action color (Blue-600). Used sparingly for links and active indicators. |
| **Code Bg** | `#1e1e1e` | Dark background for terminal blocks and code snippets (VS Code Dark default). |
---
## 2. Typography
We use a tri-font stack to separate functional UI from reading content.
### Serif: Merriweather
_Usage:_ Long-form text, blog posts, titles, quotes.
_Why:_ Establishes authority and academic rigor.
### Sans-Serif: Inter
_Usage:_ UI labels, navigation, buttons, short descriptions.
_Why:_ Clean, legible, and modern.
### Monospace: Fira Code
_Usage:_ Code snippets, terminal inputs, tags, dates, metadata.
_Why:_ Signals technical precision.
---
## 3. UI Patterns & Components
### The "Terminal" Aesthetic
Elements that represent "work" (code, search, inputs) should mimic terminal interfaces.
- **Inputs:** No rounded corners (or very slight `rounded-sm`). Bottom borders or dark backgrounds.
- **Prompt:** Use `>` or `$` prefixes for input labels.
### Borders over Shadows
Use 1px borders (`border-gray-200`) to define hierarchy. Shadows should be subtle and used only for floating elements (modals, sticky headers).
### Buttons
- **Primary:** `bg-ink` text-white, `rounded-sm`. Uppercase text, tracking-widest.
- **Secondary:** `border border-gray-200` text-ink.
- **Tags/Pills:** `rounded-full`, `uppercase`, `text-xs`, `font-bold`.
### Interactive Visualizations
Do not use static images for technical concepts. Use code blocks (`TerminalBlock`) or interactive React components (`MatrixViz`) that allow the user to manipulate variables.
---
## 4. Layout Grid
- **Container:** Max-width 7xl (`max-w-7xl`) centered.
- **Spacing:** Generous vertical whitespace (`py-24`).
- **Columns:** 12-column grid for complex layouts (Sidebar takes 3, Content takes 6-9).
---
## 5. Voice & Tone
- **English:** Professional, concise, jargon-correct. No marketing fluff.
- **Thai:** Formal but accessible. Use English terms for specific technical concepts (e.g., "Fine-tuning", "Latency") where a Thai translation would be ambiguous.

110
components/ChatWidget.tsx Normal file
View File

@ -0,0 +1,110 @@
import React, { useState, useRef, useEffect } from 'react';
import { MessageSquare, X, Send, Terminal } from 'lucide-react';
interface Message {
id: string;
text: string;
sender: 'user' | 'bot';
timestamp: Date;
}
export const ChatWidget: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{ id: '1', text: "System online. I am Pradit's automated assistant. How can I help you engineer your next AI system?", sender: 'bot', timestamp: new Date() }
]);
const [input, setInput] = useState("");
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}
}, [messages, isOpen]);
const handleSend = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
const userMsg: Message = { id: Date.now().toString(), text: input, sender: 'user', timestamp: new Date() };
setMessages(prev => [...prev, userMsg]);
setInput("");
// Mock response
setTimeout(() => {
const botMsg: Message = {
id: (Date.now() + 1).toString(),
text: "ACK. Request received. Our engineers are currently optimizing a training run. Please visit 'Hire Us' to schedule a deep dive consultation.",
sender: 'bot',
timestamp: new Date()
};
setMessages(prev => [...prev, botMsg]);
}, 800);
};
return (
<div className="fixed bottom-6 right-6 z-50 font-sans">
{/* Chat Window */}
{isOpen && (
<div className="mb-4 w-[320px] md:w-[360px] bg-white rounded-sm shadow-2xl border border-gray-200 flex flex-col overflow-hidden animate-in slide-in-from-bottom-10 fade-in duration-200">
{/* Header */}
<div className="bg-[#1e1e1e] text-white p-4 flex items-center justify-between border-b border-gray-800">
<div className="flex items-center gap-3">
<div className="p-1 bg-green-500/20 rounded-sm">
<Terminal size={16} className="text-green-400" />
</div>
<span className="text-xs font-mono font-bold tracking-wider uppercase text-gray-200">Pradit/Assistant</span>
</div>
<button onClick={() => setIsOpen(false)} className="text-gray-400 hover:text-white transition-colors">
<X size={18} />
</button>
</div>
{/* Messages */}
<div className="h-[350px] overflow-y-auto p-4 bg-[#fdfdfd] space-y-4">
{messages.map((msg) => (
<div key={msg.id} className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-[85%] p-3 rounded-sm text-sm font-serif leading-relaxed ${
msg.sender === 'user'
? 'bg-gray-100 text-ink border border-gray-200'
: 'bg-white text-subtle border border-gray-100 shadow-sm'
}`}>
{msg.text}
</div>
</div>
))}
<div ref={bottomRef} />
</div>
{/* Input */}
<form onSubmit={handleSend} className="p-3 bg-white border-t border-gray-200 flex gap-2">
<div className="flex-grow flex items-center bg-gray-50 border border-gray-200 px-3 py-2 rounded-sm focus-within:border-ink focus-within:ring-1 focus-within:ring-ink/5 transition-all">
<span className="text-gray-400 mr-2 text-xs font-mono">{">"}</span>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your query..."
className="flex-grow bg-transparent text-sm outline-none text-ink placeholder:text-gray-400 font-sans"
/>
</div>
<button type="submit" className="bg-ink text-white p-2 rounded-sm hover:bg-gray-800 transition-colors flex items-center justify-center">
<Send size={16} />
</button>
</form>
</div>
)}
{/* Toggle Button */}
{!isOpen && (
<button
onClick={() => setIsOpen(true)}
className="group flex items-center gap-2 bg-ink hover:bg-gray-800 text-white pl-4 pr-5 py-3 rounded-full shadow-lg transition-all hover:scale-105"
>
<MessageSquare size={20} className="text-gray-300 group-hover:text-white transition-colors" />
<span className="font-bold text-sm tracking-wide">Chat</span>
</button>
)}
</div>
);
};

164
components/MatrixViz.tsx Normal file
View File

@ -0,0 +1,164 @@
import React, { useState } from "react";
import { Sliders } from "lucide-react";
export const MatrixViz: React.FC = () => {
const [rank, setRank] = useState(4);
// Constants for visualization scaling
const d = 16; // Hidden dimension simulation
const k = 16; // Output dimension simulation
// Calculate imaginary parameters based on user slider
// Assuming a real model like Llama-7B, d_model might be 4096.
// We scale the display numbers to look realistic.
const realD = 4096;
const realK = 4096;
const paramsFull = realD * realK;
const paramsLoRA = realD * rank + rank * realK;
const reduction = ((1 - paramsLoRA / paramsFull) * 100).toFixed(2);
// Helper to generate random opacity blue blocks
const renderGrid = (
rows: number,
cols: number,
colorBase: string,
label: string,
) => {
return (
<div
className="relative transition-all duration-300 ease-in-out"
style={{
display: "grid",
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
width: `${cols * 6}px`,
height: `${rows * 6}px`,
border: "1px solid #e5e7eb",
backgroundColor: "#f9fafb",
}}
>
{Array.from({ length: rows * cols }).map((_, i) => (
<div
key={i}
style={{
backgroundColor: colorBase,
opacity: Math.random() * 0.6 + 0.2,
}}
/>
))}
{/* Matrix Label Overlay */}
<div className="absolute -bottom-6 left-0 w-full text-center text-[10px] font-mono text-gray-500">
{label}
</div>
</div>
);
};
return (
<div className="my-12 font-sans bg-gray-50 border border-gray-200 rounded-lg p-8">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-8 gap-6">
<div>
<h4 className="font-bold text-sm uppercase tracking-widest text-ink flex items-center gap-2">
<Sliders size={16} />
Interactive Decomposition
</h4>
<p className="text-xs text-subtle mt-1 max-w-md">
Adjust the Rank (r) to see how LoRA decomposes the weight update
matrix into two smaller dense matrices.
</p>
</div>
<div className="flex items-center gap-4 bg-white p-3 rounded-md border border-gray-200 shadow-sm">
<label
htmlFor="rank-slider"
className="text-xs font-bold text-ink uppercase"
>
Rank (r): {rank}
</label>
<input
id="rank-slider"
type="range"
min="1"
max="16"
step="1"
value={rank}
onChange={(e) => setRank(parseInt(e.target.value))}
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-accent"
/>
</div>
</div>
<div className="flex flex-wrap items-center justify-center gap-4 md:gap-12 overflow-x-auto py-4 min-h-[200px]">
{/* Matrix B (d x r) */}
<div className="flex flex-col items-center group">
<div className="mb-2 text-xs font-mono text-blue-600 font-bold">
B
</div>
{renderGrid(16, rank, "#93c5fd", `${realD} × ${rank}`)}
</div>
<div className="text-lg text-gray-400 font-light">×</div>
{/* Matrix A (r x k) */}
<div className="flex flex-col items-center group">
<div className="mb-2 text-xs font-mono text-blue-700 font-bold">
A
</div>
{renderGrid(rank, 16, "#2563eb", `${rank} × ${realK}`)}
</div>
{/* Arrow */}
<div className="text-lg text-gray-400 font-light"></div>
{/* Matrix Delta W */}
<div className="flex flex-col items-center opacity-50 grayscale">
<div className="mb-2 text-xs font-mono text-green-600 font-bold">
ΔW
</div>
{renderGrid(16, 16, "#4ade80", `${realD} × ${realK}`)}
</div>
</div>
{/* Math Dashboard */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-8 border-t border-gray-200 pt-6">
<div className="bg-white p-4 rounded border border-gray-100">
<div className="text-[10px] uppercase tracking-widest text-subtle mb-1">
Trainable Params (LoRA)
</div>
<div className="font-mono text-xl text-blue-600 font-bold">
{paramsLoRA.toLocaleString()}
</div>
<div className="text-[10px] text-gray-400 font-mono mt-1">
({realD}×{rank}) + ({rank}×{realK})
</div>
</div>
<div className="bg-white p-4 rounded border border-gray-100">
<div className="text-[10px] uppercase tracking-widest text-subtle mb-1">
Trainable Params (Full)
</div>
<div className="font-mono text-xl text-gray-400">
{paramsFull.toLocaleString()}
</div>
<div className="text-[10px] text-gray-400 font-mono mt-1">
{realD} × {realK}
</div>
</div>
<div className="bg-green-50 p-4 rounded border border-green-100">
<div className="text-[10px] uppercase tracking-widest text-green-800 mb-1">
Memory Reduction
</div>
<div className="font-mono text-xl text-green-600 font-bold">
{reduction}%
</div>
<div className="text-[10px] text-green-700 font-mono mt-1">
More VRAM for batch size
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,94 @@
import React, { useState, useEffect } from "react";
interface TerminalBlockProps {
title?: string;
lines: string[];
className?: string;
typingEffect?: boolean;
}
export const TerminalBlock: React.FC<TerminalBlockProps> = ({
title = "bash",
lines,
className,
typingEffect = false,
}) => {
const [displayedLines, setDisplayedLines] = useState<string[]>(
typingEffect ? [] : lines,
);
const [currentLineIndex, setCurrentLineIndex] = useState(0);
const [currentCharIndex, setCurrentCharIndex] = useState(0);
useEffect(() => {
if (!typingEffect) return;
if (currentLineIndex < lines.length) {
const currentLineFull = lines[currentLineIndex];
if (currentCharIndex < currentLineFull.length) {
const timeout = setTimeout(
() => {
setDisplayedLines((prev) => {
const newLines = [...prev];
if (newLines[currentLineIndex] === undefined) {
newLines[currentLineIndex] = currentLineFull[currentCharIndex];
} else {
newLines[currentLineIndex] += currentLineFull[currentCharIndex];
}
return newLines;
});
setCurrentCharIndex((prev) => prev + 1);
},
15 + Math.random() * 20,
); // Random variance for realism
return () => clearTimeout(timeout);
} else {
// Line finished, wait a bit then move to next
const timeout = setTimeout(() => {
setCurrentLineIndex((prev) => prev + 1);
setCurrentCharIndex(0);
}, 300);
return () => clearTimeout(timeout);
}
}
}, [typingEffect, lines, currentLineIndex, currentCharIndex]);
return (
<div
className={`rounded-md overflow-hidden bg-[#1e1e1e] shadow-xl border border-gray-800 ${className}`}
>
<div className="flex items-center px-4 py-2 bg-[#252526] border-b border-gray-700">
<div className="flex space-x-2 mr-4">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<div className="w-3 h-3 rounded-full bg-green-500"></div>
</div>
<div className="text-xs text-gray-400 font-mono">{title}</div>
</div>
<div className="p-6 font-mono text-sm leading-relaxed min-h-[200px]">
{displayedLines.map((line, idx) => (
<div key={idx} className="text-gray-300 break-words">
{line.startsWith("$") ? (
<span className="text-green-400">{line}</span>
) : line.startsWith(">") ? (
<span className="text-blue-400 ml-4">{line}</span>
) : line.startsWith("#") ? (
<span className="text-gray-500 italic">{line}</span>
) : (
<span className="text-gray-300">{line}</span>
)}
</div>
))}
{/* Cursor */}
{typingEffect && currentLineIndex < lines.length && (
<div className="animate-pulse inline-block w-2 h-4 bg-gray-500 align-middle ml-1"></div>
)}
{/* Static Cursor when done */}
{(!typingEffect || currentLineIndex >= lines.length) && (
<div className="animate-pulse mt-2 inline-block w-2 h-4 bg-gray-500 align-middle"></div>
)}
</div>
</div>
);
};

66
index.html Normal file
View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pradit | AI Consultancy</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
serif: ['Merriweather', 'serif'],
mono: ['Fira Code', 'monospace'],
},
colors: {
paper: '#fdfdfd',
ink: '#111111',
subtle: '#666666',
accent: '#2563eb',
}
}
}
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<style>
body {
background-color: #fdfdfd;
color: #111111;
}
/* Custom scrollbar for code blocks */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
<script type="importmap">
{
"imports": {
"react": "https://aistudiocdn.com/react@^19.2.0",
"react/": "https://aistudiocdn.com/react@^19.2.0/",
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
"react-router-dom": "https://aistudiocdn.com/react-router-dom@^7.9.6",
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0"
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

15
index.tsx Normal file
View File

@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

5
metadata.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Pradit",
"description": "A high-end, academic-style technical consultancy website featuring technical deep dives and clear service offerings.",
"requestFramePermissions": []
}

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "pradit",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.9.6",
"lucide-react": "^0.554.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

444
pages/BlogPost.tsx Normal file
View File

@ -0,0 +1,444 @@
import React, { useState, useEffect } from "react";
import { MatrixViz } from "../components/MatrixViz";
import { useLanguage } from "../App";
import {
Play,
Pause,
FileText,
Headphones,
Zap,
Volume2,
X,
} from "lucide-react";
export const BlogPost: React.FC = () => {
const { language } = useLanguage();
const [activeSection, setActiveSection] = useState("intro");
const [activeTab, setActiveTab] = useState<"read" | "listen">("read");
const [isPlaying, setIsPlaying] = useState(false);
const [showMiniPlayer, setShowMiniPlayer] = useState(false);
// Simple scroll spy logic
useEffect(() => {
const handleScroll = () => {
const sections = ["intro", "methods", "results", "discussion"];
for (const section of sections) {
const element = document.getElementById(section);
if (element) {
const rect = element.getBoundingClientRect();
if (rect.top >= 0 && rect.top <= 300) {
setActiveSection(section);
break;
}
}
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const handleMainPlayPause = () => {
const newState = !isPlaying;
setIsPlaying(newState);
if (newState) {
setShowMiniPlayer(true);
}
};
const content = {
en: {
title: "LoRA Without Regret",
subtitle: "John Schulman in collaboration with Pradit",
date: "Sep 29, 2025",
tldr_title: "Executive Summary (TL;DR)",
tldr_text:
"Low-Rank Adaptation (LoRA) is a technique to fine-tune large models efficiently. Our research confirms that while LoRA saves 90% of memory, it can match full fine-tuning performance only if the rank (r) is sufficiently high and alpha scaling is tuned correctly. Ideal for post-training but not pre-training.",
tabs: { read: "Read Article", listen: "Listen (TTS)" },
intro_1:
"Todays leading language models contain upwards of a trillion parameters, pretrained on tens of trillions of tokens. Base model performance keeps improving with scale, as these trillions are necessary for learning and representing all the patterns in written-down human knowledge.",
intro_2:
"In contrast, post-training involves smaller datasets and generally focuses on narrower domains of knowledge and ranges of behavior. It seems wasteful to use a terabit of weights to represent updates from a gigabit or megabit of training data. This intuition has motivated parameter efficient fine-tuning (PEFT), which adjusts a large network by updating a much smaller set of parameters.",
method_title: "The Method",
method_1:
"The leading PEFT method is low-rank adaptation, or LoRA. LoRA replaces each weight matrix W from the original model with a modified version W' = W + γBA, where B and A are matrices that together have far fewer parameters than W, and γ is a constant scaling factor.",
method_2:
"In effect, LoRA creates a low-dimensional representation of the updates imparted by fine-tuning. LoRA may offer advantages in the cost and speed of post-training, and there are also a few operational reasons to prefer it to full fine-tuning.",
results_title: "Operational Advantages",
results_list: [
{
title: "Multi-tenant serving",
text: "Since LoRA trains an adapter (i.e., the A and B matrices) while keeping the original weights unchanged, a single inference server can keep many adapters in memory.",
},
{
title: "Layout size for training",
text: "FullFT usually requires an order of magnitude more accelerators than sampling from the same model does.",
},
{
title: "Ease of loading",
text: "With fewer weights to store, LoRA adapters are fast and easy to set up or transfer between machines.",
},
],
conclusion_title: "Conclusion",
conclusion_text:
"The question remains: can LoRA match the performance of full fine-tuning? Our internal benchmarks suggest yes, provided rank scaling is carefully managed.",
nav: {
intro: "Introduction",
methods: "Methods",
results: "Results",
discussion: "Conclusion",
},
now_playing: "Now Playing",
paused: "Paused",
transcript_title: "Transcript",
},
th: {
title: "LoRA โดยไม่เสียใจ",
subtitle: "John Schulman ร่วมกับ Pradit (ประดิษฐ์)",
date: "29 กันยายน 2025",
tldr_title: "บทสรุปผู้บริหาร (TL;DR)",
tldr_text:
"Low-Rank Adaptation (LoRA) เป็นเทคนิคการปรับแต่งโมเดลขนาดใหญ่ที่มีประสิทธิภาพ งานวิจัยของเรายืนยันว่าแม้ LoRA จะประหยัดหน่วยความจำได้ถึง 90% แต่จะให้ผลลัพธ์เทียบเท่าการ Fine-tune แบบเต็มรูปแบบได้ก็ต่อเมื่อค่า Rank (r) สูงเพียงพอและมีการปรับค่า Alpha อย่างถูกต้อง เหมาะสำหรับขั้นตอน Post-training แต่ไม่เหมาะกับ Pre-training",
tabs: { read: "อ่านบทความ", listen: "ฟังเสียงบรรยาย" },
intro_1:
"โมเดลภาษาชั้นนำในปัจจุบันมีพารามิเตอร์มากกว่าล้านล้านตัว ผ่านการฝึกฝนบนข้อมูลมหาศาล ประสิทธิภาพของโมเดลพื้นฐานดีขึ้นเรื่อยๆ ตามขนาด ซึ่งจำเป็นสำหรับการเรียนรู้รูปแบบความรู้ทั้งหมดของมนุษย์",
intro_2:
"ในทางตรงกันข้าม การฝึกฝนหลังเสร็จสิ้น (Post-training) มักใช้ชุดข้อมูลขนาดเล็กและเน้นขอบเขตความรู้ที่แคบกว่า ดูเหมือนจะสิ้นเปลืองที่จะใช้น้ำหนักระดับเทราบิตเพื่อแทนการอัปเดตจากข้อมูลเพียงกิกะบิต แนวคิดนี้จึงนำไปสู่การปรับแต่งแบบประหยัดพารามิเตอร์ (PEFT)",
method_title: "วิธีการ",
method_1:
"วิธี PEFT ชั้นนำคือ Low-rank adaptation หรือ LoRA ซึ่งแทนที่เมทริกซ์น้ำหนัก W แต่ละตัวด้วยเวอร์ชันแก้ไข W' = W + γBA โดยที่ B และ A เป็นเมทริกซ์ที่มีพารามิเตอร์น้อยกว่า W มาก",
method_2:
"โดยสรุป LoRA สร้างตัวแทนมิติที่ต่ำกว่าของการอัปเดตที่เกิดจากการ Fine-tuning LoRA อาจมีข้อได้เปรียบในด้านต้นทุนและความเร็ว และยังมีเหตุผลด้านการปฏิบัติงานที่ทำให้เป็นที่นิยมมากกว่า Full Fine-tuning",
results_title: "ข้อได้เปรียบด้านการปฏิบัติงาน",
results_list: [
{
title: "การให้บริการแบบ Multi-tenant",
text: "เนื่องจาก LoRA ฝึกฝน Adapter (เมทริกซ์ A และ B) โดยไม่เปลี่ยนน้ำหนักเดิม เซิร์ฟเวอร์เดียวจึงสามารถเก็บ Adapter หลายตัวในหน่วยความจำได้",
},
{
title: "ขนาด Layout สำหรับการฝึก",
text: "FullFT มักต้องการตัวเร่งความเร็ว (GPU/TPU) มากกว่าการสุ่มตัวอย่างจากโมเดลเดียวกันถึงหนึ่งระดับ (Order of magnitude)",
},
{
title: "ความง่ายในการโหลด",
text: "ด้วยน้ำหนักที่ต้องเก็บน้อยกว่า LoRA adapters จึงตั้งค่าและโอนย้ายระหว่างเครื่องได้รวดเร็ว",
},
],
conclusion_title: "บทสรุป",
conclusion_text:
"คำถามยังคงอยู่: LoRA สามารถทำงานได้เทียบเท่ากับ Full Fine-tuning หรือไม่? การทดสอบภายในของเราชี้ว่า 'ได้' หากมีการจัดการ Rank scaling อย่างระมัดระวัง",
nav: {
intro: "บทนำ",
methods: "วิธีการ",
results: "ผลลัพธ์",
discussion: "บทสรุป",
},
now_playing: "กำลังเล่น",
paused: "หยุดชั่วคราว",
transcript_title: "คำบรรยาย",
},
};
const t = content[language];
return (
<div className="bg-paper min-h-screen pb-24 relative">
{/* Article Header */}
<header className="pt-24 pb-8 max-w-4xl mx-auto px-6 text-center">
<h1 className="font-serif text-4xl md:text-5xl text-ink leading-tight mb-6">
{t.title}
</h1>
<div className="font-sans text-sm text-subtle uppercase tracking-widest mb-2">
{t.subtitle}
</div>
<time className="font-serif italic text-subtle">{t.date}</time>
{/* View Tabs */}
<div className="flex justify-center mt-12 mb-8">
<div className="inline-flex bg-gray-100 p-1 rounded-lg border border-gray-200">
<button
onClick={() => setActiveTab("read")}
className={`flex items-center px-6 py-2 rounded-md text-sm font-bold transition-all ${activeTab === "read" ? "bg-white text-ink shadow-sm" : "text-subtle hover:text-ink"}`}
>
<FileText size={16} className="mr-2" />
{t.tabs.read}
</button>
<button
onClick={() => setActiveTab("listen")}
className={`flex items-center px-6 py-2 rounded-md text-sm font-bold transition-all ${activeTab === "listen" ? "bg-white text-ink shadow-sm" : "text-subtle hover:text-ink"}`}
>
<Headphones size={16} className="mr-2" />
{t.tabs.listen}
</button>
</div>
</div>
</header>
{/* TL;DR Section */}
<div className="max-w-4xl mx-auto px-6 mb-12">
<div className="bg-blue-50 border border-blue-100 p-6 md:p-8 rounded-sm relative overflow-hidden">
<div className="absolute top-0 left-0 w-1 h-full bg-accent"></div>
<div className="flex items-start gap-4">
<div className="bg-white p-2 rounded-full shadow-sm text-accent shrink-0 mt-1">
<Zap size={20} />
</div>
<div>
<h3 className="font-sans font-bold text-xs uppercase tracking-widest text-accent mb-2">
{t.tldr_title}
</h3>
<p className="font-serif text-ink leading-relaxed text-base md:text-lg">
{t.tldr_text}
</p>
</div>
</div>
</div>
</div>
{activeTab === "listen" ? (
/* Voice Interface */
<div className="max-w-5xl mx-auto px-4 sm:px-6 animate-in fade-in zoom-in duration-300">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Left: Sticky Player */}
<div className="md:sticky md:top-32 h-fit">
<div className="bg-[#1e1e1e] rounded-xl p-8 text-center shadow-2xl border border-gray-800">
<div className="w-20 h-20 bg-gray-800 rounded-full mx-auto mb-6 flex items-center justify-center shadow-inner relative">
{isPlaying && (
<span className="absolute w-full h-full rounded-full border-2 border-green-500 animate-ping opacity-20"></span>
)}
<Headphones size={28} className="text-gray-400" />
</div>
<h3 className="text-white font-serif text-xl mb-2">
{t.title}
</h3>
<p className="text-gray-400 font-mono text-[10px] mb-8">
Voice: Gemini 2.5 Flash TTS
</p>
{/* Fake Waveform */}
<div className="h-12 flex items-center justify-center gap-1 mb-8 px-4 overflow-hidden">
{[...Array(30)].map((_, i) => (
<div
key={i}
className={`w-1 bg-green-500 transition-all duration-150 rounded-full ${isPlaying ? "animate-pulse" : ""}`}
style={{
height: isPlaying ? `${Math.random() * 100}%` : "10%",
opacity: isPlaying ? 1 : 0.3,
}}
/>
))}
</div>
<div className="flex items-center justify-center gap-6">
<button className="text-gray-400 hover:text-white transition-colors font-mono text-xs">
1.0x
</button>
<button
onClick={handleMainPlayPause}
className="w-14 h-14 bg-white rounded-full flex items-center justify-center hover:scale-105 transition-transform text-black shadow-lg"
>
{isPlaying ? (
<Pause fill="black" size={20} />
) : (
<Play fill="black" size={20} className="ml-1" />
)}
</button>
<button className="text-gray-400 hover:text-white transition-colors font-mono text-xs">
-15s
</button>
</div>
</div>
</div>
{/* Right: Scrollable Transcript */}
<div className="flex flex-col h-[600px]">
<div className="font-sans text-xs font-bold uppercase tracking-widest text-subtle mb-4 flex items-center gap-2">
<FileText size={14} /> {t.transcript_title}
</div>
<div className="flex-grow overflow-y-auto pr-4 space-y-6 text-subtle font-serif leading-relaxed border-l-2 border-gray-100 pl-6 custom-scrollbar">
<p className="text-ink font-medium">{t.intro_1}</p>
<p>{t.intro_2}</p>
<div className="h-px bg-gray-100 w-full my-4"></div>
<p className="text-xs font-sans font-bold text-gray-400 uppercase">
{t.method_title}
</p>
<p>{t.method_1}</p>
<p>{t.method_2}</p>
<div className="h-px bg-gray-100 w-full my-4"></div>
<p className="text-xs font-sans font-bold text-gray-400 uppercase">
{t.results_title}
</p>
{t.results_list.map((item, idx) => (
<p key={idx}>
<strong className="text-gray-700">{item.title}:</strong>{" "}
{item.text}
</p>
))}
<div className="h-px bg-gray-100 w-full my-4"></div>
<p>{t.conclusion_text}</p>
</div>
</div>
</div>
</div>
) : (
/* Read Interface */
<main className="max-w-[1400px] mx-auto px-4 sm:px-6 lg:px-8 grid grid-cols-1 lg:grid-cols-12 gap-12 pb-20">
{/* Left Sidebar - TOC */}
<aside className="hidden lg:block lg:col-span-3 relative">
<nav className="sticky top-32 space-y-4">
<ul className="font-serif text-sm text-subtle space-y-3 border-l border-gray-200 pl-4">
<li>
<a
href="#intro"
className={`block transition-colors ${activeSection === "intro" ? "text-ink font-bold" : "hover:text-ink"}`}
>
{t.nav.intro}
</a>
</li>
<li>
<a
href="#methods"
className={`block transition-colors ${activeSection === "methods" ? "text-ink font-bold" : "hover:text-ink"}`}
>
{t.nav.methods}
</a>
</li>
<li>
<a
href="#results"
className={`block transition-colors ${activeSection === "results" ? "text-ink font-bold" : "hover:text-ink"}`}
>
{t.nav.results}
</a>
</li>
<li>
<a
href="#discussion"
className={`block transition-colors ${activeSection === "discussion" ? "text-ink font-bold" : "hover:text-ink"}`}
>
{t.nav.discussion}
</a>
</li>
</ul>
</nav>
</aside>
{/* Main Content */}
<article className="col-span-1 lg:col-span-6 font-serif text-lg leading-relaxed text-gray-800">
<MatrixViz />
<section id="intro" className="mb-12">
<p className="mb-6 first-letter:float-left first-letter:text-7xl first-letter:pr-4 first-letter:font-bold first-letter:text-ink">
{t.intro_1}
</p>
<p className="mb-6">{t.intro_2}</p>
</section>
<section id="methods" className="mb-12">
<h2 className="font-sans font-bold text-xl text-ink mt-12 mb-6">
{t.method_title}
</h2>
<p className="mb-6">{t.method_1}</p>
<p className="mb-6">{t.method_2}</p>
</section>
<section id="results" className="mb-12">
<h3 className="font-sans font-bold text-lg text-ink mt-8 mb-4">
{t.results_title}
</h3>
<ul className="list-disc list-outside ml-6 space-y-4 mb-6 text-base">
{t.results_list.map((item, idx) => (
<li key={idx}>
<strong className="text-ink">{item.title}.</strong>{" "}
{item.text}
</li>
))}
</ul>
</section>
<section id="discussion" className="mb-12">
<h2 className="font-sans font-bold text-xl text-ink mt-12 mb-6">
{t.conclusion_title}
</h2>
<p className="mb-6">{t.conclusion_text}</p>
</section>
</article>
{/* Right Sidebar - Footnotes */}
<aside className="hidden lg:block lg:col-span-3 relative">
<div className="sticky top-32 space-y-12">
<div className="text-xs text-subtle font-serif leading-relaxed border-l-2 border-gray-100 pl-3">
<sup className="font-bold mr-1">1</sup>
Punica: Multi-Tenant LoRA Serving (Chen, Ye, et al, 2023)
</div>
<div className="text-xs text-subtle font-serif leading-relaxed border-l-2 border-gray-100 pl-3">
<sup className="font-bold mr-1">2</sup>
LoRA: Low-Rank Adaptation of Large Language Models (Hu et al,
2021)
</div>
</div>
</aside>
</main>
)}
{/* Mini Player Overlay - Persistent if showMiniPlayer is true */}
{showMiniPlayer && activeTab === "read" && (
<div className="fixed bottom-6 left-1/2 transform -translate-x-1/2 z-40 animate-in slide-in-from-bottom-4 fade-in">
<div className="bg-ink text-white pl-4 pr-4 py-3 rounded-full shadow-2xl flex items-center gap-4 border border-gray-700">
<div className="flex items-center gap-3 border-r border-gray-700 pr-4">
<div className="relative flex h-3 w-3">
{isPlaying && (
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
)}
<span
className={`relative inline-flex rounded-full h-3 w-3 ${isPlaying ? "bg-green-500" : "bg-yellow-500"}`}
></span>
</div>
<div className="flex flex-col">
<span className="text-[10px] font-sans font-bold uppercase tracking-wider text-gray-400">
{isPlaying ? t.now_playing : t.paused}
</span>
<span className="text-xs font-bold font-serif truncate max-w-[150px]">
{t.title}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setIsPlaying(!isPlaying)}
className="hover:text-green-400 transition-colors p-1"
title={isPlaying ? "Pause" : "Play"}
>
{isPlaying ? (
<Pause size={20} fill="currentColor" />
) : (
<Play size={20} fill="currentColor" />
)}
</button>
<button
onClick={() => setActiveTab("listen")}
className="text-xs font-mono underline hover:text-gray-300 px-2"
>
{language === "en" ? "Transcript" : "คำบรรยาย"}
</button>
<div className="w-px h-4 bg-gray-700 mx-1"></div>
<button
onClick={() => {
setIsPlaying(false);
setShowMiniPlayer(false);
}}
className="text-gray-400 hover:text-red-400 transition-colors p-1"
title="Close Player"
>
<X size={18} />
</button>
</div>
</div>
</div>
)}
</div>
);
};

104
pages/HireUs.tsx Normal file
View File

@ -0,0 +1,104 @@
import React from 'react';
import { TerminalBlock } from '../components/TerminalBlock';
import { Mail, MessageSquare, Code } from 'lucide-react';
export const HireUs: React.FC = () => {
return (
<div className="bg-paper min-h-screen pt-24 pb-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 grid grid-cols-1 lg:grid-cols-2 gap-16">
{/* Left Column: Context */}
<div>
<h1 className="font-serif text-4xl md:text-5xl text-ink leading-tight mb-8">
Work With Us
</h1>
<p className="font-serif text-lg text-subtle leading-relaxed mb-12">
We are currently accepting new engagements for Q4 2025. We look for partners who have clear technical hurdles in their AI roadmap, rather than open-ended exploration.
</p>
<div className="space-y-12">
<div>
<h3 className="font-sans font-bold text-sm uppercase tracking-widest text-ink mb-4">Engagement Models</h3>
<ul className="space-y-6 border-l-2 border-gray-200 pl-6">
<li>
<h4 className="font-serif font-bold text-lg mb-2">Strategic Sprint (4-6 Weeks)</h4>
<p className="text-subtle font-serif text-sm">Best for MVPs, Feasibility Studies, and Architecture Audits. We ship a working artifact and a technical roadmap.</p>
</li>
<li>
<h4 className="font-serif font-bold text-lg mb-2">Quarterly Retainer</h4>
<p className="text-subtle font-serif text-sm">For teams needing ongoing specialized ML expertise to augment their core engineering staff. Includes weekly syncs and code review.</p>
</li>
</ul>
</div>
<div>
<h3 className="font-sans font-bold text-sm uppercase tracking-widest text-ink mb-4">What we need from you</h3>
<div className="prose prose-sm text-subtle font-serif">
<p>
To ensure we are a good fit, please provide technical details in your inquiry.
Mention your current stack (e.g., AWS, Python, Vercel), specific model constraints, and the primary metric you are trying to improve (latency, accuracy, cost).
</p>
</div>
</div>
</div>
</div>
{/* Right Column: Form */}
<div className="bg-white p-8 md:p-12 border border-gray-200 shadow-sm">
<form className="space-y-6" onSubmit={(e) => e.preventDefault()}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input type="text" id="name" className="w-full border-gray-300 border p-3 text-sm focus:ring-ink focus:border-ink outline-none bg-gray-50" placeholder="Jane Doe" />
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">Work Email</label>
<input type="email" id="email" className="w-full border-gray-300 border p-3 text-sm focus:ring-ink focus:border-ink outline-none bg-gray-50" placeholder="jane@company.com" />
</div>
</div>
<div>
<label htmlFor="company" className="block text-sm font-medium text-gray-700 mb-1">Company / Organization</label>
<input type="text" id="company" className="w-full border-gray-300 border p-3 text-sm focus:ring-ink focus:border-ink outline-none bg-gray-50" placeholder="Acme Corp" />
</div>
<div>
<label htmlFor="type" className="block text-sm font-medium text-gray-700 mb-1">Project Type</label>
<select id="type" className="w-full border-gray-300 border p-3 text-sm focus:ring-ink focus:border-ink outline-none bg-gray-50">
<option>MVP Development</option>
<option>System Audit</option>
<option>Model Fine-Tuning</option>
<option>Other / Consulting</option>
</select>
</div>
<div>
<label htmlFor="details" className="block text-sm font-medium text-gray-700 mb-1">Technical Context</label>
<textarea
id="details"
rows={6}
className="w-full border-gray-300 border p-3 text-sm focus:ring-ink focus:border-ink outline-none bg-gray-50 font-mono"
placeholder="Describe your stack, the models you are experimenting with, and the specific bottlenecks you are facing..."
></textarea>
</div>
<button type="submit" className="w-full bg-ink text-white font-bold py-4 hover:bg-gray-800 transition-colors uppercase tracking-widest text-xs">
Request Consultation
</button>
</form>
<div className="mt-8 pt-8 border-t border-gray-100 flex items-center justify-center space-x-8 text-subtle">
<a href="#" className="flex items-center text-xs uppercase tracking-wider hover:text-ink">
<Mail size={16} className="mr-2" /> hello@pradit.app
</a>
<a href="#" className="flex items-center text-xs uppercase tracking-wider hover:text-ink">
<Code size={16} className="mr-2" /> GitHub
</a>
</div>
</div>
</div>
</div>
);
};

699
pages/Home.tsx Normal file
View File

@ -0,0 +1,699 @@
import React, { useState } from "react";
import { TerminalBlock } from "../components/TerminalBlock";
import {
ArrowRight,
Activity,
Search,
Database,
ChevronRight,
GitCommit,
BarChart,
Server,
Shield,
Box,
Cloud,
Layers,
Cpu,
Lock,
X,
Zap,
Globe,
} from "lucide-react";
import { Link } from "react-router-dom";
import { useLanguage } from "../App";
// Type definition for Projects
interface ProjectData {
id: string;
category: string;
title: string;
shortDesc: string;
fullDesc: string;
metrics: { label: string; value: string }[];
stack: string[];
architecture: string;
challenge: string;
}
export const Home: React.FC = () => {
const { language } = useLanguage();
const [selectedProject, setSelectedProject] = useState<ProjectData | null>(
null,
);
const content = {
en: {
new_research: "New Research: LoRA Without Regret",
hero_title: "Engineering Intelligence.",
hero_subtitle:
"We bridge the gap between research papers and production environments. Specialized consulting for LLM infrastructure, evaluation pipelines, and custom model adaptation.",
btn_services: "View Services",
btn_research: "Read Research",
latest_thinking: "Latest Thinking",
view_archive: "View Archive",
lora_desc:
"Fine-tuning large models is expensive. Low-rank adaptation (LoRA) promises efficiency, but does it sacrifice performance? Our deep dive into rank scaling and matrix initialization.",
technical_deep_dive: "Technical Deep Dive",
expertise_title: "Our Expertise",
process_title: "The Pipeline",
process_desc:
"We don't just write code; we engineer systems. Our engagement process is designed for transparency and velocity.",
projects_title: "Selected Deployments",
projects_desc:
"We don't sell hours. We sell shipped, production-grade systems. Click on a project to view the architecture.",
trusted_stack: "Built with Production Standards",
modal_stack: "Tech Stack",
modal_impact: "Business Impact",
modal_arch: "Architecture Highlight",
modal_challenge: "The Challenge",
close: "Close",
services: {
mvp: {
title: "MVP & Prototyping",
desc: "From concept to deployed model in weeks. We build robust POCs using the latest foundation models (Gemini, Claude, OpenAI) to validate business value before heavy investment.",
},
audit: {
title: "System Audit",
desc: "Why is your RAG pipeline hallucinating? We perform deep architectural audits, evaluating retrieval quality, prompt efficacy, and security vulnerabilities.",
},
integration: {
title: "Custom Integration",
desc: "Fine-tuning and integrating open-weights models into your secure infrastructure. We handle the MLOps so you own your weights and your data.",
},
},
learn_more: "Learn more",
process_steps: [
{
step: "01",
title: "Discovery & Audit",
desc: "We define the metric that matters. Is it latency? Accuracy? Cost?",
},
{
step: "02",
title: "Prototyping",
desc: "Rapid iteration using state-of-the-art models to validate feasibility.",
},
{
step: "03",
title: "Evaluation (Evals)",
desc: "We build a custom test suite (Pass@k, RAGAS) before we ship.",
},
{
step: "04",
title: "Production",
desc: "Containerization, CI/CD pipelines, and observability setup.",
},
],
stack: {
infra: "Infrastructure",
orchestration: "Orchestration",
inference: "Inference & Serving",
data: "Data & Vector Ops",
},
projects: [
{
id: "compliance",
category: "FinTech / Legal",
title: "ComplianceGuard AI",
shortDesc:
"Automated regulatory compliance checking for a Tier-1 bank. Reduced manual review time by 85% using a secure RAG pipeline.",
fullDesc:
"We architected a secure, on-premise Retrieval Augmented Generation (RAG) system that ingests thousands of changing regulatory documents (PDFs) and cross-references them with loan applications in real-time.",
challenge:
"The client was spending 2000+ man-hours monthly manually checking loan applications against constantly updating Bank of Thailand regulations. Privacy requirements prevented using public APIs.",
architecture:
'Hybrid-cloud setup. Sensitive data stays on-prem (OpenShift) using a fine-tuned Llama-3-70B for inference. Regulatory documents are indexed in Weaviate. LangChain orchestrates the "Reasoning" step to cite specific regulatory clauses.',
metrics: [
{ label: "Review Time", value: "-85%" },
{ label: "False Positives", value: "< 2%" },
{ label: "Cost Saving", value: "$45k/mo" },
],
stack: [
"Llama 3 70B",
"Weaviate",
"OpenShift",
"LangChain",
"Python",
],
},
{
id: "retail",
category: "E-Commerce",
title: "OmniFlow Agentic Router",
shortDesc:
"High-throughput customer support agent handling 50k+ daily tickets. Deflects 92% of queries with sub-2s latency.",
fullDesc:
"A sophisticated multi-agent system that acts as the first line of defense for a major e-commerce platform. It uses tool-calling to actually perform actions (check status, process refund) rather than just chatting.",
challenge:
'During "11.11" sales events, support volume spikes 10x. Traditional chatbots were too rigid, and human agents were overwhelmed. The system needed to be autonomous but safe.',
architecture:
'We built a Router Agent using GPT-4o-mini for speed. It classifies intent and routes to specialized Sub-Agents (Refund Agent, Logistics Agent). We implemented a "Human-in-the-loop" handover protocol for low-confidence scores.',
metrics: [
{ label: "Deflection Rate", value: "92%" },
{ label: "Avg Response", value: "1.2s" },
{ label: "CSAT Score", value: "4.8/5" },
],
stack: ["GPT-4o", "Redis", "FastAPI", "Kubernetes", "Postgres"],
},
],
},
th: {
new_research: "งานวิจัยใหม่: LoRA โดยไม่เสียใจ",
hero_title: "วิศวกรรมแห่งปัญญาประดิษฐ์",
hero_subtitle:
"เราเชื่อมช่องว่างระหว่างงานวิจัยทางวิชาการและการใช้งานจริง เชี่ยวชาญด้านโครงสร้างพื้นฐาน LLM, การประเมินผล และการปรับแต่งโมเดลตามความต้องการ",
btn_services: "ดูบริการของเรา",
btn_research: "อ่านงานวิจัย",
latest_thinking: "แนวคิดล่าสุด",
view_archive: "ดูบทความทั้งหมด",
lora_desc:
"การ Fine-tune โมเดลขนาดใหญ่มีค่าใช้จ่ายสูง LoRA สัญญาว่าจะเพิ่มประสิทธิภาพ แต่จะลดคุณภาพหรือไม่? เจาะลึกเรื่อง Rank scaling และการเริ่มต้น Matrix",
technical_deep_dive: "เจาะลึกทางเทคนิค",
expertise_title: "ความเชี่ยวชาญของเรา",
process_title: "กระบวนการทำงาน",
process_desc:
"เราไม่ได้แค่เขียนโค้ด แต่เราออกแบบระบบ กระบวนการของเราเน้นความโปร่งใสและความรวดเร็ว",
projects_title: "ผลงานที่ผ่านมา",
projects_desc:
"เราไม่ได้ขายชั่วโมงการทำงาน แต่เราส่งมอบระบบที่ใช้งานได้จริง คลิกที่โปรเจกต์เพื่อดูสถาปัตยกรรม",
trusted_stack: "สร้างด้วยมาตรฐานระดับ Production",
modal_stack: "Tech Stack",
modal_impact: "ผลลัพธ์ทางธุรกิจ",
modal_arch: "จุดเด่นของสถาปัตยกรรม",
modal_challenge: "ความท้าทาย",
close: "ปิด",
services: {
mvp: {
title: "MVP และต้นแบบ",
desc: "จากแนวคิดสู่โมเดลที่ใช้งานได้จริงในไม่กี่สัปดาห์ เราสร้าง POC ที่แข็งแกร่งด้วยโมเดลล่าสุด (Gemini, Claude, OpenAI) เพื่อตรวจสอบความคุ้มค่าก่อนการลงทุนจริง",
},
audit: {
title: "ตรวจสอบระบบ",
desc: "ทำไมระบบ RAG ของคุณถึงให้ข้อมูลผิด? เราตรวจสอบสถาปัตยกรรมอย่างละเอียด ประเมินคุณภาพการสืบค้น และช่องโหว่ด้านความปลอดภัย",
},
integration: {
title: "การรวมระบบตามสั่ง",
desc: "ปรับแต่ง (Fine-tune) และรวมโมเดล Open-weights เข้ากับโครงสร้างพื้นฐานที่ปลอดภัยของคุณ เราดูแล MLOps เพื่อให้คุณเป็นเจ้าของข้อมูลและโมเดลอย่างแท้จริง",
},
},
learn_more: "เรียนรู้เพิ่มเติม",
process_steps: [
{
step: "01",
title: "ค้นหา & ตรวจสอบ",
desc: "เรากำหนดตัวชี้วัดที่สำคัญที่สุด ไม่ว่าจะเป็น ความหน่วง ความแม่นยำ หรือต้นทุน",
},
{
step: "02",
title: "สร้างต้นแบบ",
desc: "ทำซ้ำอย่างรวดเร็วโดยใช้โมเดลล่าสุดเพื่อตรวจสอบความเป็นไปได้",
},
{
step: "03",
title: "การประเมินผล (Evals)",
desc: "เราสร้างชุดทดสอบเฉพาะ (Pass@k, RAGAS) ก่อนส่งมอบงาน",
},
{
step: "04",
title: "Production",
desc: "การทำ Containerization, ระบบ CI/CD และการติดตั้งระบบสังเกตการณ์",
},
],
stack: {
infra: "Infrastructure",
orchestration: "Orchestration",
inference: "Inference & Serving",
data: "Data & Vector Ops",
},
projects: [
{
id: "compliance",
category: "FinTech / Legal",
title: "ComplianceGuard AI",
shortDesc:
"ระบบตรวจสอบการปฏิบัติตามกฎระเบียบอัตโนมัติสำหรับธนาคารชั้นนำ ลดเวลาการตรวจสอบด้วยคนลง 85% โดยใช้ RAG pipeline ที่ปลอดภัย",
fullDesc:
"เราออกแบบระบบ Retrieval Augmented Generation (RAG) ภายในองค์กรที่ปลอดภัย ซึ่งนำเข้าเอกสารกฎระเบียบหลายพันฉบับที่เปลี่ยนแปลงตลอดเวลา และตรวจสอบเทียบกับคำขอสินเชื่อแบบเรียลไทม์",
challenge:
"ลูกค้าใช้เวลาคนทำงานกว่า 2,000 ชั่วโมงต่อเดือนในการตรวจสอบคำขอสินเชื่อด้วยตนเองเทียบกับกฎระเบียบธนาคารแห่งประเทศไทย ข้อกำหนดด้านความเป็นส่วนตัวไม่อนุญาตให้ใช้ Public API",
architecture:
'ระบบ Hybrid-cloud ข้อมูลสำคัญอยู่ภายในองค์กร (OpenShift) โดยใช้ Llama-3-70B ที่ปรับแต่งแล้ว เอกสารกฎระเบียบถูกจัดทำดัชนีใน Weaviate LangChain ควบคุมขั้นตอน "การให้เหตุผล" เพื่ออ้างอิงข้อกฎหมายเฉพาะ',
metrics: [
{ label: "เวลาตรวจสอบ", value: "-85%" },
{ label: "False Positives", value: "< 2%" },
{ label: "ประหยัดต้นทุน", value: "$45k/เดือน" },
],
stack: [
"Llama 3 70B",
"Weaviate",
"OpenShift",
"LangChain",
"Python",
],
},
{
id: "retail",
category: "E-Commerce",
title: "OmniFlow Agentic Router",
shortDesc:
"ระบบตอบรับลูกค้าความเร็วสูง รองรับ 50,000+ ตั๋วต่อวัน แก้ไขปัญหาอัตโนมัติได้ 92% ด้วยความเร็วต่ำกว่า 2 วินาที",
fullDesc:
"ระบบ Multi-agent อัจฉริยะที่ทำหน้าที่เป็นด่านหน้าสำหรับแพลตฟอร์มอีคอมเมิร์ซขนาดใหญ่ ใช้การเรียกใช้เครื่องมือ (Tool-calling) เพื่อดำเนินการจริง (ตรวจสอบสถานะ, คืนเงิน) แทนที่จะเป็นแค่การแชท",
challenge:
'ในช่วงเทศกาล "11.11" ปริมาณการติดต่อเพิ่มขึ้น 10 เท่า Chatbot แบบเดิมแข็งทื่อเกินไป และพนักงานรับมือไม่ไหว ระบบต้องทำงานอัตโนมัติแต่ปลอดภัย',
architecture:
'เราสร้าง Router Agent โดยใช้ GPT-4o-mini เพื่อความเร็ว โดยจำแนกเจตนาและส่งต่อไปยัง Agent เฉพาะทาง (Refund Agent, Logistics Agent) เราใช้โปรโตคอล "Human-in-the-loop" สำหรับเคสที่มีความมั่นใจต่ำ',
metrics: [
{ label: "อัตราการตอบกลับ", value: "92%" },
{ label: "ความเร็วเฉลี่ย", value: "1.2s" },
{ label: "คะแนน CSAT", value: "4.8/5" },
],
stack: ["GPT-4o", "Redis", "FastAPI", "Kubernetes", "Postgres"],
},
],
},
};
const t = content[language];
return (
<div className="flex flex-col">
{/* Hero Section */}
<section className="pt-24 pb-24 bg-paper border-b border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
<div>
<Link
to="/research/lora-without-regret"
className="inline-flex items-center space-x-2 mb-8 bg-blue-50 border border-blue-100 rounded-full px-4 py-1.5 text-xs font-bold uppercase tracking-widest text-accent hover:bg-blue-100 transition-colors"
>
<span className="w-2 h-2 rounded-full bg-accent animate-pulse"></span>
<span>{t.new_research}</span>
</Link>
<h1 className="font-serif text-5xl md:text-6xl text-ink leading-tight mb-8">
{t.hero_title}
</h1>
<p className="font-serif text-lg text-subtle mb-10 max-w-lg leading-relaxed">
{t.hero_subtitle}
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Link
to="/services"
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium text-white bg-ink hover:bg-gray-800 transition-all shadow-sm rounded-sm"
>
{t.btn_services}
</Link>
<Link
to="/research"
className="inline-flex items-center justify-center px-6 py-3 border border-gray-200 text-base font-medium text-ink bg-white hover:bg-gray-50 transition-all rounded-sm"
>
{t.btn_research}
</Link>
</div>
</div>
{/* Technical Trust Signal with Typing Effect */}
<div className="lg:pl-12">
<TerminalBlock
title="audit_pipeline.py"
typingEffect={true}
lines={[
"$ python run_evals.py --model=gemini-2.5-pro",
"# Loading dataset: humaneval_v2...",
"# Initializing sandbox environment...",
"> Running 164 test cases...",
"> Pass@1: 92.4% [OK]",
"> Latency (p99): 450ms [WARN]",
"# Optimization suggestion:",
"# Switch to batch inference or semantic caching.",
"$ _",
]}
className="transform lg:rotate-1 hover:rotate-0 transition-transform duration-500"
/>
</div>
</div>
</div>
</section>
{/* Trusted Stack / Logos (Revamped) */}
<section className="py-20 bg-gray-50 border-b border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{/* Infrastructure */}
<div className="flex flex-col items-center text-center">
<div className="mb-4 p-3 bg-white rounded-full shadow-sm border border-gray-100">
<Cloud className="text-subtle" size={24} />
</div>
<h4 className="font-sans text-xs font-bold uppercase tracking-widest text-ink mb-2">
{t.stack.infra}
</h4>
<p className="font-mono text-xs text-subtle">
AWS, GCP, Azure, Terraform
</p>
</div>
{/* Orchestration */}
<div className="flex flex-col items-center text-center">
<div className="mb-4 p-3 bg-white rounded-full shadow-sm border border-gray-100">
<Box className="text-subtle" size={24} />
</div>
<h4 className="font-sans text-xs font-bold uppercase tracking-widest text-ink mb-2">
{t.stack.orchestration}
</h4>
<p className="font-mono text-xs text-subtle">
Kubernetes, Docker, Ray, Airflow
</p>
</div>
{/* Inference */}
<div className="flex flex-col items-center text-center">
<div className="mb-4 p-3 bg-white rounded-full shadow-sm border border-gray-100">
<Cpu className="text-subtle" size={24} />
</div>
<h4 className="font-sans text-xs font-bold uppercase tracking-widest text-ink mb-2">
{t.stack.inference}
</h4>
<p className="font-mono text-xs text-subtle">
vLLM, TorchServe, ONNX Runtime
</p>
</div>
{/* Data */}
<div className="flex flex-col items-center text-center">
<div className="mb-4 p-3 bg-white rounded-full shadow-sm border border-gray-100">
<Layers className="text-subtle" size={24} />
</div>
<h4 className="font-sans text-xs font-bold uppercase tracking-widest text-ink mb-2">
{t.stack.data}
</h4>
<p className="font-mono text-xs text-subtle">
Pinecone, Weaviate, Postgres, Redis
</p>
</div>
</div>
</div>
</section>
{/* Featured Research Snippet */}
<section className="py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-baseline justify-between mb-12">
<h2 className="font-sans font-bold text-xs uppercase tracking-widest text-subtle">
{t.latest_thinking}
</h2>
<Link
to="/research"
className="group flex items-center text-sm font-serif text-ink hover:text-accent"
>
{t.view_archive}{" "}
<ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform" />
</Link>
</div>
<Link to="/research/lora-without-regret" className="block group">
<article className="grid grid-cols-1 md:grid-cols-2 gap-12 border-t border-gray-100 pt-12">
<div>
<h3 className="font-serif text-3xl text-ink mb-4 group-hover:text-gray-600 transition-colors">
LoRA Without Regret
</h3>
<p className="font-serif text-subtle text-lg leading-relaxed mb-6">
{t.lora_desc}
</p>
<span className="text-xs font-sans font-bold uppercase text-accent">
{t.technical_deep_dive}
</span>
</div>
<div className="bg-gray-50 p-8 flex items-center justify-center border border-gray-100">
{/* Mini visualization for the teaser */}
<div className="grid grid-cols-12 gap-1 w-full max-w-xs opacity-50 group-hover:opacity-80 transition-opacity">
{[...Array(48)].map((_, i) => (
<div
key={i}
className="aspect-square bg-blue-500"
style={{ opacity: Math.random() }}
/>
))}
</div>
</div>
</article>
</Link>
</div>
</section>
{/* Services Preview */}
<section className="py-24 bg-paper border-t border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="font-sans font-bold text-xs uppercase tracking-widest text-subtle mb-16">
{t.expertise_title}
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Service 1 */}
<div className="group p-8 border border-gray-200 bg-white hover:border-gray-400 transition-colors cursor-pointer relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<Activity size={64} />
</div>
<h3 className="font-serif text-xl text-ink mb-4 font-bold">
{t.services.mvp.title}
</h3>
<p className="font-serif text-sm text-subtle leading-relaxed mb-8">
{t.services.mvp.desc}
</p>
<Link
to="/services"
className="inline-flex items-center text-xs font-bold uppercase tracking-wider text-ink group-hover:text-accent"
>
{t.learn_more} <ChevronRight size={16} className="ml-1" />
</Link>
</div>
{/* Service 2 */}
<div className="group p-8 border border-gray-200 bg-white hover:border-gray-400 transition-colors cursor-pointer relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<Search size={64} />
</div>
<h3 className="font-serif text-xl text-ink mb-4 font-bold">
{t.services.audit.title}
</h3>
<p className="font-serif text-sm text-subtle leading-relaxed mb-8">
{t.services.audit.desc}
</p>
<Link
to="/services"
className="inline-flex items-center text-xs font-bold uppercase tracking-wider text-ink group-hover:text-accent"
>
{t.learn_more} <ChevronRight size={16} className="ml-1" />
</Link>
</div>
{/* Service 3 */}
<div className="group p-8 border border-gray-200 bg-white hover:border-gray-400 transition-colors cursor-pointer relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<Database size={64} />
</div>
<h3 className="font-serif text-xl text-ink mb-4 font-bold">
{t.services.integration.title}
</h3>
<p className="font-serif text-sm text-subtle leading-relaxed mb-8">
{t.services.integration.desc}
</p>
<Link
to="/services"
className="inline-flex items-center text-xs font-bold uppercase tracking-wider text-ink group-hover:text-accent"
>
{t.learn_more} <ChevronRight size={16} className="ml-1" />
</Link>
</div>
</div>
</div>
</section>
{/* Process Pipeline Section */}
<section className="py-24 bg-[#1e1e1e] text-white overflow-hidden relative">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div className="mb-16 max-w-2xl">
<h2 className="font-sans font-bold text-xs uppercase tracking-widest text-green-400 mb-4">
{t.process_title}
</h2>
<p className="font-serif text-2xl text-gray-300">
{t.process_desc}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{t.process_steps.map((step, idx) => (
<div key={idx} className="relative group">
<div className="w-12 h-12 rounded-full border border-gray-600 bg-gray-800 flex items-center justify-center font-mono text-sm font-bold mb-6 group-hover:border-green-400 group-hover:text-green-400 transition-colors">
{step.step}
</div>
<h3 className="font-bold text-lg mb-2">{step.title}</h3>
<p className="text-gray-400 font-serif text-sm leading-relaxed">
{step.desc}
</p>
{/* Connecting Line */}
{idx < 3 && (
<div className="hidden md:block absolute top-6 left-14 w-[calc(100%-3rem)] h-px bg-gray-800"></div>
)}
</div>
))}
</div>
</div>
</section>
{/* Featured Projects (Improved Enterprise) */}
<section className="py-24 bg-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="mb-16 text-center max-w-3xl mx-auto">
<h2 className="font-sans font-bold text-xs uppercase tracking-widest text-subtle mb-4">
{t.projects_title}
</h2>
<p className="font-serif text-lg text-subtle">{t.projects_desc}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
{t.projects.map((project) => (
<div
key={project.id}
onClick={() => setSelectedProject(project)}
className="border border-gray-200 rounded-sm hover:shadow-xl transition-all duration-300 p-8 bg-white flex flex-col cursor-pointer group hover:-translate-y-1"
>
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className={`p-2 rounded-md ${project.id === "compliance" ? "bg-blue-50 text-accent" : "bg-purple-50 text-purple-600"}`}
>
{project.id === "compliance" ? (
<Lock size={24} />
) : (
<Zap size={24} />
)}
</div>
<span className="font-mono text-xs text-subtle uppercase tracking-widest">
{project.category}
</span>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity text-subtle">
<ArrowRight size={20} />
</div>
</div>
<h3 className="font-serif font-bold text-2xl text-ink mb-4">
{project.title}
</h3>
<p className="text-subtle font-serif text-base leading-relaxed mb-8 flex-grow">
{project.shortDesc}
</p>
<div className="grid grid-cols-3 gap-4 border-t border-gray-100 pt-6">
{project.metrics.map((metric, idx) => (
<div key={idx}>
<div className="font-mono text-lg font-bold text-ink">
{metric.value}
</div>
<div className="text-[10px] uppercase text-gray-400 font-sans font-bold">
{metric.label}
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</section>
{/* Project Modal */}
{selectedProject && (
<div className="fixed inset-0 z-[60] flex items-center justify-center p-4 md:p-8">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={() => setSelectedProject(null)}
></div>
<div className="bg-white w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-lg shadow-2xl relative z-10 flex flex-col md:flex-row animate-in fade-in zoom-in duration-200">
{/* Modal Left: Overview */}
<div className="p-8 md:p-10 md:w-2/3">
<h2 className="font-serif text-3xl font-bold text-ink mb-2">
{selectedProject.title}
</h2>
<span className="font-mono text-xs uppercase tracking-widest text-accent mb-6 block">
{selectedProject.category}
</span>
<div className="space-y-8">
<div>
<h4 className="font-sans font-bold text-xs uppercase tracking-widest text-subtle mb-2">
{t.modal_challenge}
</h4>
<p className="font-serif text-subtle leading-relaxed">
{selectedProject.challenge}
</p>
</div>
<div>
<h4 className="font-sans font-bold text-xs uppercase tracking-widest text-subtle mb-2">
{t.modal_arch}
</h4>
<p className="font-serif text-ink leading-relaxed">
{selectedProject.architecture}
</p>
</div>
<div>
<h4 className="font-sans font-bold text-xs uppercase tracking-widest text-subtle mb-2">
{t.modal_impact}
</h4>
<div className="grid grid-cols-3 gap-4 bg-gray-50 p-4 rounded border border-gray-100">
{selectedProject.metrics.map((metric, idx) => (
<div key={idx}>
<div className="font-mono text-xl font-bold text-green-600">
{metric.value}
</div>
<div className="text-[10px] uppercase text-gray-400 font-sans font-bold">
{metric.label}
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Modal Right: Stack */}
<div className="bg-[#1e1e1e] p-8 md:p-10 md:w-1/3 text-gray-300 border-l border-gray-800">
<button
onClick={() => setSelectedProject(null)}
className="absolute top-4 right-4 text-gray-400 hover:text-white"
>
<X size={24} />
</button>
<h3 className="font-sans font-bold text-xs uppercase tracking-widest text-gray-500 mb-6">
{t.modal_stack}
</h3>
<div className="space-y-3 font-mono text-sm">
{selectedProject.stack.map((tech, i) => (
<div
key={i}
className="flex items-center gap-3 pb-3 border-b border-gray-800 last:border-0"
>
<div className="w-1.5 h-1.5 rounded-full bg-green-500"></div>
{tech}
</div>
))}
</div>
<div className="mt-12 p-6 bg-gray-800/50 rounded border border-gray-700">
<Globe className="text-gray-500 mb-4" size={24} />
<p className="font-serif text-xs italic text-gray-400 leading-relaxed">
"Pradit delivered this system 2 weeks ahead of schedule. The
documentation was better than our internal standard."
</p>
<div className="mt-2 font-sans text-[10px] font-bold uppercase text-gray-500">
- CTO, Client Partner
</div>
</div>
</div>
</div>
</div>
)}
</div>
);
};

329
pages/Playground.tsx Normal file
View File

@ -0,0 +1,329 @@
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>
);
};

269
pages/Research.tsx Normal file
View File

@ -0,0 +1,269 @@
import React, { useState, useMemo } from "react";
import { Link } from "react-router-dom";
import { ArrowRight, Search as SearchIcon, Filter, X } from "lucide-react";
import { useLanguage } from "../App";
export const Research: React.FC = () => {
const { language } = useLanguage();
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("All");
const content = {
en: {
title: "Research Archive",
subtitle:
"Our collection of white papers, technical deep dives, and engineering retrospectives. We believe in open-sourcing our learnings from the frontier of applied AI.",
read_paper: "Read Paper",
search_placeholder: "grep search-query...",
no_results: "No research found matching criteria.",
categories: [
"All",
"Fine-tuning",
"Systems",
"Architecture",
"Small Models",
],
articles: [
{
id: "lora-without-regret",
title: "LoRA Without Regret",
date: "Sep 29, 2025",
authors: "John Schulman, Pradit",
abstract:
"Fine-tuning large models is expensive. We investigate whether Low-Rank Adaptation (LoRA) can truly match full fine-tuning performance across various downstream tasks, analyzing rank scaling and matrix initialization.",
tags: ["Fine-tuning", "Optimization", "Deep Learning"],
category: "Fine-tuning",
},
{
id: "rag-latency-optimization",
title: "Optimizing RAG Latency: A Systems Approach",
date: "Aug 14, 2025",
authors: "Pradit Engineering",
abstract:
"Retrieval-Augmented Generation often suffers from p99 latency spikes. We break down the contribution of embedding generation, vector search, and reranking, proposing a caching layer that reduces latency by 40%.",
tags: ["RAG", "Systems", "Latency"],
category: "Architecture",
},
{
id: "small-models-reasoning",
title: "The Unreasonable Effectiveness of Small Models",
date: "July 02, 2025",
authors: "Pradit Research",
abstract:
"Can a 7B parameter model reason as well as a 70B model? With specific chain-of-thought fine-tuning on high-quality synthetic data, we demonstrate parity on GSM8K benchmarks.",
tags: ["Small Models", "Reasoning", "Data Efficiency"],
category: "Small Models",
},
],
},
th: {
title: "คลังงานวิจัย",
subtitle:
"คลังเอกสารทางเทคนิคและการถอดบทเรียนทางวิศวกรรม เราเชื่อในการเปิดเผยสิ่งที่เราเรียนรู้จากพรมแดนแห่ง AI ประยุกต์สู่สาธารณะ",
read_paper: "อ่านบทความ",
search_placeholder: "ค้นหาบทความ...",
no_results: "ไม่พบงานวิจัยที่ตรงกับเงื่อนไข",
categories: [
"ทั้งหมด",
"Fine-tuning",
"Systems",
"Architecture",
"Small Models",
],
articles: [
{
id: "lora-without-regret",
title: "LoRA โดยไม่เสียใจ",
date: "29 ก.ย. 2025",
authors: "John Schulman, Pradit (ประดิษฐ์)",
abstract:
"การ Fine-tune โมเดลขนาดใหญ่มีค่าใช้จ่ายสูง เราตรวจสอบว่า Low-Rank Adaptation (LoRA) สามารถให้ประสิทธิภาพเทียบเท่าการ Fine-tune เต็มรูปแบบได้หรือไม่ โดยวิเคราะห์การปรับขนาด Rank และการเริ่มต้น Matrix",
tags: ["Fine-tuning", "Optimization", "Deep Learning"],
category: "Fine-tuning",
},
{
id: "rag-latency-optimization",
title: "การปรับปรุงความหน่วง RAG: แนวทางเชิงระบบ",
date: "14 ส.ค. 2025",
authors: "Pradit Engineering",
abstract:
"ระบบ RAG มักประสบปัญหาค่าความหน่วง p99 สูง เราจำแนกส่วนประกอบของการสร้าง Embedding, Vector Search และ Reranking พร้อมเสนอชั้น Caching ที่ลดความหน่วงได้ถึง 40%",
tags: ["RAG", "Systems", "Latency"],
category: "Architecture",
},
{
id: "small-models-reasoning",
title: "ประสิทธิภาพที่น่าทึ่งของโมเดลขนาดเล็ก",
date: "02 ก.ค. 2025",
authors: "Pradit Research",
abstract:
"โมเดล 7B สามารถให้เหตุผลได้ดีเท่ากับ 70B หรือไม่? ด้วยการ Fine-tune แบบ Chain-of-thought บนข้อมูลสังเคราะห์คุณภาพสูง เราแสดงให้เห็นถึงความสามารถที่เทียบเท่ากันบน GSM8K",
tags: ["Small Models", "Reasoning", "Data Efficiency"],
category: "Small Models",
},
],
},
};
const t = content[language];
// Filtering Logic
const filteredArticles = useMemo(() => {
return t.articles.filter((article) => {
const matchesSearch =
article.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
article.abstract.toLowerCase().includes(searchQuery.toLowerCase());
// Map 'All' or 'ทั้งหมด' to show everything
const isAll =
selectedCategory === "All" || selectedCategory === "ทั้งหมด";
const matchesCategory =
isAll ||
article.category === selectedCategory ||
article.tags.includes(selectedCategory);
return matchesSearch && matchesCategory;
});
}, [searchQuery, selectedCategory, t.articles]);
return (
<div className="bg-paper min-h-screen pt-24 pb-24">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<header className="mb-12 pb-8 border-b border-gray-100">
<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 max-w-3xl">
{t.subtitle}
</p>
</header>
{/* Controls: Search & Filter */}
<div className="flex flex-col md:flex-row gap-6 mb-16 items-start md:items-center justify-between">
{/* Categories */}
<div className="flex flex-wrap gap-2">
{t.categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-1.5 text-xs font-bold uppercase tracking-wider rounded-full transition-all border ${
selectedCategory === cat
? "bg-ink text-white border-ink"
: "bg-white text-subtle border-gray-200 hover:border-gray-400"
}`}
>
{cat}
</button>
))}
</div>
{/* Search Bar */}
<div className="relative w-full md:w-64 group">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon
size={14}
className="text-gray-400 group-focus-within:text-ink"
/>
</div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="block w-full pl-9 pr-3 py-2 border-b-2 border-gray-200 bg-transparent text-sm font-mono text-ink focus:outline-none focus:border-ink transition-colors placeholder:text-gray-400"
placeholder={t.search_placeholder}
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="absolute inset-y-0 right-0 pr-2 flex items-center text-gray-400 hover:text-ink"
>
<X size={14} />
</button>
)}
</div>
</div>
{/* Results List */}
<div className="space-y-16">
{filteredArticles.length > 0 ? (
filteredArticles.map((article) => (
<article
key={article.id}
className="group relative grid grid-cols-1 md:grid-cols-12 gap-8 p-6 -mx-6 rounded-lg hover:bg-gray-50 transition-colors border border-transparent hover:border-gray-100"
>
{/* Metadata Column */}
<div className="md:col-span-3 flex flex-col gap-2">
<span className="font-mono text-xs font-bold text-subtle">
{article.date}
</span>
<span className="font-sans text-xs font-medium text-gray-400 uppercase tracking-wider">
{article.category}
</span>
<div className="flex flex-wrap gap-2 mt-2">
{article.tags.slice(0, 2).map((tag) => (
<span
key={tag}
className="inline-flex px-2 py-0.5 bg-gray-200/50 text-[10px] text-gray-600 rounded-sm font-mono"
>
#{tag}
</span>
))}
</div>
</div>
{/* Content Column */}
<div className="md:col-span-9">
<h2 className="font-serif text-2xl font-bold text-ink mb-3 group-hover:text-accent transition-colors">
<Link
to={
article.id === "lora-without-regret"
? "/research/lora-without-regret"
: "#"
}
>
{article.title}
</Link>
</h2>
<div className="mb-4 font-sans text-sm text-subtle italic">
{article.authors}
</div>
<p className="font-serif text-lg text-gray-600 leading-relaxed mb-6">
{article.abstract}
</p>
<Link
to={
article.id === "lora-without-regret"
? "/research/lora-without-regret"
: "#"
}
className="inline-flex items-center text-xs font-bold uppercase tracking-widest text-ink group-hover:text-accent transition-colors border-b border-transparent group-hover:border-accent pb-0.5"
>
{t.read_paper} <ArrowRight className="ml-2 w-3 h-3" />
</Link>
</div>
</article>
))
) : (
<div className="py-24 text-center border border-dashed border-gray-200 rounded-lg">
<Filter size={48} className="mx-auto text-gray-200 mb-4" />
<p className="font-serif text-subtle text-lg">{t.no_results}</p>
<button
onClick={() => {
setSearchQuery("");
setSelectedCategory(language === "en" ? "All" : "ทั้งหมด");
}}
className="mt-4 text-sm font-bold text-ink underline"
>
Clear filters
</button>
</div>
)}
</div>
</div>
</div>
);
};

198
pages/Services.tsx Normal file
View File

@ -0,0 +1,198 @@
import React from 'react';
import { TerminalBlock } from '../components/TerminalBlock';
import { Check, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
export const Services: React.FC = () => {
return (
<div className="bg-paper min-h-screen pt-24 pb-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<header className="max-w-3xl mb-24">
<h1 className="font-serif text-4xl md:text-5xl text-ink leading-tight mb-8">
Services
</h1>
<p className="font-serif text-xl text-subtle leading-relaxed border-l-4 border-ink pl-6">
We operate at the intersection of software engineering and machine learning research. Our engagements are typically project-based or retainer-style for high-growth technical teams who need to ship, not just explore.
</p>
</header>
<div className="space-y-32">
{/* Service Block 1: MVP */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center border-t border-gray-200 pt-16">
<div>
<div className="flex items-center gap-4 mb-4">
<span className="font-sans font-bold text-xs uppercase tracking-widest text-accent">01 Prototyping</span>
<div className="h-px bg-gray-200 flex-grow"></div>
</div>
<h3 className="font-serif text-3xl text-ink mb-6">Accelerated MVP</h3>
<p className="font-serif text-lg text-subtle leading-relaxed mb-8">
We help founders and technical leads validate AI hypotheses quickly. Instead of spending months hiring a full ML team, we deliver a production-ready prototype in 4-6 weeks.
</p>
<ul className="space-y-4 font-sans text-sm text-gray-600 mb-8">
{[
"Full stack implementation (React + Python/Node)",
"Model selection (Gemini, Claude, OpenAI) & prompt engineering",
"Basic evaluation pipeline setup",
"Deployment to your cloud (AWS/GCP)"
].map((item, i) => (
<li key={i} className="flex items-start">
<Check className="w-5 h-5 text-green-600 mr-3 shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</div>
<div className="relative">
<div className="absolute -inset-4 bg-gray-100 rounded-lg transform rotate-2 z-0"></div>
<TerminalBlock
title="api/routes.py"
lines={[
"@app.post('/generate')",
"async def generate_insight(ctx: Context):",
" # Production-ready guardrails",
" if not guardrails.check(ctx.prompt):",
" raise SecurityException('Injection detected')",
"",
" # Structured output with retries",
" response = await llm.generate(",
" model='gemini-2.5-flash',",
" prompt=build_prompt(ctx),",
" schema=InsightSchema",
" )",
" return response"
]}
className="relative z-10 shadow-2xl"
/>
</div>
</section>
{/* Service Block 2: Audit */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center border-t border-gray-200 pt-16">
<div className="lg:order-2">
<div className="flex items-center gap-4 mb-4">
<span className="font-sans font-bold text-xs uppercase tracking-widest text-accent">02 Architecture</span>
<div className="h-px bg-gray-200 flex-grow"></div>
</div>
<h3 className="font-serif text-3xl text-ink mb-6">RAG & System Audit</h3>
<p className="font-serif text-lg text-subtle leading-relaxed mb-8">
Your retrieval system is likely the bottleneck. We perform a rigorous audit of your embedding strategies, chunking logic, and reranking stages to improve context relevance and reduce hallucinations.
</p>
<ul className="space-y-4 font-sans text-sm text-gray-600 mb-8">
{[
"Vector Database Indexing Strategy Review",
"Embedding Latency & Cost Analysis",
"Context Window Optimization",
"Security & Prompt Injection Vulnerability Scan"
].map((item, i) => (
<li key={i} className="flex items-start">
<Check className="w-5 h-5 text-green-600 mr-3 shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</div>
<div className="lg:order-1 relative">
<div className="absolute -inset-4 bg-gray-100 rounded-lg transform -rotate-1 z-0"></div>
<TerminalBlock
title="audit_report.json"
lines={[
"{",
" 'system_health': 'DEGRADED',",
" 'metrics': {",
" 'retrieval_precision_at_5': 0.42,",
" 'average_latency_ms': 1250,",
" 'hallucination_rate': '15%'",
" },",
" 'critical_findings': [",
" 'Embedding dimension mismatch in legacy index',",
" 'Missing semantic cache for frequent queries'",
" ],",
" 'recommendation': 'Migrate to hybrid search'",
"}"
]}
className="relative z-10 shadow-2xl"
/>
</div>
</section>
{/* Service Block 3: Integration */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center border-t border-gray-200 pt-16">
<div>
<div className="flex items-center gap-4 mb-4">
<span className="font-sans font-bold text-xs uppercase tracking-widest text-accent">03 Fine-tuning</span>
<div className="h-px bg-gray-200 flex-grow"></div>
</div>
<h3 className="font-serif text-3xl text-ink mb-6">Custom Model Integration</h3>
<p className="font-serif text-lg text-subtle leading-relaxed mb-8">
When prompt engineering hits a wall, we fine-tune open weights (Llama 3, Mistral, Gemma) on your proprietary data. We handle the data cleaning, training runs (LoRA/QLoRA), and deployment optimization.
</p>
<ul className="space-y-4 font-sans text-sm text-gray-600 mb-8">
{[
"Dataset curation and cleaning pipelines",
"Parameter-Efficient Fine-Tuning (LoRA)",
"Evaluation against base model benchmarks",
"Private cloud deployment (AWS/GCP/Azure)"
].map((item, i) => (
<li key={i} className="flex items-start">
<Check className="w-5 h-5 text-green-600 mr-3 shrink-0" />
<span>{item}</span>
</li>
))}
</ul>
</div>
<div className="relative">
<div className="absolute -inset-4 bg-gray-100 rounded-lg transform rotate-1 z-0"></div>
<TerminalBlock
title="train_lora.sh"
lines={[
"$ torchrun --nproc_per_node=8 train.py \\",
" --model_name 'mistralai/Mistral-7B' \\",
" --data_path './proprietary_docs' \\",
" --output_dir './finetuned_weights' \\",
" --use_lora True \\",
" --lora_r 16 \\",
" --lora_alpha 32 \\",
" --quantization '4bit'",
"",
"# Training started...",
"# Epoch 1: loss=0.45",
"# Epoch 2: loss=0.32"
]}
className="relative z-10 shadow-2xl"
/>
</div>
</section>
<section className="bg-ink text-white p-12 md:p-16 rounded-sm mt-12 text-center relative overflow-hidden">
<div className="relative z-10">
<h3 className="font-serif text-3xl mb-6">Ready to build?</h3>
<p className="font-sans text-gray-300 mb-10 max-w-lg mx-auto text-lg">
We take on a limited number of clients per quarter to ensure deep technical focus on every project.
</p>
<a href="mailto:hello@pradit.app" className="inline-flex items-center bg-white text-ink font-bold py-4 px-10 hover:bg-gray-100 transition-colors rounded-sm">
Schedule Consultation <ArrowRight className="ml-2 w-5 h-5" />
</a>
</div>
{/* Background decoration */}
<div className="absolute top-0 left-0 w-full h-full opacity-10 pointer-events-none">
<div className="grid grid-cols-12 h-full">
{[...Array(12)].map((_, i) => (
<div key={i} className="border-r border-white/20 h-full"></div>
))}
</div>
</div>
</section>
</div>
</div>
</div>
);
};

1138
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

29
tsconfig.json Normal file
View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

23
vite.config.ts Normal file
View File

@ -0,0 +1,23 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});