Compare commits
2 Commits
v0.3.3-bet
...
v0.3.3-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f6dc4b4d | ||
|
|
bb03c9fe2d |
@@ -206,8 +206,11 @@ func (s *Server) handleChatHistory(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
messages := s.convStore.Get()
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"messages": messages,
|
||||
"tokens": s.convStore.ApproxTokenCount(),
|
||||
"messages": messages,
|
||||
"tokens": s.convStore.ApproxTokenCount(),
|
||||
"max_tokens": maxTokensApprox,
|
||||
"summarize_at": summarizeThreshold,
|
||||
"summary": s.convStore.GetSummary(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -219,3 +222,16 @@ func (s *Server) handleChatClear(w http.ResponseWriter, r *http.Request) {
|
||||
s.convStore.Clear()
|
||||
writeJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleChatSummarize(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
s.autoSummarize()
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"status": "ok",
|
||||
"tokens": s.convStore.ApproxTokenCount(),
|
||||
"summary": s.convStore.GetSummary(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("/api/chat", s.handleChat)
|
||||
s.mux.HandleFunc("/api/chat/history", s.handleChatHistory)
|
||||
s.mux.HandleFunc("/api/chat/clear", s.handleChatClear)
|
||||
s.mux.HandleFunc("/api/chat/summarize", s.handleChatSummarize)
|
||||
s.mux.HandleFunc("/api/tool/call", s.handleToolCall)
|
||||
s.mux.HandleFunc("/api/tools/list", s.handleToolList)
|
||||
s.mux.HandleFunc("/api/shell/chat", s.handleShellChat)
|
||||
|
||||
@@ -12,66 +12,66 @@ import (
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
Name string `yaml:"name"`
|
||||
Pseudo string `yaml:"pseudo"`
|
||||
Email string `yaml:"email"`
|
||||
Languages []string `yaml:"languages"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Pseudo string `yaml:"pseudo" json:"pseudo"`
|
||||
Email string `yaml:"email" json:"email"`
|
||||
Languages []string `yaml:"languages" json:"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"`
|
||||
Language string `yaml:"language"`
|
||||
KeyboardLayout string `yaml:"keyboard_layout"`
|
||||
} `yaml:"preferences"`
|
||||
Editor string `yaml:"editor" json:"editor"`
|
||||
Shell string `yaml:"shell" json:"shell"`
|
||||
Theme string `yaml:"theme" json:"theme"`
|
||||
DefaultAI string `yaml:"default_ai" json:"default_ai"`
|
||||
AutoUpdate bool `yaml:"auto_update" json:"auto_update"`
|
||||
CheckOnStart bool `yaml:"check_on_start" json:"check_on_start"`
|
||||
Language string `yaml:"language" json:"language"`
|
||||
KeyboardLayout string `yaml:"keyboard_layout" json:"keyboard_layout"`
|
||||
} `yaml:"preferences" json:"preferences"`
|
||||
}
|
||||
|
||||
type AIProvider struct {
|
||||
Name string `yaml:"name"`
|
||||
APIKey string `yaml:"api_key,omitempty"`
|
||||
BaseURL string `yaml:"base_url,omitempty"`
|
||||
Model string `yaml:"model"`
|
||||
Active bool `yaml:"active"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"`
|
||||
BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"`
|
||||
Model string `yaml:"model" json:"model"`
|
||||
Active bool `yaml:"active" json:"active"`
|
||||
}
|
||||
|
||||
type ToolConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Installed bool `yaml:"installed"`
|
||||
Version string `yaml:"version"`
|
||||
AutoUpdate bool `yaml:"auto_update"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Installed bool `yaml:"installed" json:"installed"`
|
||||
Version string `yaml:"version" json:"version"`
|
||||
AutoUpdate bool `yaml:"auto_update" json:"auto_update"`
|
||||
}
|
||||
|
||||
type SSHConnection struct {
|
||||
Name string `yaml:"name"`
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
KeyPath string `yaml:"key_path,omitempty"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
Port int `yaml:"port" json:"port"`
|
||||
User string `yaml:"user" json:"user"`
|
||||
Password string `yaml:"password,omitempty" json:"password,omitempty"`
|
||||
KeyPath string `yaml:"key_path,omitempty" json:"key_path,omitempty"`
|
||||
}
|
||||
|
||||
type MuyueConfig struct {
|
||||
Version string `yaml:"version"`
|
||||
Profile Profile `yaml:"profile"`
|
||||
Version string `yaml:"version" json:"version"`
|
||||
Profile Profile `yaml:"profile" json:"profile"`
|
||||
AI struct {
|
||||
Providers []AIProvider `yaml:"providers"`
|
||||
} `yaml:"ai"`
|
||||
Tools []ToolConfig `yaml:"tools"`
|
||||
Providers []AIProvider `yaml:"providers" json:"providers"`
|
||||
} `yaml:"ai" json:"ai"`
|
||||
Tools []ToolConfig `yaml:"tools" json:"tools"`
|
||||
BMAD struct {
|
||||
Installed bool `yaml:"installed"`
|
||||
Version string `yaml:"version"`
|
||||
Global bool `yaml:"global"`
|
||||
} `yaml:"bmad"`
|
||||
Installed bool `yaml:"installed" json:"installed"`
|
||||
Version string `yaml:"version" json:"version"`
|
||||
Global bool `yaml:"global" json:"global"`
|
||||
} `yaml:"bmad" json:"bmad"`
|
||||
Terminal struct {
|
||||
CustomPrompt bool `yaml:"custom_prompt"`
|
||||
PromptTheme string `yaml:"prompt_theme"`
|
||||
SSH []SSHConnection `yaml:"ssh"`
|
||||
FontSize int `yaml:"font_size"`
|
||||
FontFamily string `yaml:"font_family"`
|
||||
Theme string `yaml:"theme"`
|
||||
} `yaml:"terminal"`
|
||||
CustomPrompt bool `yaml:"custom_prompt" json:"custom_prompt"`
|
||||
PromptTheme string `yaml:"prompt_theme" json:"prompt_theme"`
|
||||
SSH []SSHConnection `yaml:"ssh" json:"ssh"`
|
||||
FontSize int `yaml:"font_size" json:"font_size"`
|
||||
FontFamily string `yaml:"font_family" json:"font_family"`
|
||||
Theme string `yaml:"theme" json:"theme"`
|
||||
} `yaml:"terminal" json:"terminal"`
|
||||
}
|
||||
|
||||
type TerminalTheme struct {
|
||||
|
||||
@@ -24,12 +24,12 @@ const (
|
||||
)
|
||||
|
||||
type SystemInfo struct {
|
||||
OS OS
|
||||
Arch Arch
|
||||
IsWSL bool
|
||||
Shell string
|
||||
Terminal string
|
||||
PackageManager string
|
||||
OS OS `json:"os"`
|
||||
Arch Arch `json:"arch"`
|
||||
IsWSL bool `json:"is_wsl"`
|
||||
Shell string `json:"shell"`
|
||||
Terminal string `json:"terminal"`
|
||||
PackageManager string `json:"package_manager"`
|
||||
}
|
||||
|
||||
func Detect() SystemInfo {
|
||||
|
||||
@@ -14,27 +14,27 @@ import (
|
||||
)
|
||||
|
||||
type ToolStatus struct {
|
||||
Name string `yaml:"name"`
|
||||
Installed bool `yaml:"installed"`
|
||||
Version string `yaml:"version"`
|
||||
Path string `yaml:"path"`
|
||||
Latest string `yaml:"latest"`
|
||||
NeedsUpdate bool `yaml:"needs_update"`
|
||||
Category string `yaml:"category"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Installed bool `yaml:"installed" json:"installed"`
|
||||
Version string `yaml:"version" json:"version"`
|
||||
Path string `yaml:"path" json:"path"`
|
||||
Latest string `yaml:"latest" json:"latest"`
|
||||
NeedsUpdate bool `yaml:"needs_update" json:"needs_update"`
|
||||
Category string `yaml:"category" json:"category"`
|
||||
}
|
||||
|
||||
type RuntimeStatus struct {
|
||||
Name string `yaml:"name"`
|
||||
Installed bool `yaml:"installed"`
|
||||
Version string `yaml:"version"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Installed bool `yaml:"installed" json:"installed"`
|
||||
Version string `yaml:"version" json:"version"`
|
||||
}
|
||||
|
||||
type ScanResult struct {
|
||||
System platform.SystemInfo `yaml:"system"`
|
||||
Tools []ToolStatus `yaml:"tools"`
|
||||
Runtimes []RuntimeStatus `yaml:"runtimes"`
|
||||
ShellSetup bool `yaml:"shell_setup"`
|
||||
GitConfigured bool `yaml:"git_configured"`
|
||||
System platform.SystemInfo `yaml:"system" json:"system"`
|
||||
Tools []ToolStatus `yaml:"tools" json:"tools"`
|
||||
Runtimes []RuntimeStatus `yaml:"runtimes" json:"runtimes"`
|
||||
ShellSetup bool `yaml:"shell_setup" json:"shell_setup"`
|
||||
GitConfigured bool `yaml:"git_configured" json:"git_configured"`
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -56,6 +56,7 @@ const api = {
|
||||
saveTerminalSettings: (settings) => request('/terminal/settings', { method: 'PUT', body: JSON.stringify(settings) }),
|
||||
getChatHistory: () => request('/chat/history'),
|
||||
clearChat: () => request('/chat/clear', { method: 'POST' }),
|
||||
summarizeChat: () => request('/chat/summarize', { method: 'POST' }),
|
||||
sendChat: (message, stream = true, onChunk, signal) => {
|
||||
if (!stream) {
|
||||
return request('/chat', { method: 'POST', body: JSON.stringify({ message, stream: false }) })
|
||||
|
||||
@@ -3,6 +3,32 @@ import { useI18n } from '../i18n'
|
||||
|
||||
const MAX_POINTS = 30
|
||||
|
||||
function BgGraph({ data, max, color }) {
|
||||
if (!data || data.length < 2) return null
|
||||
const m = max || Math.max(...data, 1)
|
||||
const w = 120
|
||||
const h = 60
|
||||
const points = data.map((v, i) => {
|
||||
const x = (i / (data.length - 1)) * w
|
||||
const y = h - (v / m) * h
|
||||
return `${x},${y}`
|
||||
})
|
||||
const area = `${points.join(' ')} ${w},${h} 0,${h}`
|
||||
const line = points.join(' ')
|
||||
return (
|
||||
<svg viewBox={`0 0 ${w} ${h}`} className="dash-bg-graph" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id={`g-${color.replace('#','')}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity="0.25" />
|
||||
<stop offset="100%" stopColor={color} stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon fill={`url(#g-${color.replace('#','')})`} points={area} />
|
||||
<polyline fill="none" stroke={color} strokeWidth="1.5" points={line} vectorEffect="non-scaling-stroke" opacity="0.6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MiniGraph({ data, max, color, label, unit }) {
|
||||
if (!data || data.length < 2) return <div className="dash-graph-empty">collecting...</div>
|
||||
const m = max || Math.max(...data, 1)
|
||||
@@ -21,6 +47,13 @@ function MiniGraph({ data, max, color, label, unit }) {
|
||||
<span className="dash-graph-value" style={{ color }}>{last.toFixed(1)}{unit}</span>
|
||||
</div>
|
||||
<svg viewBox={`0 0 ${w} ${h}`} className="dash-graph-svg" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id={`fg-${color.replace('#','').replace('var(','').replace(')','')}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity="0.3" />
|
||||
<stop offset="100%" stopColor={color} stopOpacity="0.02" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon fill={`url(#fg-${color.replace('#','').replace('var(','').replace(')','')})`} points={`${points} ${w},${h} 0,${h}`} />
|
||||
<polyline fill="none" stroke={color} strokeWidth="1.5" points={points} vectorEffect="non-scaling-stroke" />
|
||||
</svg>
|
||||
</div>
|
||||
@@ -29,7 +62,6 @@ function MiniGraph({ data, max, color, label, unit }) {
|
||||
|
||||
export default function Dashboard({ api, refreshRef }) {
|
||||
const { t } = useI18n()
|
||||
const [dashboardStatus, setDashboardStatus] = useState(null)
|
||||
const [quota, setQuota] = useState(null)
|
||||
const [recentCmds, setRecentCmds] = useState([])
|
||||
const [processes, setProcesses] = useState([])
|
||||
@@ -38,17 +70,16 @@ export default function Dashboard({ api, refreshRef }) {
|
||||
const memRef = useRef([])
|
||||
const netRxRef = useRef([])
|
||||
const netTxRef = useRef([])
|
||||
const procCountRef = useRef([])
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [dashData, quotaData, cmdData, procData, metricsData] = await Promise.all([
|
||||
api.getDashboardStatus().catch(() => null),
|
||||
const [quotaData, cmdData, procData, metricsData] = await Promise.all([
|
||||
api.getProvidersQuota().catch(() => null),
|
||||
api.getRecentCommands().catch(() => ({ commands: [] })),
|
||||
api.getRunningProcesses().catch(() => ({ processes: [] })),
|
||||
api.getSystemMetrics().catch(() => null),
|
||||
])
|
||||
setDashboardStatus(dashData)
|
||||
setQuota(quotaData?.providers || [])
|
||||
setRecentCmds(cmdData.commands || [])
|
||||
setProcesses(procData.processes || [])
|
||||
@@ -59,6 +90,7 @@ export default function Dashboard({ api, refreshRef }) {
|
||||
netRxRef.current = [...netRxRef.current, metricsData.net_rx_kbs].slice(-MAX_POINTS)
|
||||
netTxRef.current = [...netTxRef.current, metricsData.net_tx_kbs].slice(-MAX_POINTS)
|
||||
}
|
||||
procCountRef.current = [...procCountRef.current, procData.processes?.length || 0].slice(-MAX_POINTS)
|
||||
} catch (err) {
|
||||
console.error('Dashboard load error:', err)
|
||||
}
|
||||
@@ -73,80 +105,99 @@ export default function Dashboard({ api, refreshRef }) {
|
||||
|
||||
const minimax = (quota || []).find(p => p.name === 'minimax')
|
||||
const zai = (quota || []).find(p => p.name === 'zai')
|
||||
const totalQuotaUsed = minimax?.data?.models?.reduce((s, m) => s + (m.used || 0), 0) || 0
|
||||
const totalQuotaMax = minimax?.data?.models?.reduce((s, m) => s + (m.total || 0), 0) || 1
|
||||
|
||||
return (
|
||||
<div className="dash-grid">
|
||||
{/* CPU / RAM / Network Graphs */}
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">CPU</span>
|
||||
<span className="dash-count">{metrics ? metrics.cpu_percent.toFixed(0) : '—'}%</span>
|
||||
{/* CPU */}
|
||||
<div className="dash-card dash-card-graph">
|
||||
<BgGraph data={cpuRef.current} max={100} color="#06b6d4" />
|
||||
<div className="dash-card-content">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">CPU</span>
|
||||
<span className="dash-count">{metrics ? metrics.cpu_percent.toFixed(0) : '—'}%</span>
|
||||
</div>
|
||||
<MiniGraph data={cpuRef.current} max={100} color="var(--accent)" label="CPU" unit="%" />
|
||||
</div>
|
||||
<MiniGraph data={cpuRef.current} max={100} color="var(--accent)" label="CPU" unit="%" />
|
||||
</div>
|
||||
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">RAM</span>
|
||||
<span className="dash-count">{metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)} MB` : '—'}</span>
|
||||
{/* RAM */}
|
||||
<div className="dash-card dash-card-graph">
|
||||
<BgGraph data={memRef.current} max={100} color="#a78bfa" />
|
||||
<div className="dash-card-content">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">RAM</span>
|
||||
<span className="dash-count">{metrics ? `${metrics.mem_used_mb.toFixed(0)}/${metrics.mem_total_mb.toFixed(0)}` : '—'}</span>
|
||||
</div>
|
||||
<MiniGraph data={memRef.current} max={100} color="#a78bfa" label="RAM" unit="%" />
|
||||
</div>
|
||||
<MiniGraph data={memRef.current} max={100} color="#a78bfa" label="RAM" unit="%" />
|
||||
</div>
|
||||
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">Network</span>
|
||||
<span className="dash-count">{metrics ? `↓${metrics.net_rx_kbs.toFixed(0)} ↑${metrics.net_tx_kbs.toFixed(0)} KB/s` : '—'}</span>
|
||||
{/* Network */}
|
||||
<div className="dash-card dash-card-graph">
|
||||
<BgGraph data={netRxRef.current} max={null} color="#34d399" />
|
||||
<div className="dash-card-content">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">Network</span>
|
||||
<span className="dash-count">{metrics ? `↓${metrics.net_rx_kbs.toFixed(0)} ↑${metrics.net_tx_kbs.toFixed(0)}` : '—'}</span>
|
||||
</div>
|
||||
<MiniGraph data={netRxRef.current} max={null} color="#34d399" label="RX" unit=" KB/s" />
|
||||
<MiniGraph data={netTxRef.current} max={null} color="#f59e0b" label="TX" unit=" KB/s" />
|
||||
</div>
|
||||
<MiniGraph data={netRxRef.current} max={null} color="#34d399" label="RX" unit=" KB/s" />
|
||||
<MiniGraph data={netTxRef.current} max={null} color="#f59e0b" label="TX" unit=" KB/s" />
|
||||
</div>
|
||||
|
||||
{/* API Quota */}
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">API Quota</span>
|
||||
</div>
|
||||
<div className="dash-quota-list">
|
||||
{minimax && minimax.data?.models?.map((m, i) => (
|
||||
<div key={i} className="dash-quota-row">
|
||||
<span className="dash-quota-name">{String(m.model).replace('MiniMax-', '')}</span>
|
||||
<div className="dash-bar">
|
||||
<div className="dash-bar-fill" style={{ width: `${Math.min(100, (m.used / m.total) * 100)}%` }} />
|
||||
<div className="dash-card dash-card-graph">
|
||||
<BgGraph data={totalQuotaMax > 0 ? [totalQuotaUsed / totalQuotaMax * 100, ...(cpuRef.current.length > 0 ? [] : [0])] : []} max={100} color="#f472b6" />
|
||||
<div className="dash-card-content">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">API Quota</span>
|
||||
</div>
|
||||
<div className="dash-quota-list">
|
||||
{minimax && minimax.data?.models?.map((m, i) => (
|
||||
<div key={i} className="dash-quota-row">
|
||||
<span className="dash-quota-name">{String(m.model).replace('MiniMax-', '')}</span>
|
||||
<div className="dash-bar">
|
||||
<div className="dash-bar-fill" style={{ width: `${Math.min(100, (m.used / m.total) * 100)}%` }} />
|
||||
</div>
|
||||
<span className="dash-quota-val">{m.remaining}/{m.total}</span>
|
||||
</div>
|
||||
<span className="dash-quota-val">{m.remaining}/{m.total}</span>
|
||||
</div>
|
||||
))}
|
||||
{minimax && minimax.data?.models?.length === 0 && (
|
||||
<div className="dash-quota-row">
|
||||
<span className="dash-quota-name">MiniMax</span>
|
||||
<span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>{minimax.error || 'no data'}</span>
|
||||
</div>
|
||||
)}
|
||||
{zai && (
|
||||
<div className="dash-quota-row">
|
||||
<span className="dash-quota-name">Z.AI</span>
|
||||
<span className="dash-quota-val">{zai.healthy ? '✓ active' : zai.error || '—'}</span>
|
||||
</div>
|
||||
)}
|
||||
{!minimax && !zai && <span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>No providers</span>}
|
||||
))}
|
||||
{minimax && minimax.data?.models?.length === 0 && (
|
||||
<div className="dash-quota-row">
|
||||
<span className="dash-quota-name">MiniMax</span>
|
||||
<span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>{minimax.error || 'no data'}</span>
|
||||
</div>
|
||||
)}
|
||||
{zai && (
|
||||
<div className="dash-quota-row">
|
||||
<span className="dash-quota-name">Z.AI</span>
|
||||
<span className="dash-quota-val">{zai.healthy ? '✓ active' : zai.error || '—'}</span>
|
||||
</div>
|
||||
)}
|
||||
{!minimax && !zai && <span className="dash-quota-val" style={{ color: 'var(--text-tertiary)' }}>No providers</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Running Processes */}
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">Running Processes</span>
|
||||
<span className="dash-count">{processes.length}</span>
|
||||
</div>
|
||||
<div className="dash-proc-list">
|
||||
{processes.length === 0 && <span className="dash-empty">No relevant processes</span>}
|
||||
{processes.slice(0, 8).map((p, i) => (
|
||||
<div key={i} className="dash-proc-row">
|
||||
<span className="dash-proc-name">{p.name}</span>
|
||||
<span className="dash-proc-res">cpu {p.cpu}% · mem {p.mem}%</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="dash-card dash-card-graph">
|
||||
<BgGraph data={procCountRef.current} max={null} color="#fb923c" />
|
||||
<div className="dash-card-content">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">Processes</span>
|
||||
<span className="dash-count">{processes.length}</span>
|
||||
</div>
|
||||
<div className="dash-proc-list">
|
||||
{processes.length === 0 && <span className="dash-empty">No relevant processes</span>}
|
||||
{processes.slice(0, 6).map((p, i) => (
|
||||
<div key={i} className="dash-proc-row">
|
||||
<span className="dash-proc-name">{p.name}</span>
|
||||
<span className="dash-proc-res">cpu {p.cpu}% · mem {p.mem}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -165,38 +216,6 @@ export default function Dashboard({ api, refreshRef }) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Services */}
|
||||
<div className="dash-card">
|
||||
<div className="dash-card-head">
|
||||
<span className="dash-label">Services</span>
|
||||
</div>
|
||||
{dashboardStatus ? (
|
||||
<div className="dash-services">
|
||||
<div className="dash-svc-row">
|
||||
<span className="dash-svc-name">MCP</span>
|
||||
<span className="dash-svc-val">{dashboardStatus.mcp?.healthy || 0}/{dashboardStatus.mcp?.total || 0} healthy</span>
|
||||
</div>
|
||||
<div className="dash-svc-row">
|
||||
<span className="dash-svc-name">LSP</span>
|
||||
<span className="dash-svc-val">{dashboardStatus.lsp?.installed || 0}/{dashboardStatus.lsp?.total || 0} installed</span>
|
||||
</div>
|
||||
<div className="dash-svc-row">
|
||||
<span className="dash-svc-name">Skills</span>
|
||||
<span className="dash-svc-val">{dashboardStatus.skills?.total || 0} deployed</span>
|
||||
</div>
|
||||
{(dashboardStatus.skills?.issues || []).length > 0 && (
|
||||
<div className="dash-svc-issues">
|
||||
{(dashboardStatus.skills.issues || []).slice(0, 3).map((issue, i) => (
|
||||
<div key={i} className="dash-svc-issue">⚠ {issue}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="dash-empty">Loading...</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -284,6 +284,7 @@ export default function Studio({ api }) {
|
||||
const [streamThinking, setStreamThinking] = useState('')
|
||||
const [streamToolCalls, setStreamToolCalls] = useState([])
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 })
|
||||
const messagesEnd = useRef(null)
|
||||
const textareaRef = useRef(null)
|
||||
const abortRef = useRef(null)
|
||||
@@ -297,6 +298,11 @@ export default function Studio({ api }) {
|
||||
{ id: 'welcome', role: 'system', content: t('studio.welcomeNew'), time: new Date().toISOString() },
|
||||
])
|
||||
}
|
||||
setTokenInfo({
|
||||
used: data.tokens || 0,
|
||||
max: data.max_tokens || 100000,
|
||||
summarizeAt: data.summarize_at || 80000,
|
||||
})
|
||||
setLoaded(true)
|
||||
}).catch(() => {
|
||||
setMessages([
|
||||
@@ -317,6 +323,28 @@ export default function Studio({ api }) {
|
||||
}
|
||||
}, [input])
|
||||
|
||||
const refreshTokens = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.getChatHistory()
|
||||
setTokenInfo({
|
||||
used: data.tokens || 0,
|
||||
max: data.max_tokens || 100000,
|
||||
summarizeAt: data.summarize_at || 80000,
|
||||
})
|
||||
} catch {}
|
||||
}, [api])
|
||||
|
||||
const handleSummarize = useCallback(async () => {
|
||||
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'system', content: 'Résumé de la conversation en cours...', time: new Date().toISOString() }])
|
||||
try {
|
||||
const data = await api.summarizeChat()
|
||||
setTokenInfo(prev => ({ ...prev, used: data.tokens || 0 }))
|
||||
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: '✓ Conversation résumée automatiquement. Le contexte a été compressé.', time: new Date().toISOString() }])
|
||||
} catch (err) {
|
||||
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: `Erreur de résumé: ${err.message}`, time: new Date().toISOString() }])
|
||||
}
|
||||
}, [api])
|
||||
|
||||
const handleClear = useCallback(async () => {
|
||||
try {
|
||||
await api.clearChat()
|
||||
@@ -341,6 +369,7 @@ export default function Studio({ api }) {
|
||||
'## Commandes Studio',
|
||||
'',
|
||||
'- `/clear` - Effacer la conversation',
|
||||
'- `/summarize` - Résumer la conversation précédente',
|
||||
'- `/help` - Afficher cette aide',
|
||||
'- `/plan <objectif>` - Demander un plan structuré',
|
||||
'- `/export` - Exporter la conversation en Markdown',
|
||||
@@ -359,6 +388,11 @@ export default function Studio({ api }) {
|
||||
return
|
||||
}
|
||||
|
||||
if (text === '/summarize') {
|
||||
handleSummarize()
|
||||
return
|
||||
}
|
||||
|
||||
if (text === '/model') {
|
||||
api.getProviders().then(data => {
|
||||
const active = data.providers?.find(p => p.active)
|
||||
@@ -474,8 +508,9 @@ export default function Studio({ api }) {
|
||||
setStreamThinking('')
|
||||
setStreamToolCalls([])
|
||||
abortRef.current = null
|
||||
refreshTokens()
|
||||
}
|
||||
}, [input, loading, api, t, handleClear, streaming])
|
||||
}, [input, loading, api, t, handleClear, streaming, refreshTokens, handleSummarize])
|
||||
|
||||
const handleStop = useCallback(() => {
|
||||
if (abortRef.current) {
|
||||
@@ -515,6 +550,18 @@ export default function Studio({ api }) {
|
||||
</div>
|
||||
|
||||
<div className="studio-input-area">
|
||||
<div className="studio-token-bar">
|
||||
<div className="studio-token-track">
|
||||
<div
|
||||
className={`studio-token-fill ${tokenInfo.used >= tokenInfo.summarizeAt ? 'warn' : ''}`}
|
||||
style={{ width: `${Math.min(100, (tokenInfo.used / tokenInfo.max) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="studio-token-text">
|
||||
{(tokenInfo.used / 1000).toFixed(1)}k / {(tokenInfo.max / 1000).toFixed(0)}k tokens
|
||||
{tokenInfo.used >= tokenInfo.summarizeAt && ' · résumé automatique déclenché'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="studio-input-row">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
@@ -543,7 +590,7 @@ export default function Studio({ api }) {
|
||||
)}
|
||||
</div>
|
||||
<div className="studio-input-hint">
|
||||
{t('studio.inputHint')} · /clear /help /plan /export /model
|
||||
{t('studio.inputHint')} · /clear /summarize /help /plan /export /model
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -541,11 +541,22 @@ input::placeholder { color: var(--text-disabled); }
|
||||
overflow: hidden;
|
||||
}
|
||||
.dash-card {
|
||||
position: relative;
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg); padding: 14px 16px;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dash-card-graph { padding: 0; }
|
||||
.dash-bg-graph {
|
||||
position: absolute; inset: 0; width: 100%; height: 100%;
|
||||
opacity: 0.35; pointer-events: none;
|
||||
}
|
||||
.dash-card-content {
|
||||
position: relative; z-index: 1;
|
||||
padding: 14px 16px;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
}
|
||||
.dash-span-2 { grid-column: span 2; }
|
||||
.dash-card-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
@@ -785,6 +796,11 @@ input::placeholder { color: var(--text-disabled); }
|
||||
.studio-thinking span:nth-child(3) { animation-delay: 0.3s; }
|
||||
@keyframes bounce { 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; } 40% { transform: translateY(-6px); opacity: 1; } }
|
||||
.studio-input-area { padding: 12px 20px 8px; border-top: 1px solid var(--border); background: var(--bg-surface); }
|
||||
.studio-token-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.studio-token-track { flex: 1; height: 3px; background: var(--bg-input); border-radius: 2px; overflow: hidden; }
|
||||
.studio-token-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.4s, background 0.3s; }
|
||||
.studio-token-fill.warn { background: var(--warning); }
|
||||
.studio-token-text { font-size: 10px; font-family: var(--font-mono); color: var(--text-tertiary); white-space: nowrap; }
|
||||
.studio-input-row { display: flex; gap: 8px; align-items: flex-end; }
|
||||
.studio-input-row textarea {
|
||||
flex: 1; resize: none; min-height: 42px; max-height: 200px; padding: 10px 14px;
|
||||
|
||||
Reference in New Issue
Block a user