refactor(web): redesign frontend for native web UX
All checks were successful
Beta Release / beta (push) Successful in 33s
All checks were successful
Beta Release / beta (push) Successful in 33s
- Replace all TUI artifacts: [OK], [FAIL], >>, [■], [$] with proper web components (badges, cards, chips, avatars) - Rename CSS variables from TUI names (cyberRed, dimRed, bgVoid) to semantic names (accent, accent-dim, bg) - Add proper interactive elements: hover states, cursor pointer, click feedback (scale), focus rings, spinner animation - Fix user-select: was none globally, now allows text selection - Redesign navigation: proper tabs with role="tab" and aria attributes - Add keyboard shortcuts only when not in input/textarea (1-4 for tabs) - Replace footer TUI shortcuts with clean statusbar - Dashboard: card-based layout, badge status, progress bar, activity log - Studio: message bubbles (aligned left/right), agent cards with avatars - Shell: command history (ArrowUp/Down), toggleable AI panel button, panel header with current directory - Config: provider cards, color swatches for theme picker, clean field rows with empty states - CSS imported via main.jsx (not HTML link) for proper Vite hashing - Remove glitch/scanline/typewriter TUI animations - Add favicon 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
@@ -4,12 +4,14 @@ export default function Shell({ api }) {
|
||||
const [history, setHistory] = useState([])
|
||||
const [input, setInput] = useState('')
|
||||
const [cwd, setCwd] = useState('~')
|
||||
const [aiPanel, setAiPanel] = useState(true)
|
||||
const [showAi, setShowAi] = useState(false)
|
||||
const [aiMessages, setAiMessages] = useState([
|
||||
{ role: 'ai', content: '>> I know your system inside out. Ask me anything.' }
|
||||
{ role: 'ai', content: 'I know your system inside out. Ask me anything.' }
|
||||
])
|
||||
const [aiInput, setAiInput] = useState('')
|
||||
const [aiLoading, setAiLoading] = useState(false)
|
||||
const [cmdHistory, setCmdHistory] = useState([])
|
||||
const [histIdx, setHistIdx] = useState(-1)
|
||||
const outputRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -18,28 +20,22 @@ export default function Shell({ api }) {
|
||||
|
||||
const handleCommand = async (cmd) => {
|
||||
if (!cmd.trim()) return
|
||||
if (cmd === 'clear') {
|
||||
setHistory([])
|
||||
return
|
||||
}
|
||||
if (cmd === 'exit' || cmd === 'quit') return
|
||||
if (cmd === 'clear') { setHistory([]); return }
|
||||
|
||||
setHistory(prev => [...prev, { type: 'input', text: `${cwd} $ ${cmd}` }])
|
||||
setCmdHistory(prev => [...prev, cmd])
|
||||
setHistIdx(-1)
|
||||
setHistory(prev => [...prev, { type: 'cmd', text: `${cwd} $ ${cmd}` }])
|
||||
|
||||
try {
|
||||
const res = await api.runCommand(cmd, cwd === '~' ? '' : cwd)
|
||||
if (res.output) {
|
||||
setHistory(prev => [...prev, { type: 'output', text: res.output }])
|
||||
}
|
||||
if (res.error) {
|
||||
setHistory(prev => [...prev, { type: 'error', text: res.error }])
|
||||
}
|
||||
if (res.output) setHistory(prev => [...prev, { type: 'out', text: res.output }])
|
||||
if (res.error) setHistory(prev => [...prev, { type: 'err', text: res.error }])
|
||||
if (cmd.startsWith('cd ')) {
|
||||
const dir = cmd.slice(3).trim()
|
||||
setCwd(dir === '~' ? '~' : dir)
|
||||
}
|
||||
} catch (err) {
|
||||
setHistory(prev => [...prev, { type: 'error', text: err.message }])
|
||||
setHistory(prev => [...prev, { type: 'err', text: err.message }])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,52 +44,60 @@ export default function Shell({ api }) {
|
||||
e.preventDefault()
|
||||
handleCommand(input)
|
||||
setInput('')
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
if (cmdHistory.length === 0) return
|
||||
const newIdx = histIdx === -1 ? cmdHistory.length - 1 : Math.max(0, histIdx - 1)
|
||||
setHistIdx(newIdx)
|
||||
setInput(cmdHistory[newIdx])
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
if (histIdx === -1) return
|
||||
const newIdx = histIdx + 1
|
||||
if (newIdx >= cmdHistory.length) { setHistIdx(-1); setInput('') }
|
||||
else { setHistIdx(newIdx); setInput(cmdHistory[newIdx]) }
|
||||
}
|
||||
}
|
||||
|
||||
const handleAiSend = async () => {
|
||||
if (!aiInput.trim() || aiLoading) return
|
||||
const text = aiInput.trim()
|
||||
setAiMessages(prev => [...prev, { role: 'user', content: '>> ' + text }])
|
||||
setAiMessages(prev => [...prev, { role: 'user', content: text }])
|
||||
setAiInput('')
|
||||
setAiLoading(true)
|
||||
|
||||
try {
|
||||
const res = await api.runCommand(`echo "AI: ${text}"`, '')
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: '>> ' + (res.output || 'No response') }])
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: res.output || 'No response' }])
|
||||
} catch (err) {
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }])
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: `Error: ${err.message}` }])
|
||||
}
|
||||
setAiLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="split-horizontal" style={{ height: '100%' }}>
|
||||
<div className="terminal-container" style={{ flex: 1 }}>
|
||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border-dim)' }}>
|
||||
<div className="section-header" style={{ margin: 0 }}>
|
||||
<div className="terminal" style={{ flex: 1 }}>
|
||||
<div className="panel-header">
|
||||
<span className="panel-title">
|
||||
Terminal
|
||||
<span style={{ color: 'var(--dim-red)', fontWeight: 400, marginLeft: 12, fontSize: 11 }}>{cwd}</span>
|
||||
</div>
|
||||
<span className="panel-subtitle">{cwd}</span>
|
||||
</span>
|
||||
<button className="ghost sm" onClick={() => setShowAi(!showAi)}>
|
||||
{showAi ? 'Hide AI' : 'AI Assistant'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="terminal-output" ref={outputRef}>
|
||||
{history.map((line, i) => (
|
||||
<div key={i} style={{
|
||||
color: line.type === 'input' ? 'var(--dim-red)' :
|
||||
line.type === 'error' ? 'var(--error)' : 'var(--text-main)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.4,
|
||||
}}>
|
||||
<div key={i} className={`terminal-line ${line.type}`}>
|
||||
{line.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="terminal-input-row">
|
||||
<span className="terminal-prompt">{'>'}</span>
|
||||
<div className="terminal-input-bar">
|
||||
<span className="terminal-prompt">›</span>
|
||||
<input
|
||||
className="terminal-input"
|
||||
value={input}
|
||||
@@ -104,44 +108,25 @@ export default function Shell({ api }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aiPanel && (
|
||||
<div style={{
|
||||
width: 320,
|
||||
borderLeft: '1px solid var(--border-dim)',
|
||||
background: 'var(--bg-surface)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid var(--border-dim)' }}>
|
||||
<div className="section-header" style={{ margin: 0 }}>AI Assistant</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 12 }}>
|
||||
{showAi && (
|
||||
<div className="ai-panel">
|
||||
<div className="ai-panel-header">AI Assistant</div>
|
||||
<div className="ai-panel-messages">
|
||||
{aiMessages.map((msg, i) => (
|
||||
<div key={i} style={{
|
||||
marginBottom: 8,
|
||||
padding: '6px 8px',
|
||||
borderRadius: 'var(--radius)',
|
||||
background: msg.role === 'ai' ? 'var(--bg-card)' : 'var(--muted-red)',
|
||||
borderLeft: `3px solid ${msg.role === 'ai' ? 'var(--cyber-red)' : 'var(--cyber-rose)'}`,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.4,
|
||||
}}>
|
||||
<div key={i} className={`ai-message ${msg.role}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
))}
|
||||
{aiLoading && <span className="loading-spinner"> thinking...</span>}
|
||||
{aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '8px 12px', borderTop: '1px solid var(--border-dim)', display: 'flex', gap: 6 }}>
|
||||
<div className="ai-panel-input">
|
||||
<input
|
||||
style={{ flex: 1, padding: '4px 8px', fontSize: 12 }}
|
||||
value={aiInput}
|
||||
onChange={e => setAiInput(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleAiSend()}
|
||||
placeholder="Ask AI..."
|
||||
/>
|
||||
<button style={{ padding: '4px 8px' }} onClick={handleAiSend}>Send</button>
|
||||
<button className="sm" onClick={handleAiSend} disabled={!aiInput.trim()}>Send</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user