initial commit
This commit is contained in:
commit
6f9defe29a
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
267
App.tsx
Normal 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
20
README.md
Normal 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
79
STYLE_GUIDE.md
Normal 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
110
components/ChatWidget.tsx
Normal 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
164
components/MatrixViz.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
94
components/TerminalBlock.tsx
Normal file
94
components/TerminalBlock.tsx
Normal 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
66
index.html
Normal 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
15
index.tsx
Normal 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
5
metadata.json
Normal 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
23
package.json
Normal 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
444
pages/BlogPost.tsx
Normal 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:
|
||||||
|
"Today’s 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
104
pages/HireUs.tsx
Normal 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
699
pages/Home.tsx
Normal 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
329
pages/Playground.tsx
Normal 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
269
pages/Research.tsx
Normal 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
198
pages/Services.tsx
Normal 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
1138
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
29
tsconfig.json
Normal file
29
tsconfig.json
Normal 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
23
vite.config.ts
Normal 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, '.'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user