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