Files
riotpiao.com/components/InteractiveTerminal.tsx
T

149 lines
4.6 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'
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] = useState(false)
const terminalRef = useRef<HTMLDivElement>(null)
useEffect(() => {
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])
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"
>
Terminal (Cmd+K)
</button>
) : (
<motion.div
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"
>
<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">ZSH_THEME="robbyrussell"</span>
<button
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-white"
>
</button>
</div>
<div
ref={terminalRef}
className="flex-1 overflow-y-auto px-4 py-3 font-mono text-sm text-gray-200 space-y-2"
>
{history.length === 0 && (
<div className="text-gray-500">type 'help' for commands</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>
</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>
<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 text-green-400 font-mono text-sm outline-none ml-1"
autoFocus
/>
</div>
</div>
</motion.div>
)}
</motion.div>
)
}