Compare commits
16 Commits
v0.3.1-bet
...
v0.3.2-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bbac499a7 | ||
|
|
83d7a573c7 | ||
|
|
0fe82f67df | ||
|
|
4b9f2c377d | ||
|
|
95bd824259 | ||
|
|
252f178bbd | ||
|
|
7dcf505360 | ||
|
|
8fb93fa47e | ||
|
|
5ec373cd6a | ||
|
|
1eb5a6d00f | ||
|
|
cd5ebe083c | ||
|
|
2004c15dd7 | ||
|
|
9306152736 | ||
|
|
e15a034de5 | ||
|
|
3b6cc38ea0 | ||
|
|
80c11cab3f |
61
CHANGELOG.md
61
CHANGELOG.md
@@ -4,6 +4,67 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
### Changes since v0.2.1
|
||||||
|
|
||||||
|
- fix(terminal): resolve PTY shell exec error, simplify CLI, unify Config tabs, restore Studio CSS (0b22109)
|
||||||
|
- feat: add API key validation flow for AI provider config (7f67473)
|
||||||
|
- feat(studio): replace sidebar layout with unified execution feed styles (040e482)
|
||||||
|
- fix: guard against empty tabs array in closeTab (c8903ef)
|
||||||
|
- refactor: redesign Config as settings window with sidebar panels, remove system overview from Dashboard (f3cb306)
|
||||||
|
- feat: add multi-tab terminal with SSH support, config editing, and dashboard redesign (3cdcb22)
|
||||||
|
- feat(studio): add i18n keys and CSS for redesigned AI chat interface (ee18bbe)
|
||||||
|
- chore: bump version to 0.3.0 (b0b0e1d)
|
||||||
|
- chore: remove dead code (packages, functions, types, constants) (fc79810)
|
||||||
|
- docs: rewrite README and CHANGELOG for desktop app mode (f7222b0)
|
||||||
|
- feat(web): add i18n support with FR/EN locales and keyboard layout awareness (11417d3)
|
||||||
|
- refactor(web): redesign frontend for native web UX (3dc24ae)
|
||||||
|
- refactor: remove TUI, desktop web UI is now the default and only mode (aa0ff19)
|
||||||
|
- refactor: unify into single `muyue` binary with embedded desktop mode (3463605)
|
||||||
|
- fix(ci): add frontend build step before Go vet/test/build (097cf40)
|
||||||
|
- feat: add desktop app with React frontend, API backend, theme system (#2) (88d2a03)
|
||||||
|
- chore: update CHANGELOG for v0.2.1 (1830c18)
|
||||||
|
- feat: complete TUI redesign with cyberpunk theme (#1) (cb8e3d0)
|
||||||
|
|
||||||
|
### Downloads
|
||||||
|
|
||||||
|
| Platform | File |
|
||||||
|
|----------|------|
|
||||||
|
| Linux x86_64 | [muyue-linux-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-linux-amd64.tar.gz) |
|
||||||
|
| Linux ARM64 | [muyue-linux-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-linux-arm64.tar.gz) |
|
||||||
|
| macOS Intel | [muyue-darwin-amd64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-darwin-amd64.tar.gz) |
|
||||||
|
| macOS Apple Silicon | [muyue-darwin-arm64.tar.gz](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-darwin-arm64.tar.gz) |
|
||||||
|
| Windows x86_64 | [muyue-windows-amd64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-windows-amd64.zip) |
|
||||||
|
| Windows ARM64 | [muyue-windows-arm64.zip](https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-windows-arm64.zip) |
|
||||||
|
|
||||||
|
The binary includes both CLI and Desktop modes.
|
||||||
|
Run `muyue` for TUI, `muyue desktop` for web UI.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
**Linux (x86_64)**
|
||||||
|
```bash
|
||||||
|
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-linux-amd64.tar.gz | tar xz
|
||||||
|
chmod +x muyue-linux-amd64
|
||||||
|
sudo mv muyue-linux-amd64 /usr/local/bin/muyue
|
||||||
|
```
|
||||||
|
|
||||||
|
**macOS (Apple Silicon)**
|
||||||
|
```bash
|
||||||
|
curl -sL https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-darwin-arm64.tar.gz | tar xz
|
||||||
|
chmod +x muyue-darwin-arm64
|
||||||
|
sudo mv muyue-darwin-arm64 /usr/local/bin/muyue
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows (x86_64)**
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest -Uri "https://gitea.legion-muyue.fr/Muyue/MuyueWorkspace/releases/download/v0.3.0/muyue-windows-amd64.zip" -OutFile "muyue.zip"
|
||||||
|
Expand-Archive -Path "muyue.zip" -DestinationPath "."
|
||||||
|
Move-Item muyue-windows-amd64.exe C:\Windows\muyue.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package api
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -281,3 +284,203 @@ func (s *Server) handleGetTerminalThemes(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
writeJSON(w, map[string]interface{}{"themes": themes})
|
writeJSON(w, map[string]interface{}{"themes": themes})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleResetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir, err := config.ConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := filepath.Join(dir, "config.yaml")
|
||||||
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||||
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.config = config.Default()
|
||||||
|
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) handleApplyStarshipTheme(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
writeError(w, "POST only", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Theme string `json:"theme"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeError(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Theme == "" {
|
||||||
|
body.Theme = s.config.Terminal.PromptTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgDir, err := config.ConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
starshipDir := filepath.Join(cfgDir, "starship")
|
||||||
|
if err := os.MkdirAll(starshipDir, 0755); err != nil {
|
||||||
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
themeFile := filepath.Join(starshipDir, "starship.toml")
|
||||||
|
|
||||||
|
themeContent := getStarshipThemeConfig(body.Theme)
|
||||||
|
if err := os.WriteFile(themeFile, []byte(themeContent), 0644); err != nil {
|
||||||
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
shellRCs := []string{
|
||||||
|
filepath.Join(home, ".bashrc"),
|
||||||
|
filepath.Join(home, ".zshrc"),
|
||||||
|
}
|
||||||
|
for _, rc := range shellRCs {
|
||||||
|
if _, err := os.Stat(rc); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content, _ := os.ReadFile(rc)
|
||||||
|
if strings.Contains(string(content), "STARSHIP_CONFIG") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
exportLine := fmt.Sprintf("\n# Muyue Starship config\nexport STARSHIP_CONFIG=%s\n", themeFile)
|
||||||
|
f, err := os.OpenFile(rc, os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.WriteString(exportLine)
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.config.Terminal.PromptTheme = body.Theme
|
||||||
|
config.Save(s.config)
|
||||||
|
|
||||||
|
writeJSON(w, map[string]interface{}{"status": "ok", "config": themeFile})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStarshipThemeConfig(theme string) string {
|
||||||
|
switch theme {
|
||||||
|
case "charm":
|
||||||
|
return `[format]
|
||||||
|
before_format = "$"
|
||||||
|
format = """
|
||||||
|
$username$directory$git_branch$git_status$cmd_duration$line_break$character"""
|
||||||
|
|
||||||
|
[character]
|
||||||
|
success_symbol = "[➜](bold #00E676)"
|
||||||
|
error_symbol = "[✗](bold #FF0033)"
|
||||||
|
|
||||||
|
[directory]
|
||||||
|
truncation_length = 3
|
||||||
|
truncation_symbol = "…/"
|
||||||
|
style = "bold #00BCD4"
|
||||||
|
|
||||||
|
[username]
|
||||||
|
show_on_left = false
|
||||||
|
style_user = "bold #FF0033"
|
||||||
|
style_root = "bold #FF0033"
|
||||||
|
|
||||||
|
[git_branch]
|
||||||
|
symbol = " "
|
||||||
|
format = "on [$symbol$branch]($style)"
|
||||||
|
style = "bold #FFD740"
|
||||||
|
|
||||||
|
[git_status]
|
||||||
|
format = "[$all_status$ahead_behind]($style) "
|
||||||
|
style = "bold #FF1A5E"
|
||||||
|
conflicted = "!"
|
||||||
|
untracked = "?"
|
||||||
|
modified = "~"
|
||||||
|
staged = "[+]"
|
||||||
|
renamed = "»"
|
||||||
|
deleted = "-"
|
||||||
|
|
||||||
|
[cmd_duration]
|
||||||
|
min_time = 500
|
||||||
|
format = "took [$duration]($style)"
|
||||||
|
style = "bold #75715E"
|
||||||
|
`
|
||||||
|
case "zerotwo":
|
||||||
|
return `[format]
|
||||||
|
before_format = "$"
|
||||||
|
format = """
|
||||||
|
$username$directory$git_branch$git_status$cmd_duration$line_break$character"""
|
||||||
|
|
||||||
|
[character]
|
||||||
|
success_symbol = "[❯](bold #3B82F6)"
|
||||||
|
error_symbol = "[❯](bold #EF4444)"
|
||||||
|
|
||||||
|
[directory]
|
||||||
|
truncation_length = 3
|
||||||
|
truncation_symbol = "…/"
|
||||||
|
style = "bold #8B5CF6"
|
||||||
|
|
||||||
|
[username]
|
||||||
|
show_on_left = false
|
||||||
|
style_user = "bold #EC4899"
|
||||||
|
style_root = "bold #EF4444"
|
||||||
|
|
||||||
|
[git_branch]
|
||||||
|
symbol = " "
|
||||||
|
format = "on [$symbol$branch]($style)"
|
||||||
|
style = "bold #F472B6"
|
||||||
|
|
||||||
|
[git_status]
|
||||||
|
format = "[$all_status$ahead_behind]($style) "
|
||||||
|
style = "bold #EF4444"
|
||||||
|
conflicted = "!"
|
||||||
|
untracked = "?"
|
||||||
|
modified = "~"
|
||||||
|
staged = "[+]"
|
||||||
|
renamed = "»"
|
||||||
|
deleted = "-"
|
||||||
|
|
||||||
|
[cmd_duration]
|
||||||
|
min_time = 500
|
||||||
|
format = "took [$duration]($style)"
|
||||||
|
style = "bold #6B7280"
|
||||||
|
`
|
||||||
|
default:
|
||||||
|
return `[format]
|
||||||
|
before_format = "$"
|
||||||
|
format = """
|
||||||
|
$username$directory$git_branch$git_status$line_break$character"""
|
||||||
|
|
||||||
|
[character]
|
||||||
|
success_symbol = "[❯](bold green)"
|
||||||
|
error_symbol = "[❯](bold red)"
|
||||||
|
|
||||||
|
[directory]
|
||||||
|
truncation_length = 3
|
||||||
|
truncation_symbol = "…/"
|
||||||
|
style = "bold cyan"
|
||||||
|
|
||||||
|
[username]
|
||||||
|
show_on_left = false
|
||||||
|
style_user = "bold red"
|
||||||
|
style_root = "bold red"
|
||||||
|
|
||||||
|
[git_branch]
|
||||||
|
symbol = " "
|
||||||
|
format = "on [$symbol$branch]($style)"
|
||||||
|
style = "bold yellow"
|
||||||
|
|
||||||
|
[cmd_duration]
|
||||||
|
min_time = 500
|
||||||
|
format = "took [$duration]($style)"
|
||||||
|
style = "bold bright-black"
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package version
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
Name = "muyue"
|
Name = "muyue"
|
||||||
Version = "0.3.1"
|
Version = "0.3.2"
|
||||||
Author = "La Légion de Muyue"
|
Author = "La Légion de Muyue"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Dashboard from './Dashboard'
|
|||||||
import Studio from './Studio'
|
import Studio from './Studio'
|
||||||
import Shell from './Shell'
|
import Shell from './Shell'
|
||||||
import Config from './Config'
|
import Config from './Config'
|
||||||
|
import OnboardingWizard from './OnboardingWizard'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [activeTab, setActiveTab] = useState('dash')
|
const [activeTab, setActiveTab] = useState('dash')
|
||||||
@@ -15,6 +16,7 @@ export default function App() {
|
|||||||
const [updates, setUpdates] = useState([])
|
const [updates, setUpdates] = useState([])
|
||||||
const [tools, setTools] = useState([])
|
const [tools, setTools] = useState([])
|
||||||
const [config, setConfig] = useState(null)
|
const [config, setConfig] = useState(null)
|
||||||
|
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||||
const { t, layout } = useI18n()
|
const { t, layout } = useI18n()
|
||||||
|
|
||||||
const TABS = useMemo(() => [
|
const TABS = useMemo(() => [
|
||||||
@@ -32,8 +34,11 @@ export default function App() {
|
|||||||
setConfig(d)
|
setConfig(d)
|
||||||
const theme = d.profile?.preferences?.theme || 'cyberpunk-red'
|
const theme = d.profile?.preferences?.theme || 'cyberpunk-red'
|
||||||
applyTheme(getTheme(theme))
|
applyTheme(getTheme(theme))
|
||||||
|
const hasProfile = d.profile?.name || d.profile?.pseudo
|
||||||
|
if (!hasProfile) setShowOnboarding(true)
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
applyTheme(getTheme('cyberpunk-red'))
|
applyTheme(getTheme('cyberpunk-red'))
|
||||||
|
setShowOnboarding(true)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -150,6 +155,8 @@ export default function App() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
{showOnboarding && <OnboardingWizard api={api} onComplete={() => setShowOnboarding(false)} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const PANELS = [
|
|||||||
{ id: 'updates', icon: RefreshCw },
|
{ id: 'updates', icon: RefreshCw },
|
||||||
{ id: 'locale', icon: Globe },
|
{ id: 'locale', icon: Globe },
|
||||||
{ id: 'skills', icon: Wrench },
|
{ id: 'skills', icon: Wrench },
|
||||||
|
{ id: 'system', icon: Monitor },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function Config({ api }) {
|
export default function Config({ api }) {
|
||||||
@@ -27,9 +28,7 @@ export default function Config({ api }) {
|
|||||||
const [profileForm, setProfileForm] = useState({})
|
const [profileForm, setProfileForm] = useState({})
|
||||||
const [providerForm, setProviderForm] = useState({})
|
const [providerForm, setProviderForm] = useState({})
|
||||||
const [toast, setToast] = useState(null)
|
const [toast, setToast] = useState(null)
|
||||||
const [terminalThemes, setTerminalThemes] = useState([])
|
|
||||||
const [terminalSettings, setTerminalSettings] = useState({ font_size: 14, font_family: '', theme: 'default' })
|
|
||||||
const [savingTerminal, setSavingTerminal] = useState(false)
|
|
||||||
|
|
||||||
const layouts = getLayoutList()
|
const layouts = getLayoutList()
|
||||||
|
|
||||||
@@ -43,19 +42,13 @@ export default function Config({ api }) {
|
|||||||
editor: d.profile?.preferences?.editor || '',
|
editor: d.profile?.preferences?.editor || '',
|
||||||
shell: d.profile?.preferences?.shell || '',
|
shell: d.profile?.preferences?.shell || '',
|
||||||
})
|
})
|
||||||
if (d.terminal) {
|
|
||||||
setTerminalSettings({
|
|
||||||
font_size: d.terminal.font_size || 14,
|
|
||||||
font_family: d.terminal.font_family || '',
|
|
||||||
theme: d.terminal.theme || 'default',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
api.getProviders().then(d => setProviders(d.providers || [])).catch(() => {})
|
api.getProviders().then(d => setProviders(d.providers || [])).catch(() => {})
|
||||||
api.getSkills().then(d => setSkillList(d.skills || [])).catch(() => {})
|
api.getSkills().then(d => setSkillList(d.skills || [])).catch(() => {})
|
||||||
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
api.getUpdates().then(d => setUpdates(d.updates || [])).catch(() => {})
|
||||||
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
api.getTools().then(d => setTools(d.tools || [])).catch(() => {})
|
||||||
api.getTerminalThemes().then(d => setTerminalThemes(d.themes || [])).catch(() => {})
|
|
||||||
}, [api])
|
}, [api])
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [loadData])
|
useEffect(() => { loadData() }, [loadData])
|
||||||
@@ -126,18 +119,6 @@ export default function Config({ api }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveTerminalSettings = async () => {
|
|
||||||
setSavingTerminal(true)
|
|
||||||
try {
|
|
||||||
await api.saveTerminalSettings(terminalSettings)
|
|
||||||
showToast(t('config.saved'))
|
|
||||||
window.location.reload()
|
|
||||||
} catch (err) {
|
|
||||||
showToast(`${t('config.error')}: ${err.message}`)
|
|
||||||
}
|
|
||||||
setSavingTerminal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const openProviderEdit = (p) => {
|
const openProviderEdit = (p) => {
|
||||||
setProviderForm({
|
setProviderForm({
|
||||||
name: p.name,
|
name: p.name,
|
||||||
@@ -213,13 +194,10 @@ export default function Config({ api }) {
|
|||||||
{activePanel === 'skills' && (
|
{activePanel === 'skills' && (
|
||||||
<PanelSkills skillList={skillList} t={t} />
|
<PanelSkills skillList={skillList} t={t} />
|
||||||
)}
|
)}
|
||||||
{activePanel === 'terminal' && (
|
{activePanel === 'system' && (
|
||||||
<PanelTerminal
|
<PanelSystem api={api} t={t} />
|
||||||
settings={terminalSettings} setSettings={setTerminalSettings}
|
|
||||||
themes={terminalThemes} saving={savingTerminal}
|
|
||||||
onSave={handleSaveTerminalSettings} t={t}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -470,99 +448,70 @@ function PanelSkills({ skillList, t }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PanelTerminal({ settings, setSettings, themes, saving, onSave, t }) {
|
function PanelSystem({ api, t }) {
|
||||||
const previewTheme = {
|
const [resetConfirm, setResetConfirm] = useState(false)
|
||||||
background: settings.theme === 'default' ? '#0A0A0C' :
|
const [toast, setToast] = useState(null)
|
||||||
settings.theme === 'monokai' ? '#272822' :
|
|
||||||
settings.theme === 'gruvbox' ? '#282828' :
|
const showToast = (msg) => {
|
||||||
settings.theme === 'nord' ? '#2E3440' :
|
setToast(msg)
|
||||||
settings.theme === 'solarized-dark' ? '#002B36' :
|
setTimeout(() => setToast(null), 3000)
|
||||||
settings.theme === 'dracula' ? '#282A36' : '#0A0A0C',
|
}
|
||||||
foreground: settings.theme === 'default' ? '#EAE0E2' :
|
|
||||||
settings.theme === 'monokai' ? '#F8F8F2' :
|
const handleReset = async () => {
|
||||||
settings.theme === 'gruvbox' ? '#EBDBB2' :
|
try {
|
||||||
settings.theme === 'nord' ? '#D8DEE9' :
|
await api.resetConfig()
|
||||||
settings.theme === 'solarized-dark' ? '#839496' :
|
setResetConfirm(false)
|
||||||
settings.theme === 'dracula' ? '#F8F8F2' : '#EAE0E2',
|
showToast(t('config.resetDone'))
|
||||||
|
setTimeout(() => window.location.reload(), 1500)
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`${t('config.error')}: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleApplyStarship = async () => {
|
||||||
|
try {
|
||||||
|
await api.applyStarshipTheme('charm')
|
||||||
|
showToast(t('config.starshipApplied'))
|
||||||
|
} catch (err) {
|
||||||
|
showToast(`${t('config.error')}: ${err.message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="config-card">
|
<>
|
||||||
<div className="config-card-group">
|
{toast && <div className="config-toast">{toast}</div>}
|
||||||
<span className="config-card-group-label">{t('config.terminalTheme')}</span>
|
<div className="config-card">
|
||||||
<div className="chip-row">
|
<div className="config-card-row" style={{ marginBottom: 16 }}>
|
||||||
{themes.map(th => (
|
<span className="config-card-label" style={{ fontWeight: 600 }}>{t('config.applyStarship')}</span>
|
||||||
<div
|
|
||||||
key={th.id}
|
|
||||||
className={`chip ${settings.theme === th.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSettings(s => ({ ...s, theme: th.id }))}
|
|
||||||
>
|
|
||||||
{th.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style={{ fontSize: 12, color: 'var(--muted)', marginBottom: 12 }}>
|
||||||
|
{t('config.starshipApplied')}
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.fontSize')}</span>
|
|
||||||
<div className="chip-row">
|
|
||||||
{[12, 14, 16, 18, 20, 24].map(size => (
|
|
||||||
<div
|
|
||||||
key={size}
|
|
||||||
className={`chip ${settings.font_size === size ? 'active' : ''}`}
|
|
||||||
onClick={() => setSettings(s => ({ ...s, font_size: size }))}
|
|
||||||
>
|
|
||||||
{size}px
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button className="sm primary" onClick={handleApplyStarship}>
|
||||||
|
{t('config.applyStarship')}
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.fontFamily')}</span>
|
|
||||||
<select
|
|
||||||
className="config-form-input"
|
|
||||||
value={settings.font_family}
|
|
||||||
onChange={e => setSettings(s => ({ ...s, font_family: e.target.value }))}
|
|
||||||
style={{ maxWidth: 300 }}
|
|
||||||
>
|
|
||||||
<option value="">Default (JetBrains Mono)</option>
|
|
||||||
<option value="'Fira Code', monospace">Fira Code</option>
|
|
||||||
<option value="'Cascadia Code', 'SF Mono', monospace">Cascadia Code</option>
|
|
||||||
<option value="'SF Mono', 'Menlo', monospace">SF Mono</option>
|
|
||||||
<option value="'Source Code Pro', monospace">Source Code Pro</option>
|
|
||||||
<option value="monospace">System Monospace</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{t('config.preview')}</span>
|
|
||||||
<div style={{
|
|
||||||
background: previewTheme.background,
|
|
||||||
color: previewTheme.foreground,
|
|
||||||
padding: '16px 20px',
|
|
||||||
borderRadius: 'var(--radius)',
|
|
||||||
fontFamily: settings.font_family || "'JetBrains Mono', monospace",
|
|
||||||
fontSize: settings.font_size || 14,
|
|
||||||
border: '1px solid var(--border)',
|
|
||||||
}}>
|
|
||||||
<span style={{ color: '#00E676' }}>➜</span> <span>~/projects</span>
|
|
||||||
<span style={{ color: '#448AFF' }}> git status</span>
|
|
||||||
<br />
|
|
||||||
<span>On branch </span>
|
|
||||||
<span style={{ color: '#FFD740' }}>main</span>
|
|
||||||
<br />
|
|
||||||
<span style={{ opacity: 0.6 }}>Type a command...</span>
|
|
||||||
<span style={{ animation: 'blink 1s step-end infinite' }}> ▋</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="config-card-actions" style={{ marginTop: 16 }}>
|
|
||||||
<button className="primary sm" onClick={onSave} disabled={saving}>
|
|
||||||
{saving ? t('config.saving') : t('config.save')}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="config-card" style={{ marginTop: 12 }}>
|
||||||
|
<div className="config-card-row" style={{ marginBottom: 16 }}>
|
||||||
|
<span className="config-card-label" style={{ fontWeight: 600 }}>{t('config.resetConfig')}</span>
|
||||||
|
</div>
|
||||||
|
{resetConfirm ? (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--warning)', marginBottom: 12 }}>
|
||||||
|
{t('config.resetConfirm')}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
|
<button className="sm" onClick={() => setResetConfirm(false)}>{t('config.cancel')}</button>
|
||||||
|
<button className="sm danger" onClick={handleReset}>{t('config.resetConfig')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button className="sm ghost danger" onClick={() => setResetConfirm(true)}>
|
||||||
|
{t('config.resetConfig')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
224
web/src/components/OnboardingWizard.jsx
Normal file
224
web/src/components/OnboardingWizard.jsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Sparkles, ArrowRight } from 'lucide-react'
|
||||||
|
import { useI18n, LANGUAGES } from '../i18n'
|
||||||
|
import { getLayoutList } from '../i18n/keyboards'
|
||||||
|
|
||||||
|
const STEPS = [
|
||||||
|
{ key: 'welcome', title: 'welcome', field: null },
|
||||||
|
{ key: 'name', title: 'name', field: 'name' },
|
||||||
|
{ key: 'language', title: 'language', field: 'language' },
|
||||||
|
{ key: 'keyboard', title: 'keyboard', field: 'keyboard' },
|
||||||
|
{ key: 'editor', title: 'editor', field: 'editor' },
|
||||||
|
{ key: 'done', title: 'done', field: null },
|
||||||
|
]
|
||||||
|
|
||||||
|
const EDITOR_SUGGESTIONS = ['vim', 'nvim', 'vscode', 'emacs', 'nano', 'helix']
|
||||||
|
|
||||||
|
export default function OnboardingWizard({ api, onComplete }) {
|
||||||
|
const { t, language, keyboard, setLanguage, setKeyboard } = useI18n()
|
||||||
|
const [step, setStep] = useState(0)
|
||||||
|
const [answers, setAnswers] = useState({
|
||||||
|
name: '',
|
||||||
|
language: 'fr',
|
||||||
|
keyboard: 'azerty',
|
||||||
|
editor: '',
|
||||||
|
})
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
const current = STEPS[step]
|
||||||
|
const layouts = getLayoutList()
|
||||||
|
|
||||||
|
const goNext = () => {
|
||||||
|
if (step < STEPS.length - 1) setStep(step + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
await api.saveProfile({
|
||||||
|
name: answers.name,
|
||||||
|
pseudo: answers.name.split(' ')[0] || 'user',
|
||||||
|
editor: answers.editor,
|
||||||
|
})
|
||||||
|
await api.savePreferences({
|
||||||
|
language: answers.language,
|
||||||
|
keyboard_layout: answers.keyboard,
|
||||||
|
})
|
||||||
|
onComplete()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="onboarding-overlay">
|
||||||
|
<div className="onboarding-card">
|
||||||
|
<div className="onboarding-header">
|
||||||
|
<Sparkles size={20} style={{ color: 'var(--accent)' }} />
|
||||||
|
<span> Muyue Setup</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="onboarding-progress">
|
||||||
|
{STEPS.map((_, i) => (
|
||||||
|
<div key={i} className={`onboarding-dot ${i === step ? 'active' : ''} ${i < step ? 'done' : ''}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="onboarding-body">
|
||||||
|
{current.key === 'welcome' && (
|
||||||
|
<div className="onboarding-step">
|
||||||
|
<div className="onboarding-title">Bienvenue ! 👋</div>
|
||||||
|
<div className="onboarding-desc">
|
||||||
|
Je suis votre assistant de configuration. Quelques questions rapides pour personnaliser votre expérience.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current.key === 'name' && (
|
||||||
|
<div className="onboarding-step">
|
||||||
|
<div className="onboarding-title">Comment vous appelez-vous ?</div>
|
||||||
|
<input
|
||||||
|
className="onboarding-input"
|
||||||
|
placeholder="Votre nom..."
|
||||||
|
value={answers.name}
|
||||||
|
onChange={e => setAnswers(a => ({ ...a, name: e.target.value }))}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current.key === 'language' && (
|
||||||
|
<div className="onboarding-step">
|
||||||
|
<div className="onboarding-title">Quelle langue pr\u00e9f\u00e9rez-vous ?</div>
|
||||||
|
<div className="onboarding-chips">
|
||||||
|
{LANGUAGES.map(lang => (
|
||||||
|
<div
|
||||||
|
key={lang.id}
|
||||||
|
className={`chip ${answers.language === lang.id ? 'active' : ''}`}
|
||||||
|
onClick={() => setAnswers(a => ({ ...a, language: lang.id }))}
|
||||||
|
>
|
||||||
|
{lang.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current.key === 'keyboard' && (
|
||||||
|
<div className="onboarding-step">
|
||||||
|
<div className="onboarding-title">Disposition du clavier ?</div>
|
||||||
|
<div className="onboarding-chips">
|
||||||
|
{layouts.map(l => (
|
||||||
|
<div
|
||||||
|
key={l.id}
|
||||||
|
className={`chip ${answers.keyboard === l.id ? 'active' : ''}`}
|
||||||
|
onClick={() => setAnswers(a => ({ ...a, keyboard: l.id }))}
|
||||||
|
>
|
||||||
|
{l.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current.key === 'editor' && (
|
||||||
|
<div className="onboarding-step">
|
||||||
|
<div className="onboarding-title">Quel \u00e9diteur utilisez-vous ?</div>
|
||||||
|
<div className="onboarding-chips">
|
||||||
|
{EDITOR_SUGGESTIONS.map(ed => (
|
||||||
|
<div
|
||||||
|
key={ed}
|
||||||
|
className={`chip ${answers.editor === ed ? 'active' : ''}`}
|
||||||
|
onClick={() => setAnswers(a => ({ ...a, editor: ed }))}
|
||||||
|
>
|
||||||
|
{ed}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="onboarding-input"
|
||||||
|
style={{ marginTop: 12 }}
|
||||||
|
placeholder="Autre (vim, nvim, vscode...)"
|
||||||
|
value={answers.editor}
|
||||||
|
onChange={e => setAnswers(a => ({ ...a, editor: e.target.value }))}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current.key === 'done' && (
|
||||||
|
<div className="onboarding-step">
|
||||||
|
<div className="onboarding-title">C'est parti ! 🚀</div>
|
||||||
|
<div className="onboarding-desc">
|
||||||
|
Votre profil est configur\u00e9. Vous pouvez toujours ajuster les param\u00e8tres dans l'onglet Configuration.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="onboarding-footer">
|
||||||
|
{current.key === 'done' ? (
|
||||||
|
<button className="primary" onClick={handleSave} disabled={saving}>
|
||||||
|
{saving ? '...' : 'Commencer'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button className="primary" onClick={goNext}>
|
||||||
|
Suivant <ArrowRight size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.onboarding-overlay {
|
||||||
|
position: fixed; inset: 0; z-index: 500;
|
||||||
|
background: rgba(10,10,12,0.85);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.onboarding-card {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
width: 480px; max-width: 90vw;
|
||||||
|
box-shadow: 0 24px 64px rgba(0,0,0,0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.onboarding-header {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
padding: 16px 20px; font-size: 14px; font-weight: 700;
|
||||||
|
color: var(--accent); border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
.onboarding-progress {
|
||||||
|
display: flex; gap: 6px; padding: 14px 20px;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.onboarding-dot {
|
||||||
|
width: 32px; height: 4px; border-radius: 2px;
|
||||||
|
background: var(--bg-input); transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.onboarding-dot.active { background: var(--accent); }
|
||||||
|
.onboarding-dot.done { background: var(--accent-dim); }
|
||||||
|
.onboarding-body { padding: 28px 24px; min-height: 200px; }
|
||||||
|
.onboarding-step { display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
.onboarding-title { font-size: 18px; font-weight: 700; color: var(--text-primary); }
|
||||||
|
.onboarding-desc { font-size: 14px; color: var(--text-tertiary); line-height: 1.6; }
|
||||||
|
.onboarding-input {
|
||||||
|
width: 100%; background: var(--bg-input); border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius); padding: 10px 14px; color: var(--text-primary);
|
||||||
|
font-size: 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.onboarding-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--border-accent); }
|
||||||
|
.onboarding-chips { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.onboarding-footer {
|
||||||
|
display: flex; justify-content: flex-end; gap: 8px;
|
||||||
|
padding: 16px 20px; border-top: 1px solid var(--border);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -121,6 +121,7 @@ const en = {
|
|||||||
updates: 'Updates',
|
updates: 'Updates',
|
||||||
locale: 'Language & Keyboard',
|
locale: 'Language & Keyboard',
|
||||||
skills: 'Skills',
|
skills: 'Skills',
|
||||||
|
system: 'System',
|
||||||
},
|
},
|
||||||
profile: 'Profile',
|
profile: 'Profile',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -180,6 +181,12 @@ const en = {
|
|||||||
fontFamily: 'Font Family',
|
fontFamily: 'Font Family',
|
||||||
preview: 'Preview',
|
preview: 'Preview',
|
||||||
saving: 'Saving...',
|
saving: 'Saving...',
|
||||||
|
resetConfig: 'Reset all',
|
||||||
|
resetConfirm: 'Are you sure? All preferences will be erased.',
|
||||||
|
resetDone: 'Settings reset.',
|
||||||
|
applyStarship: 'Apply starship',
|
||||||
|
starshipApplied: 'Starship theme applied! Restart your shell to see the result.',
|
||||||
|
starshipError: 'Failed to apply starship theme.',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ const fr = {
|
|||||||
terminal: 'Terminal',
|
terminal: 'Terminal',
|
||||||
updates: 'Mises \u00e0 jour',
|
updates: 'Mises \u00e0 jour',
|
||||||
locale: 'Langue & Clavier',
|
locale: 'Langue & Clavier',
|
||||||
skills: 'Comp\u00e9tences',
|
skills: 'Comp\u00e9ENCES',
|
||||||
|
system: 'Syst\u00e8me',
|
||||||
},
|
},
|
||||||
profile: 'Profil',
|
profile: 'Profil',
|
||||||
name: 'Nom',
|
name: 'Nom',
|
||||||
@@ -143,7 +144,7 @@ const fr = {
|
|||||||
save: 'Enregistrer',
|
save: 'Enregistrer',
|
||||||
saved: 'Enregistr\u00e9 !',
|
saved: 'Enregistr\u00e9 !',
|
||||||
error: 'Erreur',
|
error: 'Erreur',
|
||||||
skills: 'Comp\u00e9tences',
|
skills: 'Comp\u00e9ENCES',
|
||||||
noSkills: 'Aucune comp\u00e9tence install\u00e9e.',
|
noSkills: 'Aucune comp\u00e9tence install\u00e9e.',
|
||||||
runSkillsInit: 'Ex\u00e9cutez muyue skills init',
|
runSkillsInit: 'Ex\u00e9cutez muyue skills init',
|
||||||
language: 'Langue',
|
language: 'Langue',
|
||||||
@@ -167,10 +168,10 @@ const fr = {
|
|||||||
editProfile: 'Modifier',
|
editProfile: 'Modifier',
|
||||||
editProvider: 'Configurer',
|
editProvider: 'Configurer',
|
||||||
validateKey: 'Valider',
|
validateKey: 'Valider',
|
||||||
validating: 'Vérification...',
|
validating: 'V\u00e9rification...',
|
||||||
keyValid: 'Clé valide',
|
keyValid: 'Cl\u00e9 valide',
|
||||||
keyInvalid: 'Clé invalide',
|
keyInvalid: 'Cl\u00e9 invalide',
|
||||||
connectionFailed: 'Connexion échouée',
|
connectionFailed: 'Connexion \u00e9chou\u00e9e',
|
||||||
enterToken: 'Entrez votre token API pour {provider}',
|
enterToken: 'Entrez votre token API pour {provider}',
|
||||||
tokenPlaceholder: 'sk-...',
|
tokenPlaceholder: 'sk-...',
|
||||||
setupDescription: 'Configurez le token de votre fournisseur IA pour utiliser l\'assistant.',
|
setupDescription: 'Configurez le token de votre fournisseur IA pour utiliser l\'assistant.',
|
||||||
@@ -180,6 +181,12 @@ const fr = {
|
|||||||
fontFamily: 'Police',
|
fontFamily: 'Police',
|
||||||
preview: 'Aper\u00e7u',
|
preview: 'Aper\u00e7u',
|
||||||
saving: 'Enregistrement...',
|
saving: 'Enregistrement...',
|
||||||
|
resetConfig: 'R\u00e9initialiser',
|
||||||
|
resetConfirm: '\u00cates-vous s\u00fbr ? Toutes les pr\u00e9f\u00e9rences seront effac\u00e9es.',
|
||||||
|
resetDone: 'Param\u00e8tres r\u00e9initialis\u00e9s.',
|
||||||
|
applyStarship: 'Appliquer starship',
|
||||||
|
starshipApplied: 'Th\u00e8me starship appliqu\u00e9 ! Red\u00e9marrez votre shell pour voir le r\u00e9sultat.',
|
||||||
|
starshipError: '\u00c9chec de l\u2019application du th\u00e8me starship.',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user