feat: integrate Poimen LLM chat with reasoning model + deep technical depth
Build & Push Portfolio Image / build-push (push) Successful in 3m20s
Build & Push Portfolio Image / build-push (push) Successful in 3m20s
- Terminal: hook to api.riotpiao.com reasoning model with streaming - Add markdown rendering via react-markdown + typography plugin - Show thinking spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) + V100 32GB context during reasoning - Interview-style help: cert rotation, Talos, SQS failover, vLLM, exactly-once, deploy root cause, K8s CRD - Chat route: ultra-compressed caveman prompt (60% token reduction) - K8s deep expertise: CRDs, operators, reconciliation, Talos, Cilium - Cert management: cert-manager, 30d renewal, SOPS, Git audit trail - Multi-region queues: SQS FIFO, exactly-once, QueueTask CRD - AWS: CDK, CloudFormation, CloudWatch across 57+ regions - Remove dark mode toggle button (system preference only) - Update hire-me.png (dark/light auto-switch) - Add tailwind typography for prose styling
This commit is contained in:
@@ -3,56 +3,87 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTerminal } from '@/lib/TerminalContext'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
function Spinner() {
|
||||
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
||||
const [frame, setFrame] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setFrame(f => (f + 1) % frames.length)
|
||||
}, 80)
|
||||
return () => clearInterval(interval)
|
||||
}, [frames.length])
|
||||
|
||||
return <span className="inline-block w-3">{frames[frame]}</span>
|
||||
}
|
||||
|
||||
function useTheme() {
|
||||
const [isDark, setIsDark] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => setIsDark(document.documentElement.classList.contains('dark'))
|
||||
check()
|
||||
|
||||
const observer = new MutationObserver(check)
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
return isDark
|
||||
}
|
||||
|
||||
const commands: Record<string, string> = {
|
||||
help: `Available commands:
|
||||
ls - List services
|
||||
kubectl - Cluster info
|
||||
terraform - Infrastructure
|
||||
argocd - Deployments
|
||||
kafka - Message broker
|
||||
about - About me
|
||||
clear - Clear terminal`,
|
||||
ls: `storage/
|
||||
monitoring/
|
||||
cicd/
|
||||
sqs/
|
||||
temporal/
|
||||
iam/`,
|
||||
kubectl: `Nodes: 3 (talos-cp-1, talos-worker-1, talos-worker-2)
|
||||
Pods: 42 running
|
||||
Uptime: 99.2%
|
||||
Cluster Version: v1.36.1`,
|
||||
terraform: `Modules: 18 deployed
|
||||
Services: Kafka, PostgreSQL, MinIO, Grafana
|
||||
Storage: Longhorn (3-replica)
|
||||
Network: Cilium eBPF CNI`,
|
||||
argocd: `Applications: 12
|
||||
Synced: 11/12
|
||||
Last Sync: 2 min ago
|
||||
Health: Healthy`,
|
||||
kafka: `Brokers: 3 (KRaft)
|
||||
Replication Factor: 3
|
||||
Min ISR: 2
|
||||
Topics: 5 active
|
||||
Throughput: ~1K msgs/sec`,
|
||||
about: `Rock Liang - Senior Full-Stack Systems Engineer
|
||||
Experience: 5+ years (AWS, RBC, Homelab)
|
||||
Focus: Infrastructure × Backend × LLM Systems
|
||||
Tech: Go, Java, Python, C++ | Kubernetes, Terraform, gRPC
|
||||
Current: Building production homelab + LLM inference optimization
|
||||
Open Source: go-flink (distributed DataLakeHouse)`,
|
||||
help: `Commands: help, /clear
|
||||
|
||||
INTERVIEW-STYLE QUESTIONS:
|
||||
• Cert rotation in production—downtime?
|
||||
• Why Talos over standard K8s?
|
||||
• Multi-region SQS failover—atomic safety?
|
||||
• vLLM 60% latency: bottleneck & fix?
|
||||
• Exactly-once delivery design?
|
||||
• 2hr→20min deploy: root cause?
|
||||
• K8s CRD reconciliation at scale?
|
||||
|
||||
homelab | RBC | AWS | ask anything`,
|
||||
about: `Rock Liang - Senior Software Engineer
|
||||
6+ years | Infrastructure × Backend × LLM Systems
|
||||
AWS → RBC → Building production homelab
|
||||
|
||||
Ask me anything specific!`,
|
||||
}
|
||||
|
||||
interface HistoryEntry {
|
||||
cmd: string
|
||||
output: string
|
||||
thinking?: string
|
||||
isStreaming?: boolean
|
||||
}
|
||||
|
||||
export function InteractiveTerminal() {
|
||||
const [input, setInput] = useState('')
|
||||
const [history, setHistory] = useState<{ cmd: string; output: string }[]>([])
|
||||
const [history, setHistory] = useState<HistoryEntry[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { isOpen, setIsOpen } = useTerminal()
|
||||
const [isMac, setIsMac] = useState(true)
|
||||
const terminalRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const isDark = useTheme()
|
||||
|
||||
// Close on click outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (isOpen && containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isOpen, setIsOpen])
|
||||
|
||||
useEffect(() => {
|
||||
// Detect if user is on Mac
|
||||
const isMacOS = /Mac|iPhone|iPad|iPod/.test(navigator.platform)
|
||||
setIsMac(isMacOS)
|
||||
|
||||
@@ -67,21 +98,104 @@ export function InteractiveTerminal() {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [isOpen, setIsOpen])
|
||||
|
||||
const handleCommand = (cmd: string) => {
|
||||
const trimmed = cmd.trim().toLowerCase()
|
||||
if (trimmed === 'clear') {
|
||||
// Auto-scroll on history change
|
||||
useEffect(() => {
|
||||
terminalRef.current?.scrollTo(0, terminalRef.current.scrollHeight)
|
||||
}, [history])
|
||||
|
||||
// Focus input when opened
|
||||
useEffect(() => {
|
||||
if (isOpen) inputRef.current?.focus()
|
||||
}, [isOpen])
|
||||
|
||||
const handleCommand = async (cmd: string) => {
|
||||
const trimmed = cmd.trim()
|
||||
if (!trimmed) return
|
||||
|
||||
const lowerCmd = trimmed.toLowerCase()
|
||||
|
||||
if (lowerCmd === 'clear' || lowerCmd === '/clear') {
|
||||
setHistory([])
|
||||
setInput('')
|
||||
return
|
||||
}
|
||||
|
||||
const output = commands[trimmed] || `command not found: ${cmd}`
|
||||
setHistory([...history, { cmd, output }])
|
||||
setInput('')
|
||||
// Check for built-in commands
|
||||
if (commands[lowerCmd]) {
|
||||
setHistory(prev => [...prev, { cmd: trimmed, output: commands[lowerCmd] }])
|
||||
setInput('')
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
terminalRef.current?.scrollTo(0, terminalRef.current.scrollHeight)
|
||||
}, 0)
|
||||
// Otherwise, call the LLM API
|
||||
setIsLoading(true)
|
||||
setInput('')
|
||||
|
||||
const entryIndex = history.length
|
||||
setHistory(prev => [...prev, { cmd: trimmed, output: '', thinking: '', isStreaming: true }])
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: trimmed }),
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('API error')
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) throw new Error('No reader')
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const lines = chunk.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue
|
||||
const data = line.slice(6)
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data)
|
||||
|
||||
setHistory(prev => {
|
||||
const updated = [...prev]
|
||||
const entry = updated[entryIndex]
|
||||
if (!entry) return prev
|
||||
|
||||
if (json.type === 'reasoning') {
|
||||
entry.thinking = json.text
|
||||
} else if (json.type === 'content') {
|
||||
entry.output = json.text
|
||||
} else if (json.type === 'done') {
|
||||
entry.isStreaming = false
|
||||
}
|
||||
|
||||
return updated
|
||||
})
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setHistory(prev => {
|
||||
const updated = [...prev]
|
||||
const entry = updated[entryIndex]
|
||||
if (entry) {
|
||||
entry.output = 'Error: Failed to get response. Try again.'
|
||||
entry.isStreaming = false
|
||||
}
|
||||
return updated
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -99,15 +213,26 @@ export function InteractiveTerminal() {
|
||||
</button>
|
||||
) : (
|
||||
<motion.div
|
||||
ref={containerRef}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-gray-950 border border-gray-700 rounded-lg shadow-2xl w-96 h-96 flex flex-col"
|
||||
className={`rounded-lg shadow-2xl w-[28rem] h-[32rem] flex flex-col border ${
|
||||
isDark
|
||||
? 'bg-gray-950 border-gray-700'
|
||||
: 'bg-white border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="bg-gray-900 border-b border-gray-700 px-4 py-3 flex justify-between items-center">
|
||||
<span className="text-gray-300 font-mono text-xs">Poimen (Agent Terminal)</span>
|
||||
<div className={`border-b px-4 py-3 flex justify-between items-center ${
|
||||
isDark
|
||||
? 'bg-gray-900 border-gray-700'
|
||||
: 'bg-gray-100 border-gray-300'
|
||||
}`}>
|
||||
<span className={`font-mono text-xs ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
|
||||
Poimen (AI Assistant)
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="text-gray-400 hover:text-white"
|
||||
className={isDark ? 'text-gray-400 hover:text-white' : 'text-gray-500 hover:text-gray-900'}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@@ -115,34 +240,65 @@ export function InteractiveTerminal() {
|
||||
|
||||
<div
|
||||
ref={terminalRef}
|
||||
className="flex-1 overflow-y-auto px-4 py-3 font-mono text-sm text-gray-200 space-y-2"
|
||||
className={`flex-1 overflow-y-auto px-4 py-3 font-mono text-sm space-y-4 ${
|
||||
isDark ? 'text-gray-200' : 'text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{history.length === 0 && (
|
||||
<div className="text-gray-500">type 'help' for commands</div>
|
||||
<div className={isDark ? 'text-gray-500' : 'text-gray-400'}>
|
||||
Ask me about Rock's experience, projects, or skills!
|
||||
<br />
|
||||
<span className="text-xs">Type 'help' for commands</span>
|
||||
</div>
|
||||
)}
|
||||
{history.map((entry, i) => (
|
||||
<div key={i}>
|
||||
<div className="text-green-400"><span className="text-red-400">➜</span> % {entry.cmd}</div>
|
||||
<div className="text-gray-300 whitespace-pre-wrap text-xs mt-1">
|
||||
{entry.output}
|
||||
<div key={i} className="space-y-2">
|
||||
<div className={isDark ? 'text-green-400' : 'text-green-600'}>
|
||||
<span className={isDark ? 'text-blue-400' : 'text-blue-600'}>→</span> {entry.cmd}
|
||||
</div>
|
||||
|
||||
{entry.isStreaming && !entry.output && (
|
||||
<div className={`space-y-2 text-xs ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
|
||||
<div className="flex items-start gap-2">
|
||||
<Spinner />
|
||||
<span className="italic truncate max-w-[300px]">
|
||||
{entry.thinking ? entry.thinking.slice(-80) : 'Working...'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`text-[11px] ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
|
||||
(Reasoning on V100 32GB, slight delay expected)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entry.output && (
|
||||
<div className={`prose prose-sm max-w-none ${isDark ? 'prose-invert' : ''}`}>
|
||||
<ReactMarkdown>{entry.output}</ReactMarkdown>
|
||||
{entry.isStreaming && (
|
||||
<span className="animate-pulse">▊</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700 px-4 py-2">
|
||||
<div className="flex items-center">
|
||||
<span className="text-red-400 font-mono text-sm">➜</span>
|
||||
<span className="text-green-400 font-mono text-sm ml-2">% </span>
|
||||
<div className={`border-t px-4 py-3 ${isDark ? 'border-gray-700' : 'border-gray-300'}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={isDark ? 'text-blue-400' : 'text-blue-600'}>→</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') handleCommand(input)
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !isLoading) handleCommand(input)
|
||||
}}
|
||||
placeholder=""
|
||||
className="flex-1 bg-transparent text-green-400 font-mono text-sm outline-none ml-1"
|
||||
placeholder={isLoading ? 'Thinking...' : 'Ask anything...'}
|
||||
disabled={isLoading}
|
||||
className={`flex-1 bg-transparent text-sm outline-none ${
|
||||
isDark ? 'text-gray-200 placeholder-gray-600' : 'text-gray-800 placeholder-gray-400'
|
||||
} ${isLoading ? 'opacity-50' : ''}`}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user