Compare commits
2 Commits
v0.2.1-bet
...
v0.2.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11417d3ea7 | ||
|
|
3dc24ae22c |
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
|
||||
"github.com/muyue/muyue/internal/config"
|
||||
"github.com/muyue/muyue/internal/lsp"
|
||||
"github.com/muyue/muyue/internal/mcp"
|
||||
"github.com/muyue/muyue/internal/scanner"
|
||||
@@ -174,6 +175,36 @@ func (s *Server) handleScan(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdatePreferences(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PUT" {
|
||||
writeError(w, "PUT only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if s.config == nil {
|
||||
writeError(w, "no config", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Language string `json:"language"`
|
||||
KeyboardLayout string `json:"keyboard_layout"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Language != "" {
|
||||
s.config.Profile.Preferences.Language = body.Language
|
||||
}
|
||||
if body.KeyboardLayout != "" {
|
||||
s.config.Profile.Preferences.KeyboardLayout = body.KeyboardLayout
|
||||
}
|
||||
if err := config.Save(s.config); err != nil {
|
||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleTerminal(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
|
||||
@@ -35,6 +35,7 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("/api/updates", s.handleUpdates)
|
||||
s.mux.HandleFunc("/api/install", s.handleInstall)
|
||||
s.mux.HandleFunc("/api/scan", s.handleScan)
|
||||
s.mux.HandleFunc("/api/preferences", s.handleUpdatePreferences)
|
||||
s.mux.HandleFunc("/api/terminal", s.handleTerminal)
|
||||
s.mux.HandleFunc("/api/mcp/configure", s.handleMCPConfigure)
|
||||
}
|
||||
@@ -42,7 +43,7 @@ func (s *Server) routes() {
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
@@ -15,12 +15,14 @@ type Profile struct {
|
||||
Email string `yaml:"email"`
|
||||
Languages []string `yaml:"languages"`
|
||||
Preferences struct {
|
||||
Editor string `yaml:"editor"`
|
||||
Shell string `yaml:"shell"`
|
||||
Theme string `yaml:"theme"`
|
||||
DefaultAI string `yaml:"default_ai"`
|
||||
AutoUpdate bool `yaml:"auto_update"`
|
||||
CheckOnStart bool `yaml:"check_on_start"`
|
||||
Editor string `yaml:"editor"`
|
||||
Shell string `yaml:"shell"`
|
||||
Theme string `yaml:"theme"`
|
||||
DefaultAI string `yaml:"default_ai"`
|
||||
AutoUpdate bool `yaml:"auto_update"`
|
||||
CheckOnStart bool `yaml:"check_on_start"`
|
||||
Language string `yaml:"language"`
|
||||
KeyboardLayout string `yaml:"keyboard_layout"`
|
||||
} `yaml:"preferences"`
|
||||
}
|
||||
|
||||
@@ -179,6 +181,8 @@ func Default() *MuyueConfig {
|
||||
cfg.Profile.Preferences.AutoUpdate = true
|
||||
cfg.Profile.Preferences.CheckOnStart = true
|
||||
cfg.Profile.Preferences.Theme = "charm"
|
||||
cfg.Profile.Preferences.Language = "fr"
|
||||
cfg.Profile.Preferences.KeyboardLayout = "azerty"
|
||||
|
||||
cfg.AI.Providers = []AIProvider{
|
||||
{
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0A0A0C" />
|
||||
<title>muyue</title>
|
||||
<link rel="stylesheet" href="/src/styles/global.css" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⬡</text></svg>" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -25,6 +25,7 @@ const api = {
|
||||
runScan: () => request('/scan', { method: 'POST' }),
|
||||
installTools: (tools) => request('/install', { method: 'POST', body: JSON.stringify({ tools }) }),
|
||||
configureMCP: () => request('/mcp/configure', { method: 'POST' }),
|
||||
savePreferences: (prefs) => request('/preferences', { method: 'PUT', body: JSON.stringify(prefs) }),
|
||||
runCommand: (command, cwd) => request('/terminal', { method: 'POST', body: JSON.stringify({ command, cwd }) }),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,88 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import api from '../api/client'
|
||||
import { getTheme, getThemeNames, applyTheme } from '../themes'
|
||||
import { useI18n } from '../i18n'
|
||||
import Dashboard from './Dashboard'
|
||||
import Studio from './Studio'
|
||||
import Shell from './Shell'
|
||||
import Config from './Config'
|
||||
|
||||
const TABS = [
|
||||
{ id: 'dash', label: 'DASH', icon: '[■]' },
|
||||
{ id: 'studio', label: 'STUDIO', icon: '[<>]' },
|
||||
{ id: 'shell', label: 'SHELL', icon: '[$]' },
|
||||
{ id: 'config', label: 'CONFIG', icon: '[//]' },
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
const [activeTab, setActiveTab] = useState('dash')
|
||||
const [info, setInfo] = useState({})
|
||||
const [clock, setClock] = useState(new Date())
|
||||
const [updates, setUpdates] = useState([])
|
||||
const [tools, setTools] = useState([])
|
||||
const [transition, setTransition] = useState(false)
|
||||
const [currentTheme, setCurrentTheme] = useState('cyberpunk-red')
|
||||
// api is imported directly
|
||||
const { t, layout } = useI18n()
|
||||
|
||||
const TABS = useMemo(() => [
|
||||
{ id: 'dash', label: t('tabs.dashboard'), icon: '\u25A0' },
|
||||
{ id: 'studio', label: t('tabs.studio'), icon: '\u27E8\u27E9' },
|
||||
{ id: 'shell', label: t('tabs.shell'), icon: '$' },
|
||||
{ id: 'config', label: t('tabs.config'), icon: '\u2699' },
|
||||
], [t])
|
||||
|
||||
useEffect(() => {
|
||||
api.getInfo().then(setInfo).catch(() => {})
|
||||
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
||||
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
||||
|
||||
const theme = getTheme(currentTheme)
|
||||
applyTheme(theme)
|
||||
applyTheme(getTheme('cyberpunk-red'))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setClock(new Date()), 1000)
|
||||
return () => clearInterval(timer)
|
||||
const id = setInterval(() => setClock(new Date()), 1000)
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
const switchTab = useCallback((tabId) => {
|
||||
if (tabId === activeTab) return
|
||||
setTransition(true)
|
||||
setTimeout(() => {
|
||||
setActiveTab(tabId)
|
||||
setTimeout(() => setTransition(false), 150)
|
||||
}, 100)
|
||||
}, [activeTab])
|
||||
useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
||||
if (!e.ctrlKey && !e.metaKey) return
|
||||
const map = {
|
||||
Digit1: 'dash',
|
||||
Digit2: 'studio',
|
||||
Digit3: 'shell',
|
||||
Digit4: 'config',
|
||||
}
|
||||
if (map[e.code]) {
|
||||
e.preventDefault()
|
||||
setActiveTab(map[e.code])
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [])
|
||||
|
||||
const switchTab = useCallback((tabId) => setActiveTab(tabId), [])
|
||||
|
||||
const hasUpdates = updates.some(u => u.needsUpdate)
|
||||
const installed = tools.filter(t => t.installed).length
|
||||
|
||||
const WINDOW_SHORTCUTS = useMemo(() => ({
|
||||
dash: [
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
||||
],
|
||||
studio: [
|
||||
{ keys: layout.keys.enter, desc: t('statusbar.sendMessage') },
|
||||
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
||||
],
|
||||
shell: [
|
||||
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
||||
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
||||
],
|
||||
config: [
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
||||
],
|
||||
}), [layout, t])
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'dash': return <Dashboard tools={tools} updates={updates} api={api} onRescan={(t) => setTools(t)} />
|
||||
case 'dash': return <Dashboard tools={tools} updates={updates} api={api} onRescan={t => setTools(t)} />
|
||||
case 'studio': return <Studio api={api} />
|
||||
case 'shell': return <Shell api={api} />
|
||||
case 'config': return <Config api={api} theme={currentTheme} onThemeChange={setCurrentTheme} />
|
||||
case 'config': return <Config api={api} onThemeChange={() => {}} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
@@ -61,45 +90,66 @@ export default function App() {
|
||||
return (
|
||||
<div className="app-layout">
|
||||
<header className="header">
|
||||
<span className="header-logo">MUYUE</span>
|
||||
<span className="header-version">v{info.version || '...'}</span>
|
||||
<div className="header-brand">
|
||||
<span className="header-logo">MUYUE</span>
|
||||
<span className="header-version">v{info.version || '...'}</span>
|
||||
</div>
|
||||
|
||||
<div className="header-tabs">
|
||||
<nav className="header-nav">
|
||||
{TABS.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`header-tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||
className={`nav-tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => switchTab(tab.id)}
|
||||
role="tab"
|
||||
aria-selected={activeTab === tab.id}
|
||||
>
|
||||
{tab.icon} {tab.label}
|
||||
<span className="tab-icon">{tab.icon}</span>
|
||||
{tab.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="header-spacer" />
|
||||
|
||||
<div className="header-status">
|
||||
<span className={`status-dot ${tools.length > 0 ? 'ok' : 'off'}`} title="System" />
|
||||
<span className={`status-dot ${hasUpdates ? 'warn' : 'ok'}`} title="Updates" />
|
||||
<div className="header-indicators">
|
||||
<span
|
||||
className={`indicator ${installed > 0 ? 'ok' : 'off'}`}
|
||||
title={t('header.toolsInstalled', { count: installed })}
|
||||
/>
|
||||
<span
|
||||
className={`indicator ${hasUpdates ? 'warn' : 'ok'}`}
|
||||
title={hasUpdates ? t('header.updatesAvailable') : t('header.upToDate')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="header-date">{clock.toLocaleDateString('fr-FR')}</span>
|
||||
<span className="header-clock">{clock.toLocaleTimeString('fr-FR')}</span>
|
||||
<span className="header-clock">
|
||||
{clock.toLocaleTimeString(layout.locale, { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className={`content ${transition ? 'glitch-text' : 'fade-in tab-transition'}`}>
|
||||
<main className="content fade-in" key={`${activeTab}-${TABS.length}`}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className="footer">
|
||||
<span className="footer-shortcuts">
|
||||
<kbd>1-4</kbd> tabs · <kbd>Ctrl+T</kbd> switcher · <kbd>Ctrl+C</kbd> quit
|
||||
</span>
|
||||
<span className={`footer-update ${hasUpdates ? 'available' : 'uptodate'}`}>
|
||||
{hasUpdates ? '[UPD] Updates available' : '[OK] Up to date'}
|
||||
</span>
|
||||
<span className="footer-version">v{info.version || '...'}</span>
|
||||
<footer className="statusbar">
|
||||
<div className="statusbar-left">
|
||||
<FooterShortcuts shortcuts={WINDOW_SHORTCUTS[activeTab] || []} />
|
||||
</div>
|
||||
<div className="statusbar-right">
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>
|
||||
{layout.keys.ctrl}+{layout.keys.range} {t('statusbar.switchWindow')}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FooterShortcuts({ shortcuts }) {
|
||||
return shortcuts.map((s, i) => (
|
||||
<span key={i} className="statusbar-shortcut">
|
||||
<kbd>{s.keys}</kbd> {s.desc}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getThemeNames, applyTheme, getTheme } from '../themes'
|
||||
import { useI18n, LANGUAGES } from '../i18n'
|
||||
import { getLayoutList } from '../i18n/keyboards'
|
||||
|
||||
export default function Config({ api, theme, onThemeChange }) {
|
||||
export default function Config({ api }) {
|
||||
const { t, language, keyboard, setLanguage, setKeyboard, layout } = useI18n()
|
||||
const [config, setConfig] = useState(null)
|
||||
const [providers, setProviders] = useState([])
|
||||
const [skillList, setSkillList] = useState([])
|
||||
const [currentTheme, setCurrentTheme] = useState('cyberpunk-red')
|
||||
|
||||
useEffect(() => {
|
||||
api.getConfig().then(d => setConfig(d)).catch(() => {})
|
||||
@@ -13,73 +17,117 @@ export default function Config({ api, theme, onThemeChange }) {
|
||||
}, [])
|
||||
|
||||
const themes = getThemeNames()
|
||||
const layouts = getLayoutList()
|
||||
|
||||
const handleThemeChange = (themeId) => {
|
||||
const t = getTheme(themeId)
|
||||
applyTheme(t)
|
||||
onThemeChange(themeId)
|
||||
applyTheme(getTheme(themeId))
|
||||
setCurrentTheme(themeId)
|
||||
}
|
||||
|
||||
const themeColors = {
|
||||
'cyberpunk-red': '#FF0033',
|
||||
'cyberpunk-pink': '#FF1A8C',
|
||||
'midnight-blue': '#0088FF',
|
||||
'matrix-green': '#00FF41',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="config-container">
|
||||
<div className="config-layout">
|
||||
<div className="config-section">
|
||||
<div className="section-header">Profile</div>
|
||||
{config?.profile && (
|
||||
<div className="config-section-title">{t('config.profile')}</div>
|
||||
{config?.profile ? (
|
||||
<div>
|
||||
<Field label="Name" value={config.profile.name} />
|
||||
<Field label="Pseudo" value={config.profile.pseudo} />
|
||||
<Field label="Email" value={config.profile.email} />
|
||||
<Field label="Editor" value={config.profile.preferences?.editor} />
|
||||
<Field label="Shell" value={config.profile.preferences?.shell} />
|
||||
<Field label="Default AI" value={config.profile.preferences?.defaultAI} />
|
||||
<Field label="Languages" value={config.profile.languages?.join(', ')} />
|
||||
<FieldRow label={t('config.name')} value={config.profile.name} />
|
||||
<FieldRow label={t('config.pseudo')} value={config.profile.pseudo} />
|
||||
<FieldRow label={t('config.email')} value={config.profile.email} />
|
||||
<FieldRow label={t('config.editor')} value={config.profile.preferences?.editor} />
|
||||
<FieldRow label={t('config.shell')} value={config.profile.preferences?.shell} />
|
||||
<FieldRow label={t('config.defaultAi')} value={config.profile.preferences?.defaultAI} />
|
||||
<FieldRow label={t('config.languages')} value={config.profile.languages?.join(', ')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-state">{t('config.loadingProfile')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="section-header">AI Providers</div>
|
||||
{providers.map((p, i) => (
|
||||
<div key={i} className="config-field" style={{ flexDirection: 'column', alignItems: 'flex-start', gap: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ color: 'var(--text-bright)', fontWeight: 700 }}>{p.name}</span>
|
||||
{p.active && <span style={{ color: 'var(--cyber-red)', fontSize: 11, fontWeight: 700 }}>{'>>'}</span>}
|
||||
<div className="config-section-title">{t('config.language')}</div>
|
||||
<div className="actions-stack">
|
||||
{LANGUAGES.map(lang => (
|
||||
<div
|
||||
key={lang.id}
|
||||
className={`chip ${language === lang.id ? 'active' : ''}`}
|
||||
onClick={() => setLanguage(lang.id)}
|
||||
>
|
||||
{lang.name}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16, fontSize: 12 }}>
|
||||
<span style={{ color: 'var(--dim-red)' }}>model={p.model}</span>
|
||||
<span style={{ color: p.apiKey ? 'var(--success)' : 'var(--error)' }}>
|
||||
key={p.apiKey ? 'configured' : 'no key'}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="config-section-title">{t('config.keyboardLayout')}</div>
|
||||
<div className="actions-stack">
|
||||
{layouts.map(l => (
|
||||
<div
|
||||
key={l.id}
|
||||
className={`chip ${keyboard === l.id ? 'active' : ''}`}
|
||||
onClick={() => setKeyboard(l.id)}
|
||||
>
|
||||
{l.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="config-section-title">{t('config.aiProviders')}</div>
|
||||
{providers.map((p, i) => (
|
||||
<div key={i} className="provider-card">
|
||||
<div className="provider-info">
|
||||
<div className="provider-name">
|
||||
{p.name}
|
||||
{p.active && <span className="badge accent" style={{ marginLeft: 8 }}>{t('config.active')}</span>}
|
||||
</div>
|
||||
<div className="provider-meta">
|
||||
<span>{p.model}</span>
|
||||
<span style={{ color: p.apiKey ? 'var(--success)' : 'var(--error)' }}>
|
||||
{p.apiKey ? t('config.keyConfigured') : t('config.noKey')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="section-header">Theme</div>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{themes.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
className={theme === t.id ? 'primary' : ''}
|
||||
onClick={() => handleThemeChange(t.id)}
|
||||
>
|
||||
{t.name}
|
||||
</button>
|
||||
<div className="config-section-title">{t('config.theme')}</div>
|
||||
<div className="theme-picker">
|
||||
{themes.map(th => (
|
||||
<div
|
||||
key={th.id}
|
||||
className={`theme-swatch ${currentTheme === th.id ? 'active' : ''}`}
|
||||
style={{ background: themeColors[th.id] || '#FF0033' }}
|
||||
onClick={() => handleThemeChange(th.id)}
|
||||
title={th.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-section">
|
||||
<div className="section-header">Skills ({skillList.length})</div>
|
||||
<div className="config-section-title">{t('config.skills')} ({skillList.length})</div>
|
||||
{skillList.length === 0 ? (
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 12 }}>No skills. Run `muyue skills init`.</span>
|
||||
<div className="empty-state">
|
||||
{t('config.noSkills')}
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>{t('config.runSkillsInit')}</span>
|
||||
</div>
|
||||
) : (
|
||||
skillList.map((s, i) => (
|
||||
<div key={i} className="tool-item">
|
||||
<div key={i} className="tool-row">
|
||||
<span className="tool-name">{s.name}</span>
|
||||
<span style={{ color: 'var(--cyber-red)', fontSize: 11 }}>[{s.target || 'both'}]</span>
|
||||
<span style={{ color: 'var(--dim-red)', fontSize: 11 }}>{s.description}</span>
|
||||
<span className="badge neutral">{s.target || 'both'}</span>
|
||||
<span style={{ color: 'var(--text-tertiary)', fontSize: 12 }}>{s.description}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -88,11 +136,11 @@ export default function Config({ api, theme, onThemeChange }) {
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ label, value }) {
|
||||
function FieldRow({ label, value }) {
|
||||
return (
|
||||
<div className="config-field">
|
||||
<span className="config-label">{label}:</span>
|
||||
<span className="config-value">{value || '-'}</span>
|
||||
<div className="field-row">
|
||||
<span className="field-label">{label}</span>
|
||||
<span className={`field-value ${!value ? 'empty' : ''}`}>{value || '—'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,114 +1,103 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function Dashboard({ tools, updates, api, onRescan }) {
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const [installLog, setInstallLog] = useState([])
|
||||
const { t, layout } = useI18n()
|
||||
const [activeSection, setActiveSection] = useState('tools')
|
||||
const [notifications, setNotifications] = useState([])
|
||||
|
||||
const installed = tools.filter(t => t.installed).length
|
||||
const installed = tools.filter(tool => tool.installed).length
|
||||
const total = tools.length
|
||||
const pct = total > 0 ? (installed / total) * 100 : 0
|
||||
const missing = tools.filter(t => !t.installed).map(t => t.Name || t.name)
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (missing.length === 0) return
|
||||
setInstalling(true)
|
||||
setInstallLog(prev => [...prev, { text: `Installing ${missing.length} tools...`, type: 'info' }])
|
||||
try {
|
||||
await api.installTools(missing)
|
||||
setInstallLog(prev => [...prev, { text: 'Install started. Rescan to see changes.', type: 'ok' }])
|
||||
const data = await api.runScan()
|
||||
const toolData = await api.getTools()
|
||||
onRescan(toolData.tools || [])
|
||||
} catch (err) {
|
||||
setInstallLog(prev => [...prev, { text: err.message, type: 'error' }])
|
||||
}
|
||||
setInstalling(false)
|
||||
const addNotif = (text, type) => {
|
||||
setNotifications(prev => [{ text, type, id: Date.now(), time: new Date() }, ...prev])
|
||||
}
|
||||
|
||||
const handleScan = async () => {
|
||||
await api.runScan()
|
||||
const data = await api.getTools()
|
||||
onRescan(data.tools || [])
|
||||
}
|
||||
const sections = [
|
||||
{ id: 'tools', label: t('dashboard.systemOverview') },
|
||||
{ id: 'notifications', label: t('dashboard.activityLog') },
|
||||
{ id: 'workflows', label: t('studio.workflows') },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="grid-2">
|
||||
<div style={{ overflow: 'auto', padding: '4px' }}>
|
||||
<div className="section-header">System</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ color: 'var(--text-main)' }}>{installed}/{total} tools installed</span>
|
||||
</div>
|
||||
|
||||
<div className="section-header">Installed Tools</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
{tools.map((t, i) => (
|
||||
<div key={i} className="tool-item">
|
||||
<span className={`tool-status ${t.installed ? 'ok' : 'missing'}`}>
|
||||
{t.installed ? '[OK]' : '[--]'}
|
||||
</span>
|
||||
<span className="tool-name">{t.Name || t.name}</span>
|
||||
{(t.Version || t.version) && (
|
||||
<span className="tool-version">{extractVersion(t.Version || t.version)}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="progress-bar" style={{ marginBottom: 16 }}>
|
||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
|
||||
{installing && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span className="loading-spinner"> Installing...</span>
|
||||
<div className="dashboard-layout">
|
||||
<div className="dashboard-tabs">
|
||||
{sections.map(s => (
|
||||
<div
|
||||
key={s.id}
|
||||
className={`dashboard-tab ${activeSection === s.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection(s.id)}
|
||||
>
|
||||
{s.label}
|
||||
{s.id === 'tools' && total > 0 && (
|
||||
<span className="tab-count">{installed}/{total}</span>
|
||||
)}
|
||||
{s.id === 'notifications' && notifications.length > 0 && (
|
||||
<span className="tab-count warn">{notifications.length}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{installLog.length > 0 && (
|
||||
<div>
|
||||
<div className="section-header">Install Log</div>
|
||||
{installLog.map((log, i) => (
|
||||
<div key={i} style={{
|
||||
color: log.type === 'error' ? 'var(--error)' :
|
||||
log.type === 'ok' ? 'var(--success)' : 'var(--text-dim)',
|
||||
fontSize: 12, padding: '2px 0'
|
||||
}}>
|
||||
{log.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ overflow: 'auto', padding: '4px' }}>
|
||||
<div className="section-header">Quick Actions</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 16 }}>
|
||||
<button onClick={handleInstall} disabled={installing || missing.length === 0}>
|
||||
[i] Install missing ({missing.length})
|
||||
</button>
|
||||
<button onClick={() => api.getUpdates().then(d => {})}> [u] Check updates</button>
|
||||
<button onClick={handleScan}>[s] Rescan system</button>
|
||||
<button onClick={() => api.configureMCP()}>[m] Configure MCP</button>
|
||||
</div>
|
||||
<div className="dashboard-content">
|
||||
{activeSection === 'tools' && (
|
||||
<div className="dashboard-tools">
|
||||
{tools.length === 0 ? (
|
||||
<div className="empty-state">{t('dashboard.noUpdateData')}</div>
|
||||
) : (
|
||||
<div className="tools-compact">
|
||||
{tools.map((tool, i) => {
|
||||
const name = tool.name || tool.Name
|
||||
const ver = extractVersion(tool.Version || tool.version)
|
||||
return (
|
||||
<div key={i} className="tool-compact-row">
|
||||
<span className={`badge sm ${tool.installed ? 'ok' : 'error'}`}>
|
||||
{tool.installed ? '\u2713' : '\u2717'}
|
||||
</span>
|
||||
<span className="tool-compact-name">{name}</span>
|
||||
{ver && <span className="tool-compact-ver">{ver}</span>}
|
||||
{tool.installed && <span className="tool-compact-installed">{t('dashboard.installed')}</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="section-header">Updates</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
{updates.length === 0 ? (
|
||||
<span style={{ color: 'var(--text-muted)' }}>No update data yet</span>
|
||||
) : updates.map((u, i) => (
|
||||
<div key={i} className="tool-item">
|
||||
<span className={`tool-status ${u.needsUpdate ? 'missing' : 'ok'}`}>
|
||||
{u.needsUpdate ? '[!!]' : '[OK]'}
|
||||
</span>
|
||||
<span className="tool-name">{u.tool}</span>
|
||||
{u.needsUpdate && (
|
||||
<span style={{ color: 'var(--warning)', fontSize: 11 }}>
|
||||
{u.current} → {u.latest}
|
||||
</span>
|
||||
)}
|
||||
{activeSection === 'notifications' && (
|
||||
<div className="dashboard-notifications">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="empty-state">{t('dashboard.noUpdateData')}</div>
|
||||
) : (
|
||||
notifications.map(n => (
|
||||
<div key={n.id} className={`notif-row notif-${n.type}`}>
|
||||
<span className="notif-time">
|
||||
{n.time.toLocaleTimeString(layout.locale, { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
||||
</span>
|
||||
<span className="notif-text">{n.text}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'workflows' && (
|
||||
<div className="dashboard-workflows">
|
||||
<div className="workflow-section">
|
||||
<div className="section-label">{t('studio.workflows')}</div>
|
||||
<div className="empty-state" style={{ padding: 20 }}>
|
||||
{t('studio.noWorkflow')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="workflow-section">
|
||||
<div className="section-label">{t('studio.activeAgents')}</div>
|
||||
<div className="empty-state" style={{ padding: 20 }}>
|
||||
{t('studio.noWorkflow')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function Shell({ api }) {
|
||||
const { t } = useI18n()
|
||||
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: t('shell.aiWelcome') }
|
||||
])
|
||||
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 +22,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 +46,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 || t('shell.noResponse') }])
|
||||
} catch (err) {
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }])
|
||||
setAiMessages(prev => [...prev, { role: 'ai', content: `${t('shell.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 }}>
|
||||
Terminal
|
||||
<span style={{ color: 'var(--dim-red)', fontWeight: 400, marginLeft: 12, fontSize: 11 }}>{cwd}</span>
|
||||
</div>
|
||||
<div className="terminal" style={{ flex: 1 }}>
|
||||
<div className="panel-header">
|
||||
<span className="panel-title">
|
||||
{t('shell.terminal')}
|
||||
<span className="panel-subtitle">{cwd}</span>
|
||||
</span>
|
||||
<button className="ghost sm" onClick={() => setShowAi(!showAi)}>
|
||||
{showAi ? t('shell.hideAi') : t('shell.aiAssistant')}
|
||||
</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 +110,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">{t('shell.aiAssistant')}</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..."
|
||||
placeholder={t('shell.askAi')}
|
||||
/>
|
||||
<button style={{ padding: '4px 8px' }} onClick={handleAiSend}>Send</button>
|
||||
<button className="sm" onClick={handleAiSend} disabled={!aiInput.trim()}>{t('shell.send')}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function Studio({ api }) {
|
||||
const { t, layout } = useI18n()
|
||||
const [messages, setMessages] = useState([
|
||||
{ role: 'ai', content: '>> Welcome to Studio! Chat with your AI assistant here.' },
|
||||
{ role: 'ai', content: '>> Configure agents and workflows from the sidebar.' },
|
||||
{ role: 'ai', content: t('studio.welcome') },
|
||||
{ role: 'ai', content: t('studio.configureHint') },
|
||||
])
|
||||
const [input, setInput] = useState('')
|
||||
const [sidebarPanel, setSidebarPanel] = useState('chat')
|
||||
@@ -17,16 +19,16 @@ export default function Studio({ api }) {
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || loading) return
|
||||
const text = input.trim()
|
||||
setMessages(prev => [...prev, { role: 'user', content: '>> ' + text }])
|
||||
setMessages(prev => [...prev, { role: 'user', content: text }])
|
||||
setInput('')
|
||||
setLoading(true)
|
||||
|
||||
api.runCommand(`echo "AI response simulation for: ${text}"`, '')
|
||||
.then(res => {
|
||||
setMessages(prev => [...prev, { role: 'ai', content: '>> ' + (res.output || res.error || 'No response') }])
|
||||
setMessages(prev => [...prev, { role: 'ai', content: res.output || res.error || t('studio.noResponse') }])
|
||||
})
|
||||
.catch(err => {
|
||||
setMessages(prev => [...prev, { role: 'ai', content: '[ERROR] ' + err.message }])
|
||||
setMessages(prev => [...prev, { role: 'ai', content: `${t('studio.error')}: ${err.message}` }])
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
@@ -38,88 +40,100 @@ export default function Studio({ api }) {
|
||||
}
|
||||
}
|
||||
|
||||
const sidebarItems = [
|
||||
{ id: 'chat', label: t('studio.chat'), icon: '#' },
|
||||
{ id: 'agents', label: t('studio.agents'), icon: '*' },
|
||||
{ id: 'workflows', label: t('studio.workflows'), icon: '~' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="split-horizontal">
|
||||
<div className="chat-container" style={{ flex: 1, borderRight: '1px solid var(--border-dim)' }}>
|
||||
<div style={{ padding: '12px 16px', borderBottom: '1px solid var(--border-dim)' }}>
|
||||
<div className="section-header" style={{ margin: 0 }}>
|
||||
Chat
|
||||
{loading && <span className="loading-spinner" style={{ marginLeft: 8 }}> thinking...</span>}
|
||||
</div>
|
||||
<div className="chat-layout" style={{ flex: 1, borderRight: '1px solid var(--border)' }}>
|
||||
<div className="panel-header">
|
||||
<span className="panel-title">
|
||||
{t('studio.chat')}
|
||||
{loading && <span className="spinner" />}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="chat-messages">
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className={`chat-message ${msg.role}`}>
|
||||
<div key={i} className={`message ${msg.role}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEnd} />
|
||||
</div>
|
||||
|
||||
<div className="chat-input-container">
|
||||
<div className="chat-input-bar">
|
||||
<input
|
||||
className="chat-input"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message... (/plan <goal> for workflows)"
|
||||
placeholder={t('studio.placeholder')}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button className="primary" onClick={handleSend} disabled={loading || !input.trim()}>
|
||||
Send
|
||||
{t('studio.send')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="split-right">
|
||||
<div className="sidebar-section">
|
||||
<div className="section-header">Studio</div>
|
||||
{['chat', 'agents', 'workflows'].map(panel => (
|
||||
<div className="sidebar-nav">
|
||||
{sidebarItems.map(item => (
|
||||
<div
|
||||
key={panel}
|
||||
className={`sidebar-item ${sidebarPanel === panel ? 'active' : ''}`}
|
||||
onClick={() => setSidebarPanel(panel)}
|
||||
key={item.id}
|
||||
className={`sidebar-tab ${sidebarPanel === item.id ? 'active' : ''}`}
|
||||
onClick={() => setSidebarPanel(item.id)}
|
||||
>
|
||||
[{panel === 'chat' ? '#' : panel === 'agents' ? '*' : '~'}] {panel.charAt(0).toUpperCase() + panel.slice(1)}
|
||||
<span style={{ fontFamily: 'var(--font-mono)', width: 16 }}>{item.icon}</span>
|
||||
{item.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: '1px solid var(--border-dim)', paddingTop: 12 }}>
|
||||
{sidebarPanel === 'chat' && (
|
||||
<div>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12, marginBottom: 8 }}>Commands</div>
|
||||
<div style={{ color: 'var(--dim-red)', fontSize: 12, fontFamily: 'var(--font-mono)' }}>
|
||||
/plan {'<goal>'}<br/>
|
||||
/help
|
||||
</div>
|
||||
{sidebarPanel === 'chat' && (
|
||||
<div>
|
||||
<div className="section-title">{t('studio.commands')}</div>
|
||||
<div style={{ fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--text-tertiary)' }}>
|
||||
{t('studio.planGoal')}<br />
|
||||
{t('studio.help')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sidebarPanel === 'agents' && (
|
||||
<div>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12, marginBottom: 8 }}>Active Agents</div>
|
||||
<div style={{ padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--text-main)', fontWeight: 600 }}>Crush</span>
|
||||
<span style={{ color: 'var(--text-muted)', marginLeft: 8, fontSize: 11 }}>[|| stopped]</span>
|
||||
</div>
|
||||
<div style={{ padding: '4px 0' }}>
|
||||
<span style={{ color: 'var(--text-main)', fontWeight: 600 }}>Claude Code</span>
|
||||
<span style={{ color: 'var(--text-muted)', marginLeft: 8, fontSize: 11 }}>[|| stopped]</span>
|
||||
{sidebarPanel === 'agents' && (
|
||||
<div>
|
||||
<div className="section-title">{t('studio.activeAgents')}</div>
|
||||
<div className="agent-card">
|
||||
<div className="agent-avatar">C</div>
|
||||
<div>
|
||||
<div className="agent-name">{t('studio.crush')}</div>
|
||||
<div className="agent-status">{t('studio.stopped')}</div>
|
||||
</div>
|
||||
<span className="badge neutral" style={{ marginLeft: 'auto' }}>{t('studio.inactive')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="agent-card">
|
||||
<div className="agent-avatar">CC</div>
|
||||
<div>
|
||||
<div className="agent-name">{t('studio.claudeCode')}</div>
|
||||
<div className="agent-status">{t('studio.stopped')}</div>
|
||||
</div>
|
||||
<span className="badge neutral" style={{ marginLeft: 'auto' }}>{t('studio.inactive')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sidebarPanel === 'workflows' && (
|
||||
<div>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>No active workflow.</div>
|
||||
<div style={{ color: 'var(--dim-red)', fontSize: 12, marginTop: 8 }}>
|
||||
Use /plan {'<goal>'} in chat to start.
|
||||
</div>
|
||||
{sidebarPanel === 'workflows' && (
|
||||
<div>
|
||||
<div className="section-title">{t('studio.workflows')}</div>
|
||||
<div className="empty-state">
|
||||
{t('studio.noWorkflow')}
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>{t('studio.usePlan')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
105
web/src/i18n/en.js
Normal file
105
web/src/i18n/en.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const en = {
|
||||
tabs: {
|
||||
dashboard: 'Dashboard',
|
||||
studio: 'Studio',
|
||||
shell: 'Shell',
|
||||
config: 'Config',
|
||||
},
|
||||
|
||||
header: {
|
||||
toolsInstalled: '{count} tools installed',
|
||||
updatesAvailable: 'Updates available',
|
||||
upToDate: 'Up to date',
|
||||
},
|
||||
|
||||
statusbar: {
|
||||
switchWindow: 'Switch window',
|
||||
sendMessage: 'Send message',
|
||||
newLine: 'New line',
|
||||
runCommand: 'Run command',
|
||||
commandHistory: 'Command history',
|
||||
},
|
||||
|
||||
dashboard: {
|
||||
systemOverview: 'System Overview',
|
||||
tools: 'tools',
|
||||
installed: 'Installed',
|
||||
missing: 'Missing',
|
||||
quickActions: 'Quick Actions',
|
||||
installMissing: 'Install missing',
|
||||
checkUpdates: 'Check for updates',
|
||||
rescanSystem: 'Rescan system',
|
||||
configureMCP: 'Configure MCP',
|
||||
updates: 'Updates',
|
||||
update: 'Update',
|
||||
latest: 'Latest',
|
||||
activityLog: 'Activity Log',
|
||||
noUpdateData: 'No update data yet.',
|
||||
installing: 'Installing {count} tools...',
|
||||
installStarted: 'Install started. Rescanning...',
|
||||
done: 'Done.',
|
||||
scanComplete: 'Scan complete.',
|
||||
updatesCount: '{count} updates available.',
|
||||
allUpToDate: 'All tools up to date.',
|
||||
mcpConfigured: 'MCP configured.',
|
||||
},
|
||||
|
||||
studio: {
|
||||
welcome: 'Welcome to Studio! Chat with your AI assistant here.',
|
||||
configureHint: 'Configure agents and workflows from the sidebar.',
|
||||
chat: 'Chat',
|
||||
agents: 'Agents',
|
||||
workflows: 'Workflows',
|
||||
placeholder: 'Type a message... (Enter to send)',
|
||||
send: 'Send',
|
||||
commands: 'Commands',
|
||||
planGoal: '/plan <goal>',
|
||||
help: '/help',
|
||||
activeAgents: 'Active Agents',
|
||||
crush: 'Crush',
|
||||
claudeCode: 'Claude Code',
|
||||
stopped: 'Stopped',
|
||||
inactive: 'Inactive',
|
||||
noWorkflow: 'No active workflow.',
|
||||
usePlan: 'Use /plan <goal> in chat to start.',
|
||||
noResponse: 'No response',
|
||||
error: 'Error',
|
||||
},
|
||||
|
||||
shell: {
|
||||
terminal: 'Terminal',
|
||||
hideAi: 'Hide AI',
|
||||
aiAssistant: 'AI Assistant',
|
||||
aiWelcome: 'I know your system inside out. Ask me anything.',
|
||||
askAi: 'Ask AI...',
|
||||
send: 'Send',
|
||||
noResponse: 'No response',
|
||||
error: 'Error',
|
||||
},
|
||||
|
||||
config: {
|
||||
profile: 'Profile',
|
||||
name: 'Name',
|
||||
pseudo: 'Pseudo',
|
||||
email: 'Email',
|
||||
editor: 'Editor',
|
||||
shell: 'Shell',
|
||||
defaultAi: 'Default AI',
|
||||
languages: 'Languages',
|
||||
loadingProfile: 'Loading profile...',
|
||||
notSet: 'Not set',
|
||||
aiProviders: 'AI Providers',
|
||||
active: 'Active',
|
||||
keyConfigured: 'Key configured',
|
||||
noKey: 'No key',
|
||||
theme: 'Theme',
|
||||
skills: 'Skills',
|
||||
noSkills: 'No skills installed.',
|
||||
runSkillsInit: 'Run muyue skills init',
|
||||
language: 'Language',
|
||||
keyboardLayout: 'Keyboard Layout',
|
||||
target: 'Target',
|
||||
},
|
||||
}
|
||||
|
||||
export default en
|
||||
105
web/src/i18n/fr.js
Normal file
105
web/src/i18n/fr.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const fr = {
|
||||
tabs: {
|
||||
dashboard: 'Tableau de bord',
|
||||
studio: 'Studio',
|
||||
shell: 'Terminal',
|
||||
config: 'Configuration',
|
||||
},
|
||||
|
||||
header: {
|
||||
toolsInstalled: '{count} outils install\u00e9s',
|
||||
updatesAvailable: 'Mises \u00e0 jour disponibles',
|
||||
upToDate: '\u00c0 jour',
|
||||
},
|
||||
|
||||
statusbar: {
|
||||
switchWindow: 'Changer de fen\u00eatre',
|
||||
sendMessage: 'Envoyer le message',
|
||||
newLine: 'Nouvelle ligne',
|
||||
runCommand: 'Ex\u00e9cuter',
|
||||
commandHistory: 'Historique',
|
||||
},
|
||||
|
||||
dashboard: {
|
||||
systemOverview: 'Vue d\u2019ensemble du syst\u00e8me',
|
||||
tools: 'outils',
|
||||
installed: 'Install\u00e9',
|
||||
missing: 'Manquant',
|
||||
quickActions: 'Actions rapides',
|
||||
installMissing: 'Installer les manquants',
|
||||
checkUpdates: 'V\u00e9rifier les mises \u00e0 jour',
|
||||
rescanSystem: 'Rescanner le syst\u00e8me',
|
||||
configureMCP: 'Configurer MCP',
|
||||
updates: 'Mises \u00e0 jour',
|
||||
update: 'Mise \u00e0 jour',
|
||||
latest: '\u00c0 jour',
|
||||
activityLog: 'Journal d\u2019activit\u00e9',
|
||||
noUpdateData: 'Aucune donn\u00e9e de mise \u00e0 jour.',
|
||||
installing: 'Installation de {count} outils...',
|
||||
installStarted: 'Installation lanc\u00e9e. Rescan en cours...',
|
||||
done: 'Termin\u00e9.',
|
||||
scanComplete: 'Scan termin\u00e9.',
|
||||
updatesCount: '{count} mises \u00e0 jour disponibles.',
|
||||
allUpToDate: 'Tous les outils sont \u00e0 jour.',
|
||||
mcpConfigured: 'MCP configur\u00e9.',
|
||||
},
|
||||
|
||||
studio: {
|
||||
welcome: 'Bienvenue dans Studio ! Discutez avec votre assistant IA ici.',
|
||||
configureHint: 'Configurez les agents et workflows depuis la barre lat\u00e9rale.',
|
||||
chat: 'Chat',
|
||||
agents: 'Agents',
|
||||
workflows: 'Workflows',
|
||||
placeholder: 'Tapez un message... (Entr\u00e9e pour envoyer)',
|
||||
send: 'Envoyer',
|
||||
commands: 'Commandes',
|
||||
planGoal: '/plan <objectif>',
|
||||
help: '/help',
|
||||
activeAgents: 'Agents actifs',
|
||||
crush: 'Crush',
|
||||
claudeCode: 'Claude Code',
|
||||
stopped: 'Arr\u00eat\u00e9',
|
||||
inactive: 'Inactif',
|
||||
noWorkflow: 'Aucun workflow actif.',
|
||||
usePlan: 'Utilisez /plan <objectif> dans le chat pour d\u00e9marrer.',
|
||||
noResponse: 'Pas de r\u00e9ponse',
|
||||
error: 'Erreur',
|
||||
},
|
||||
|
||||
shell: {
|
||||
terminal: 'Terminal',
|
||||
hideAi: 'Masquer IA',
|
||||
aiAssistant: 'Assistant IA',
|
||||
aiWelcome: 'Je connais votre syst\u00e8me sur le bout des doigts. Demandez-moi n\u2019importe quoi.',
|
||||
askAi: 'Demander \u00e0 l\u2019IA...',
|
||||
send: 'Envoyer',
|
||||
noResponse: 'Pas de r\u00e9ponse',
|
||||
error: 'Erreur',
|
||||
},
|
||||
|
||||
config: {
|
||||
profile: 'Profil',
|
||||
name: 'Nom',
|
||||
pseudo: 'Pseudo',
|
||||
email: 'Email',
|
||||
editor: '\u00c9diteur',
|
||||
shell: 'Shell',
|
||||
defaultAi: 'IA par d\u00e9faut',
|
||||
languages: 'Langages',
|
||||
loadingProfile: 'Chargement du profil...',
|
||||
notSet: 'Non d\u00e9fini',
|
||||
aiProviders: 'Fournisseurs IA',
|
||||
active: 'Actif',
|
||||
keyConfigured: 'Cl\u00e9 configur\u00e9e',
|
||||
noKey: 'Pas de cl\u00e9',
|
||||
theme: 'Th\u00e8me',
|
||||
skills: 'Comp\u00e9tences',
|
||||
noSkills: 'Aucune comp\u00e9tence install\u00e9e.',
|
||||
runSkillsInit: 'Ex\u00e9cutez muyue skills init',
|
||||
language: 'Langue',
|
||||
keyboardLayout: 'Disposition du clavier',
|
||||
target: 'Cible',
|
||||
},
|
||||
}
|
||||
|
||||
export default fr
|
||||
101
web/src/i18n/index.jsx
Normal file
101
web/src/i18n/index.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import en from './en'
|
||||
import fr from './fr'
|
||||
import { getLayout, getLayoutList } from './keyboards'
|
||||
import api from '../api/client'
|
||||
|
||||
const translations = { en, fr }
|
||||
|
||||
const STORAGE_KEY_LANG = 'muyue-language'
|
||||
const STORAGE_KEY_KBD = 'muyue-keyboard'
|
||||
|
||||
const I18nContext = createContext(null)
|
||||
|
||||
function resolveLocale(layout) {
|
||||
const l = getLayout(layout)
|
||||
return l.locale
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }) {
|
||||
const [language, setLanguageState] = useState(() => localStorage.getItem(STORAGE_KEY_LANG) || 'fr')
|
||||
const [keyboard, setKeyboardState] = useState(() => localStorage.getItem(STORAGE_KEY_KBD) || 'azerty')
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const pendingSave = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
api.getConfig()
|
||||
.then(d => {
|
||||
const prefs = d.profile?.preferences
|
||||
if (prefs?.language) setLanguageState(prefs.language)
|
||||
if (prefs?.keyboard_layout) setKeyboardState(prefs.keyboard_layout)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoaded(true))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded) return
|
||||
if (pendingSave.current) clearTimeout(pendingSave.current)
|
||||
pendingSave.current = setTimeout(() => {
|
||||
api.savePreferences({ language, keyboard_layout: keyboard }).catch(() => {})
|
||||
}, 500)
|
||||
return () => { if (pendingSave.current) clearTimeout(pendingSave.current) }
|
||||
}, [language, keyboard, loaded])
|
||||
|
||||
const setLanguage = useCallback((lang) => {
|
||||
setLanguageState(lang)
|
||||
localStorage.setItem(STORAGE_KEY_LANG, lang)
|
||||
}, [])
|
||||
|
||||
const setKeyboard = useCallback((kbd) => {
|
||||
setKeyboardState(kbd)
|
||||
localStorage.setItem(STORAGE_KEY_KBD, kbd)
|
||||
}, [])
|
||||
|
||||
const layout = useMemo(() => getLayout(keyboard), [keyboard])
|
||||
|
||||
const t = useCallback((key, params) => {
|
||||
const dict = translations[language] || translations.fr
|
||||
const keys = key.split('.')
|
||||
let value = dict
|
||||
for (const k of keys) {
|
||||
if (value == null) return key
|
||||
value = value[k]
|
||||
}
|
||||
if (typeof value !== 'string') return key
|
||||
if (params) {
|
||||
return Object.entries(params).reduce((str, [k, v]) => str.replace(`{${k}}`, v), value)
|
||||
}
|
||||
return value
|
||||
}, [language])
|
||||
|
||||
const clockLocale = useMemo(() => resolveLocale(keyboard), [keyboard])
|
||||
|
||||
const contextValue = useMemo(() => ({
|
||||
language,
|
||||
keyboard,
|
||||
layout,
|
||||
setLanguage,
|
||||
setKeyboard,
|
||||
t,
|
||||
clockLocale,
|
||||
layouts: getLayoutList(),
|
||||
}), [language, keyboard, layout, t, clockLocale])
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const ctx = useContext(I18nContext)
|
||||
if (!ctx) throw new Error('useI18n must be used within I18nProvider')
|
||||
return ctx
|
||||
}
|
||||
|
||||
export const LANGUAGES = [
|
||||
{ id: 'fr', name: 'Fran\u00e7ais' },
|
||||
{ id: 'en', name: 'English' },
|
||||
]
|
||||
61
web/src/i18n/keyboards.js
Normal file
61
web/src/i18n/keyboards.js
Normal file
@@ -0,0 +1,61 @@
|
||||
export const LAYOUTS = {
|
||||
qwerty: {
|
||||
id: 'qwerty',
|
||||
name: 'QWERTY',
|
||||
locale: 'en-US',
|
||||
keys: {
|
||||
tab1: '1',
|
||||
tab2: '2',
|
||||
tab3: '3',
|
||||
tab4: '4',
|
||||
ctrl: 'Ctrl',
|
||||
enter: 'Enter',
|
||||
shift: 'Shift',
|
||||
up: '\u2191',
|
||||
down: '\u2193',
|
||||
range: '1-4',
|
||||
},
|
||||
},
|
||||
azerty: {
|
||||
id: 'azerty',
|
||||
name: 'AZERTY',
|
||||
locale: 'fr-FR',
|
||||
keys: {
|
||||
tab1: '&',
|
||||
tab2: '\u00e9',
|
||||
tab3: '"',
|
||||
tab4: "'",
|
||||
ctrl: 'Ctrl',
|
||||
enter: 'Entr\u00e9e',
|
||||
shift: 'Maj',
|
||||
up: '\u2191',
|
||||
down: '\u2193',
|
||||
range: '&-\u00e9-"-\'',
|
||||
},
|
||||
},
|
||||
qwertz: {
|
||||
id: 'qwertz',
|
||||
name: 'QWERTZ',
|
||||
locale: 'de-DE',
|
||||
keys: {
|
||||
tab1: '1',
|
||||
tab2: '2',
|
||||
tab3: '3',
|
||||
tab4: '4',
|
||||
ctrl: 'Strg',
|
||||
enter: 'Enter',
|
||||
shift: 'Umschalt',
|
||||
up: '\u2191',
|
||||
down: '\u2193',
|
||||
range: '1-4',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function getLayout(id) {
|
||||
return LAYOUTS[id] || LAYOUTS.azerty
|
||||
}
|
||||
|
||||
export function getLayoutList() {
|
||||
return Object.values(LAYOUTS)
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { I18nProvider } from './i18n'
|
||||
import './styles/global.css'
|
||||
import App from './components/App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<I18nProvider>
|
||||
<App />
|
||||
</I18nProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
@@ -1,576 +1,401 @@
|
||||
:root {
|
||||
--bg-void: #0A0A0C;
|
||||
--bg: #0A0A0C;
|
||||
--bg-base: #0F0D10;
|
||||
--bg-surface: #161218;
|
||||
--bg-panel: #1C1719;
|
||||
--bg-elevated: #1C1719;
|
||||
--bg-card: #221B1E;
|
||||
--bg-input: #2A2225;
|
||||
--bg-hover: #332528;
|
||||
|
||||
--cyber-red: #FF0033;
|
||||
--cyber-red-dark: #8B0020;
|
||||
--cyber-red-deep: #5C0015;
|
||||
--cyber-pink: #FF1A5E;
|
||||
--cyber-rose: #FF4D6D;
|
||||
--neon-red: #FF1744;
|
||||
--bright-red: #FF5252;
|
||||
--dim-red: #6B2033;
|
||||
--muted-red: #4A1525;
|
||||
--accent: #FF0033;
|
||||
--accent-dark: #8B0020;
|
||||
--accent-deep: #5C0015;
|
||||
--accent-light: #FF1A5E;
|
||||
--accent-muted: #FF4D6D;
|
||||
--accent-bright: #FF1744;
|
||||
--accent-soft: #FF5252;
|
||||
--accent-dim: #6B2033;
|
||||
--accent-bg: #4A1525;
|
||||
|
||||
--text-bright: #EAE0E2;
|
||||
--text-main: #D4C4C8;
|
||||
--text-dim: #8A7A7E;
|
||||
--text-muted: #5A4F52;
|
||||
--text-primary: #EAE0E2;
|
||||
--text-secondary: #D4C4C8;
|
||||
--text-tertiary: #8A7A7E;
|
||||
--text-disabled: #5A4F52;
|
||||
|
||||
--success: #00E676;
|
||||
--warning: #FFD740;
|
||||
--error: #FF1744;
|
||||
--info: #448AFF;
|
||||
|
||||
--border-dim: #2A1F22;
|
||||
--border-red: #FF003344;
|
||||
--border-red-full: #FF0033;
|
||||
--border: #2A1F22;
|
||||
--border-accent: #FF003344;
|
||||
--border-accent-full: #FF0033;
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace;
|
||||
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
|
||||
--header-h: 48px;
|
||||
--footer-h: 32px;
|
||||
--tab-h: 40px;
|
||||
--header-h: 52px;
|
||||
--sidebar-w: 280px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
html, body, #root { height: 100%; width: 100%; overflow: hidden; }
|
||||
|
||||
body {
|
||||
background: var(--bg-void);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-ui);
|
||||
font-size: 13px;
|
||||
background: var(--bg);
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
user-select: none;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::selection { background: var(--accent); color: #fff; }
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--accent-dim); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--accent-dark); }
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--dim-red);
|
||||
border-radius: 3px;
|
||||
}
|
||||
a { color: var(--accent); text-decoration: none; cursor: pointer; }
|
||||
a:hover { color: var(--accent-light); }
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--cyber-red-dark);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--cyber-red);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--cyber-red);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--cyber-pink);
|
||||
button {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
button:hover:not(:disabled) { background: var(--bg-hover); border-color: var(--accent-dark); color: var(--text-primary); }
|
||||
button:active:not(:disabled) { transform: scale(0.97); }
|
||||
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
button.primary:hover:not(:disabled) { background: var(--accent-bright); border-color: var(--accent-bright); }
|
||||
button.ghost { background: transparent; border-color: transparent; color: var(--text-tertiary); }
|
||||
button.ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); }
|
||||
button.sm { font-size: 12px; padding: 4px 10px; }
|
||||
|
||||
input, textarea {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border-dim);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
input:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--border-accent); }
|
||||
input::placeholder { color: var(--text-disabled); }
|
||||
|
||||
input:focus, textarea:focus {
|
||||
border-color: var(--cyber-red);
|
||||
box-shadow: 0 0 0 2px var(--border-red);
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-dim);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-main);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--cyber-red-dark);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--cyber-red);
|
||||
color: #fff;
|
||||
border-color: var(--cyber-red);
|
||||
}
|
||||
|
||||
button.primary:hover {
|
||||
background: var(--neon-red);
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-layout { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
|
||||
|
||||
.header {
|
||||
height: var(--header-h);
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 16px;
|
||||
padding: 0 20px;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
color: var(--cyber-red);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.header-brand { display: flex; align-items: center; gap: 8px; }
|
||||
.header-logo { font-family: var(--font-mono); font-weight: 900; font-size: 18px; color: var(--accent); letter-spacing: 3px; user-select: none; }
|
||||
.header-version { font-size: 11px; color: var(--accent-dim); font-family: var(--font-mono); }
|
||||
|
||||
.header-version {
|
||||
font-size: 11px;
|
||||
color: var(--dim-red);
|
||||
}
|
||||
.header-nav { display: flex; gap: 4px; margin-left: 32px; }
|
||||
|
||||
.header-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.header-tab {
|
||||
padding: 6px 16px;
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
font-size: 12px;
|
||||
.nav-tab {
|
||||
padding: 8px 18px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
color: var(--text-dim);
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.15s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.header-tab:hover {
|
||||
color: var(--text-main);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.header-tab.active {
|
||||
color: #fff;
|
||||
background: var(--cyber-red);
|
||||
border-color: var(--cyber-red);
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-status {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
user-select: none;
|
||||
}
|
||||
.nav-tab:hover { color: var(--text-primary); background: var(--bg-card); }
|
||||
.nav-tab.active { color: #fff; background: var(--accent); }
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.header-spacer { flex: 1; }
|
||||
|
||||
.status-dot.ok { background: var(--success); }
|
||||
.status-dot.warn { background: var(--warning); }
|
||||
.status-dot.error { background: var(--error); }
|
||||
.status-dot.off { background: var(--text-muted); }
|
||||
.header-indicators { display: flex; align-items: center; gap: 12px; }
|
||||
.indicator { width: 8px; height: 8px; border-radius: 50%; transition: background 0.3s; }
|
||||
.indicator.ok { background: var(--success); }
|
||||
.indicator.warn { background: var(--warning); }
|
||||
.indicator.error { background: var(--error); }
|
||||
.indicator.off { background: var(--text-disabled); }
|
||||
|
||||
.header-clock {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--cyber-red);
|
||||
font-weight: 700;
|
||||
}
|
||||
.header-clock { font-family: var(--font-mono); font-size: 12px; color: var(--accent); font-weight: 600; }
|
||||
|
||||
.header-date {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.content { flex: 1; overflow: hidden; }
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: var(--footer-h);
|
||||
.statusbar {
|
||||
height: 28px;
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--border-dim);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer-shortcuts {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
|
||||
.footer-shortcuts kbd {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.footer-version {
|
||||
font-size: 11px;
|
||||
color: var(--dim-red);
|
||||
}
|
||||
|
||||
.footer-update {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.footer-update.available {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.footer-update.uptodate {
|
||||
color: var(--success);
|
||||
.statusbar-left, .statusbar-right { display: flex; align-items: center; gap: 12px; }
|
||||
.statusbar-shortcut { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.statusbar-shortcut kbd {
|
||||
display: inline-block; padding: 1px 5px; border-radius: 3px;
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
font-family: var(--font-mono); font-size: 10px; color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--dim-red);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: var(--cyber-red);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
color: var(--cyber-red);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.section-header::before {
|
||||
content: '■';
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.tool-status.ok { color: var(--success); }
|
||||
.tool-status.missing { color: var(--error); }
|
||||
|
||||
.tool-name {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.tool-version {
|
||||
color: var(--dim-red);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: var(--bg-input);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--cyber-red), var(--cyber-pink));
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.split-horizontal {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.split-left {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.split-right {
|
||||
width: 320px;
|
||||
border-left: 1px solid var(--border-dim);
|
||||
background: var(--bg-surface);
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chat-message.ai {
|
||||
background: var(--bg-card);
|
||||
border-left: 3px solid var(--cyber-red);
|
||||
}
|
||||
|
||||
.chat-message.user {
|
||||
background: var(--muted-red);
|
||||
border-left: 3px solid var(--cyber-rose);
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
.card:hover { border-color: var(--accent-dim); }
|
||||
.card-header {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
transition: all 0.15s;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
margin-bottom: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
background: var(--cyber-red);
|
||||
color: #fff;
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 99px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge.ok { background: rgba(0,230,118,0.15); color: var(--success); }
|
||||
.badge.error { background: rgba(255,23,68,0.15); color: var(--error); }
|
||||
.badge.warn { background: rgba(255,215,64,0.15); color: var(--warning); }
|
||||
.badge.info { background: rgba(68,138,255,0.15); color: var(--info); }
|
||||
.badge.neutral { background: var(--bg-hover); color: var(--text-tertiary); }
|
||||
.badge.accent { background: var(--accent-bg); color: var(--accent); }
|
||||
|
||||
.terminal-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
background: var(--bg-void);
|
||||
}
|
||||
|
||||
.terminal-input-row {
|
||||
display: flex;
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-input);
|
||||
border-top: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.terminal-prompt {
|
||||
color: var(--success);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.config-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.config-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.config-label {
|
||||
width: 140px;
|
||||
color: var(--text-dim);
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.chip:hover { border-color: var(--accent-dark); background: var(--bg-hover); }
|
||||
.chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
|
||||
.config-value {
|
||||
color: var(--text-main);
|
||||
.progress { height: 6px; background: var(--bg-input); border-radius: 3px; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-light)); border-radius: 3px; transition: width 0.4s ease; }
|
||||
|
||||
.tool-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
}
|
||||
.tool-row:last-child { border-bottom: none; }
|
||||
.tool-name { flex: 1; color: var(--text-primary); font-weight: 500; }
|
||||
.tool-version { color: var(--text-tertiary); font-size: 12px; font-family: var(--font-mono); }
|
||||
|
||||
@keyframes glitch {
|
||||
0% { transform: translate(0); }
|
||||
20% { transform: translate(-2px, 2px); }
|
||||
40% { transform: translate(-2px, -2px); }
|
||||
60% { transform: translate(2px, 2px); }
|
||||
80% { transform: translate(2px, -2px); }
|
||||
100% { transform: translate(0); }
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; height: 100%; padding: 20px; overflow: auto; }
|
||||
.split-horizontal { display: flex; height: 100%; }
|
||||
.split-right { width: var(--sidebar-w); border-left: 1px solid var(--border); background: var(--bg-surface); overflow: auto; padding: 16px; }
|
||||
|
||||
.chat-layout { display: flex; flex-direction: column; height: 100%; }
|
||||
.chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.message {
|
||||
max-width: 80%;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
.message.user { align-self: flex-end; background: var(--accent-bg); color: var(--text-primary); border-bottom-right-radius: 4px; }
|
||||
.message.ai { align-self: flex-start; background: var(--bg-card); color: var(--text-primary); border-bottom-left-radius: 4px; border-left: 3px solid var(--accent); }
|
||||
.chat-input-bar { display: flex; gap: 8px; padding: 16px 20px; border-top: 1px solid var(--border); background: var(--bg-surface); }
|
||||
.chat-input-bar input { flex: 1; }
|
||||
|
||||
.sidebar-nav { display: flex; flex-direction: column; gap: 2px; margin-bottom: 20px; }
|
||||
.sidebar-tab {
|
||||
display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: var(--radius);
|
||||
font-size: 13px; color: var(--text-tertiary); cursor: pointer; transition: all 0.15s;
|
||||
}
|
||||
.sidebar-tab:hover { background: var(--bg-card); color: var(--text-primary); }
|
||||
.sidebar-tab.active { background: var(--accent); color: #fff; font-weight: 600; }
|
||||
|
||||
.terminal { display: flex; flex-direction: column; height: 100%; background: var(--bg); }
|
||||
.terminal-output { flex: 1; padding: 16px; font-family: var(--font-mono); font-size: 13px; line-height: 1.6; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
|
||||
.terminal-line { margin-bottom: 2px; }
|
||||
.terminal-line.cmd { color: var(--accent-dim); }
|
||||
.terminal-line.out { color: var(--text-primary); }
|
||||
.terminal-line.err { color: var(--error); }
|
||||
.terminal-input-bar { display: flex; align-items: center; padding: 10px 16px; background: var(--bg-surface); border-top: 1px solid var(--border); gap: 8px; }
|
||||
.terminal-prompt { color: var(--success); font-family: var(--font-mono); font-weight: 700; font-size: 14px; flex-shrink: 0; }
|
||||
.terminal-input { flex: 1; background: transparent; border: none; outline: none; color: var(--text-primary); font-family: var(--font-mono); font-size: 13px; padding: 0; }
|
||||
.terminal-input:focus { box-shadow: none; border-color: transparent; }
|
||||
|
||||
.config-layout { max-width: 840px; margin: 0 auto; padding: 24px; overflow-y: auto; height: 100%; }
|
||||
.config-section { margin-bottom: 28px; }
|
||||
.config-section-title {
|
||||
font-size: 12px; font-weight: 700; color: var(--accent); text-transform: uppercase;
|
||||
letter-spacing: 1px; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.field-row { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); gap: 12px; }
|
||||
.field-row:last-child { border-bottom: none; }
|
||||
.field-label { width: 140px; flex-shrink: 0; color: var(--text-tertiary); font-size: 13px; }
|
||||
.field-value { color: var(--text-primary); font-size: 14px; flex: 1; }
|
||||
.field-value.empty { color: var(--text-disabled); font-style: italic; }
|
||||
|
||||
.provider-card {
|
||||
background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 14px 16px; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.provider-card:hover { border-color: var(--accent-dim); }
|
||||
.provider-info { display: flex; flex-direction: column; gap: 4px; }
|
||||
.provider-name { font-weight: 600; color: var(--text-primary); font-size: 14px; }
|
||||
.provider-meta { display: flex; gap: 12px; font-size: 12px; color: var(--text-tertiary); font-family: var(--font-mono); }
|
||||
|
||||
.theme-picker { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.theme-swatch {
|
||||
width: 48px; height: 48px; border-radius: var(--radius); border: 2px solid var(--border);
|
||||
cursor: pointer; transition: all 0.15s; position: relative;
|
||||
}
|
||||
.theme-swatch:hover { transform: scale(1.1); border-color: var(--accent-dim); }
|
||||
.theme-swatch.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--border-accent); }
|
||||
.theme-swatch.active::after {
|
||||
content: '\2713'; position: absolute; inset: 0; display: flex; align-items: center;
|
||||
justify-content: center; color: #fff; font-size: 18px; font-weight: 700; text-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
@keyframes scanline {
|
||||
0% { top: -10%; }
|
||||
100% { top: 110%; }
|
||||
.section-title { font-size: 12px; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; }
|
||||
.actions-stack { display: flex; flex-direction: column; gap: 6px; }
|
||||
|
||||
.agent-card { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: var(--radius); background: var(--bg-card); margin-bottom: 6px; }
|
||||
.agent-avatar {
|
||||
width: 32px; height: 32px; border-radius: 50%; background: var(--accent-bg); color: var(--accent);
|
||||
display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; flex-shrink: 0;
|
||||
}
|
||||
.agent-name { font-weight: 600; color: var(--text-primary); font-size: 13px; }
|
||||
.agent-status { font-size: 11px; color: var(--text-tertiary); margin-top: 2px; }
|
||||
|
||||
.ai-panel { width: 320px; border-left: 1px solid var(--border); background: var(--bg-surface); display: flex; flex-direction: column; }
|
||||
.ai-panel-header { padding: 12px 16px; border-bottom: 1px solid var(--border); font-weight: 700; font-size: 13px; color: var(--accent); }
|
||||
.ai-panel-messages { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.ai-message { padding: 8px 12px; border-radius: var(--radius); font-size: 13px; line-height: 1.5; word-break: break-word; }
|
||||
.ai-message.ai { background: var(--bg-card); border-left: 3px solid var(--accent); }
|
||||
.ai-message.user { background: var(--accent-bg); border-left: 3px solid var(--accent-muted); }
|
||||
.ai-panel-input { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--border); }
|
||||
.ai-panel-input input { flex: 1; font-size: 13px; padding: 6px 10px; }
|
||||
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; color: var(--text-disabled); font-size: 13px; text-align: center; gap: 8px; }
|
||||
|
||||
.dashboard-layout { display: flex; flex-direction: column; height: 100%; }
|
||||
.dashboard-tabs {
|
||||
display: flex; gap: 0; border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-surface); flex-shrink: 0;
|
||||
}
|
||||
.dashboard-tab {
|
||||
padding: 10px 24px; font-size: 13px; font-weight: 600;
|
||||
color: var(--text-tertiary); cursor: pointer; transition: all 0.15s;
|
||||
display: flex; align-items: center; gap: 8px; border-bottom: 2px solid transparent;
|
||||
user-select: none;
|
||||
}
|
||||
.dashboard-tab:hover { color: var(--text-primary); background: var(--bg-card); }
|
||||
.dashboard-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
.tab-count {
|
||||
font-size: 10px; padding: 1px 6px; border-radius: 99px;
|
||||
background: var(--bg-card); color: var(--text-tertiary); font-family: var(--font-mono);
|
||||
}
|
||||
.tab-count.warn { background: rgba(255,215,64,0.15); color: var(--warning); }
|
||||
|
||||
.dashboard-content { flex: 1; overflow-y: auto; }
|
||||
|
||||
.dashboard-tools { padding: 16px 24px; }
|
||||
.tools-compact { display: flex; flex-direction: column; gap: 2px; }
|
||||
.tool-compact-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 6px 12px; border-radius: var(--radius);
|
||||
font-size: 13px; transition: background 0.1s;
|
||||
}
|
||||
.tool-compact-row:hover { background: var(--bg-card); }
|
||||
.badge.sm { padding: 1px 5px; font-size: 10px; }
|
||||
.tool-compact-name { color: var(--text-primary); font-weight: 500; flex: 1; }
|
||||
.tool-compact-ver { color: var(--text-tertiary); font-size: 11px; font-family: var(--font-mono); }
|
||||
.tool-compact-installed { color: var(--success); font-size: 11px; font-family: var(--font-mono); opacity: 0.7; }
|
||||
|
||||
.dashboard-notifications { padding: 16px 24px; }
|
||||
.notif-row {
|
||||
display: flex; align-items: flex-start; gap: 12px;
|
||||
padding: 8px 12px; border-radius: var(--radius); margin-bottom: 4px;
|
||||
}
|
||||
.notif-row:hover { background: var(--bg-card); }
|
||||
.notif-time { color: var(--text-disabled); font-size: 11px; font-family: var(--font-mono); flex-shrink: 0; padding-top: 1px; }
|
||||
.notif-text { font-size: 13px; color: var(--text-secondary); }
|
||||
.notif-info .notif-text { color: var(--info); }
|
||||
.notif-ok .notif-text { color: var(--success); }
|
||||
.notif-warn .notif-text { color: var(--warning); }
|
||||
.notif-error .notif-text { color: var(--error); }
|
||||
|
||||
.dashboard-workflows { padding: 16px 24px; display: flex; flex-direction: column; gap: 24px; }
|
||||
.workflow-section { }
|
||||
.section-label {
|
||||
font-size: 11px; font-weight: 700; color: var(--accent); text-transform: uppercase;
|
||||
letter-spacing: 1px; margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@keyframes typewriter {
|
||||
from { width: 0; }
|
||||
to { width: 100%; }
|
||||
.panel-header {
|
||||
display: flex; align-items: center; justify-content: space-between; padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--border); background: var(--bg-surface);
|
||||
}
|
||||
.panel-title { font-weight: 700; font-size: 13px; color: var(--accent); display: flex; align-items: center; gap: 8px; }
|
||||
.panel-subtitle { font-weight: 400; font-size: 12px; color: var(--text-tertiary); margin-left: 8px; }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.tab-transition {
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.glitch-text {
|
||||
animation: glitch 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
animation: pulse 1s infinite;
|
||||
color: var(--cyber-red);
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.spinner { width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.6s linear infinite; display: inline-block; vertical-align: middle; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.fade-in { animation: fadeIn 0.2s ease-out; }
|
||||
|
||||
@@ -1,43 +1,33 @@
|
||||
const defaultTheme = {
|
||||
name: 'Cyberpunk Red',
|
||||
colors: {
|
||||
bgVoid: '#0A0A0C',
|
||||
bg: '#0A0A0C',
|
||||
bgBase: '#0F0D10',
|
||||
bgSurface: '#161218',
|
||||
bgPanel: '#1C1719',
|
||||
bgElevated: '#1C1719',
|
||||
bgCard: '#221B1E',
|
||||
bgInput: '#2A2225',
|
||||
bgHover: '#332528',
|
||||
cyberRed: '#FF0033',
|
||||
cyberRedDark: '#8B0020',
|
||||
cyberRedDeep: '#5C0015',
|
||||
cyberPink: '#FF1A5E',
|
||||
cyberRose: '#FF4D6D',
|
||||
neonRed: '#FF1744',
|
||||
brightRed: '#FF5252',
|
||||
dimRed: '#6B2033',
|
||||
mutedRed: '#4A1525',
|
||||
textBright: '#EAE0E2',
|
||||
textMain: '#D4C4C8',
|
||||
textDim: '#8A7A7E',
|
||||
textMuted: '#5A4F52',
|
||||
accent: '#FF0033',
|
||||
accentDark: '#8B0020',
|
||||
accentDeep: '#5C0015',
|
||||
accentLight: '#FF1A5E',
|
||||
accentMuted: '#FF4D6D',
|
||||
accentBright: '#FF1744',
|
||||
accentSoft: '#FF5252',
|
||||
accentDim: '#6B2033',
|
||||
accentBg: '#4A1525',
|
||||
textPrimary: '#EAE0E2',
|
||||
textSecondary: '#D4C4C8',
|
||||
textTertiary: '#8A7A7E',
|
||||
textDisabled: '#5A4F52',
|
||||
success: '#00E676',
|
||||
warning: '#FFD740',
|
||||
error: '#FF1744',
|
||||
borderDim: '#2A1F22',
|
||||
borderRed: '#FF003344',
|
||||
borderRedFull: '#FF0033',
|
||||
},
|
||||
fonts: {
|
||||
mono: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
|
||||
ui: "-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||
},
|
||||
borderRadius: '8px',
|
||||
animations: {
|
||||
glitch: true,
|
||||
scanline: true,
|
||||
typewriter: true,
|
||||
pulse: true,
|
||||
info: '#448AFF',
|
||||
border: '#2A1F22',
|
||||
borderAccent: '#FF003344',
|
||||
borderAccentFull: '#FF0033',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -48,15 +38,15 @@ const themes = {
|
||||
name: 'Cyberpunk Pink',
|
||||
colors: {
|
||||
...defaultTheme.colors,
|
||||
cyberRed: '#FF1A8C',
|
||||
cyberRedDark: '#8B1050',
|
||||
cyberRedDeep: '#5C0A35',
|
||||
cyberPink: '#FF4DAE',
|
||||
cyberRose: '#FF6DC2',
|
||||
neonRed: '#FF1A8C',
|
||||
brightRed: '#FF6DC2',
|
||||
dimRed: '#6B2050',
|
||||
mutedRed: '#4A1535',
|
||||
accent: '#FF1A8C',
|
||||
accentDark: '#8B1050',
|
||||
accentDeep: '#5C0A35',
|
||||
accentLight: '#FF4DAE',
|
||||
accentMuted: '#FF6DC2',
|
||||
accentBright: '#FF1A8C',
|
||||
accentSoft: '#FF6DC2',
|
||||
accentDim: '#6B2050',
|
||||
accentBg: '#4A1535',
|
||||
},
|
||||
},
|
||||
'midnight-blue': {
|
||||
@@ -64,15 +54,15 @@ const themes = {
|
||||
name: 'Midnight Blue',
|
||||
colors: {
|
||||
...defaultTheme.colors,
|
||||
cyberRed: '#0088FF',
|
||||
cyberRedDark: '#004488',
|
||||
cyberRedDeep: '#002255',
|
||||
cyberPink: '#00AAFF',
|
||||
cyberRose: '#44CCFF',
|
||||
neonRed: '#0088FF',
|
||||
brightRed: '#44CCFF',
|
||||
dimRed: '#203366',
|
||||
mutedRed: '#152244',
|
||||
accent: '#0088FF',
|
||||
accentDark: '#004488',
|
||||
accentDeep: '#002255',
|
||||
accentLight: '#00AAFF',
|
||||
accentMuted: '#44CCFF',
|
||||
accentBright: '#0088FF',
|
||||
accentSoft: '#44CCFF',
|
||||
accentDim: '#203366',
|
||||
accentBg: '#152244',
|
||||
},
|
||||
},
|
||||
'matrix-green': {
|
||||
@@ -80,15 +70,15 @@ const themes = {
|
||||
name: 'Matrix Green',
|
||||
colors: {
|
||||
...defaultTheme.colors,
|
||||
cyberRed: '#00FF41',
|
||||
cyberRedDark: '#008822',
|
||||
cyberRedDeep: '#005515',
|
||||
cyberPink: '#33FF66',
|
||||
cyberRose: '#66FF99',
|
||||
neonRed: '#00FF41',
|
||||
brightRed: '#66FF99',
|
||||
dimRed: '#206630',
|
||||
mutedRed: '#154420',
|
||||
accent: '#00FF41',
|
||||
accentDark: '#008822',
|
||||
accentDeep: '#005515',
|
||||
accentLight: '#33FF66',
|
||||
accentMuted: '#66FF99',
|
||||
accentBright: '#00FF41',
|
||||
accentSoft: '#66FF99',
|
||||
accentDim: '#206630',
|
||||
accentBg: '#154420',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -105,32 +95,33 @@ export function applyTheme(theme) {
|
||||
const root = document.documentElement
|
||||
const c = theme.colors
|
||||
const map = {
|
||||
'--bg-void': c.bgVoid,
|
||||
'--bg': c.bg,
|
||||
'--bg-base': c.bgBase,
|
||||
'--bg-surface': c.bgSurface,
|
||||
'--bg-panel': c.bgPanel,
|
||||
'--bg-elevated': c.bgElevated,
|
||||
'--bg-card': c.bgCard,
|
||||
'--bg-input': c.bgInput,
|
||||
'--bg-hover': c.bgHover,
|
||||
'--cyber-red': c.cyberRed,
|
||||
'--cyber-red-dark': c.cyberRedDark,
|
||||
'--cyber-red-deep': c.cyberRedDeep,
|
||||
'--cyber-pink': c.cyberPink,
|
||||
'--cyber-rose': c.cyberRose,
|
||||
'--neon-red': c.neonRed,
|
||||
'--bright-red': c.brightRed,
|
||||
'--dim-red': c.dimRed,
|
||||
'--muted-red': c.mutedRed,
|
||||
'--text-bright': c.textBright,
|
||||
'--text-main': c.textMain,
|
||||
'--text-dim': c.textDim,
|
||||
'--text-muted': c.textMuted,
|
||||
'--accent': c.accent,
|
||||
'--accent-dark': c.accentDark,
|
||||
'--accent-deep': c.accentDeep,
|
||||
'--accent-light': c.accentLight,
|
||||
'--accent-muted': c.accentMuted,
|
||||
'--accent-bright': c.accentBright,
|
||||
'--accent-soft': c.accentSoft,
|
||||
'--accent-dim': c.accentDim,
|
||||
'--accent-bg': c.accentBg,
|
||||
'--text-primary': c.textPrimary,
|
||||
'--text-secondary': c.textSecondary,
|
||||
'--text-tertiary': c.textTertiary,
|
||||
'--text-disabled': c.textDisabled,
|
||||
'--success': c.success,
|
||||
'--warning': c.warning,
|
||||
'--error': c.error,
|
||||
'--border-dim': c.borderDim,
|
||||
'--border-red': c.borderRed,
|
||||
'--border-red-full': c.borderRedFull,
|
||||
'--info': c.info,
|
||||
'--border': c.border,
|
||||
'--border-accent': c.borderAccent,
|
||||
'--border-accent-full': c.borderAccentFull,
|
||||
}
|
||||
Object.entries(map).forEach(([k, v]) => root.style.setProperty(k, v))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user