refactor(web): redesign frontend for native web UX
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:
Augustin
2026-04-21 21:25:55 +02:00
parent aa0ff199c6
commit 3dc24ae22c
9 changed files with 620 additions and 829 deletions

View File

@@ -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">&rsaquo;</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>
)}