Files
riotpiao.com/components/InteractiveTerminal.tsx
T
Story Crater Bot 72cc1bbe76
Build & Push Portfolio Image / build-push (push) Successful in 2m37s
feat: theme-aware terminal + agent knowledge doc
- Terminal adapts to dark/light mode
- Add docs/poimen-knowledge.md for memory-service upload
2026-08-31 22:40:45 -07:00

189 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'
import { useTerminal } from '@/lib/TerminalContext'
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)`,
}
export function InteractiveTerminal() {
const [input, setInput] = useState('')
const [history, setHistory] = useState<{ cmd: string; output: string }[]>([])
const { isOpen, setIsOpen } = useTerminal()
const [isMac, setIsMac] = useState(true)
const terminalRef = useRef<HTMLDivElement>(null)
const isDark = useTheme()
useEffect(() => {
// Detect if user is on Mac
const isMacOS = /Mac|iPhone|iPad|iPod/.test(navigator.platform)
setIsMac(isMacOS)
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setIsOpen(!isOpen)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isOpen, setIsOpen])
const handleCommand = (cmd: string) => {
const trimmed = cmd.trim().toLowerCase()
if (trimmed === 'clear') {
setHistory([])
setInput('')
return
}
const output = commands[trimmed] || `command not found: ${cmd}`
setHistory([...history, { cmd, output }])
setInput('')
setTimeout(() => {
terminalRef.current?.scrollTo(0, terminalRef.current.scrollHeight)
}, 0)
}
return (
<motion.div
className="fixed bottom-6 right-6 z-40"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
>
{!isOpen ? (
<button
onClick={() => setIsOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-mono text-sm shadow-lg"
>
Ask Poimen ({isMac ? 'Cmd' : 'Ctrl'}+K)
</button>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className={`rounded-lg shadow-2xl w-96 h-96 flex flex-col border ${
isDark
? 'bg-gray-950 border-gray-700'
: 'bg-white border-gray-300'
}`}
>
<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 (Agent Terminal)
</span>
<button
onClick={() => setIsOpen(false)}
className={isDark ? 'text-gray-400 hover:text-white' : 'text-gray-500 hover:text-gray-900'}
>
</button>
</div>
<div
ref={terminalRef}
className={`flex-1 overflow-y-auto px-4 py-3 font-mono text-sm space-y-2 ${
isDark ? 'text-gray-200' : 'text-gray-800'
}`}
>
{history.length === 0 && (
<div className={isDark ? 'text-gray-500' : 'text-gray-400'}>
type &apos;help&apos; for commands
</div>
)}
{history.map((entry, i) => (
<div key={i}>
<div className={isDark ? 'text-green-400' : 'text-green-600'}>
<span className={isDark ? 'text-red-400' : 'text-red-600'}></span> % {entry.cmd}
</div>
<div className={`whitespace-pre-wrap text-xs mt-1 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
{entry.output}
</div>
</div>
))}
</div>
<div className={`border-t px-4 py-2 ${isDark ? 'border-gray-700' : 'border-gray-300'}`}>
<div className="flex items-center">
<span className={`font-mono text-sm ${isDark ? 'text-red-400' : 'text-red-600'}`}></span>
<span className={`font-mono text-sm ml-2 ${isDark ? 'text-green-400' : 'text-green-600'}`}>% </span>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') handleCommand(input)
}}
placeholder=""
className={`flex-1 bg-transparent font-mono text-sm outline-none ml-1 ${
isDark ? 'text-green-400' : 'text-green-600'
}`}
autoFocus
/>
</div>
</div>
</motion.div>
)}
</motion.div>
)
}