Compare commits
3 Commits
v0.3.3-bet
...
v0.3.3-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b55feaed09 | ||
|
|
54621bd960 | ||
|
|
6bad2948c5 |
@@ -477,25 +477,16 @@ func (s *Server) handleProvidersQuota(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "zai":
|
case "zai":
|
||||||
if p.APIKey == "" {
|
// Z.AI (GLM) est utilisé uniquement via Crush, pas de quota check externe
|
||||||
q.Error = "no API key"
|
q.Healthy = true
|
||||||
results = append(results, q)
|
q.Data = map[string]interface{}{"note": "crush-only"}
|
||||||
continue
|
case "claude", "anthropic":
|
||||||
}
|
// Claude Code n'a pas d'API externe, vérifier l'installation
|
||||||
req, _ := http.NewRequest("GET", "https://api.z.ai/api/monitor/usage/quota/limit", nil)
|
claudePath := "/usr/bin/claude"
|
||||||
req.Header.Set("Authorization", "Bearer "+p.APIKey)
|
if _, err := os.Stat(claudePath); err == nil {
|
||||||
req.Header.Set("Accept", "application/json")
|
q.Healthy = true
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
q.Error = err.Error()
|
|
||||||
} else {
|
} else {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
q.Error = "claude code not installed"
|
||||||
resp.Body.Close()
|
|
||||||
var data map[string]interface{}
|
|
||||||
if json.Unmarshal(body, &data) == nil {
|
|
||||||
q.Data = data
|
|
||||||
q.Healthy = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
q.Error = "quota not supported"
|
q.Error = "quota not supported"
|
||||||
|
|||||||
@@ -220,28 +220,47 @@ function PanelProfile({ config, editProfile, profileForm, setProfileForm, setEdi
|
|||||||
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return (
|
return (
|
||||||
<div className="config-card">
|
<div className="config-profile-center">
|
||||||
<div className="empty-state">{t('config.loadingProfile')}</div>
|
<div className="config-card">
|
||||||
|
<div className="empty-state">{t('config.loadingProfile')}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const personalKeys = Object.entries(profile).filter(([k, v]) => k !== 'preferences' && typeof v !== 'object')
|
||||||
|
const personalObj = Object.fromEntries(personalKeys)
|
||||||
|
const preferences = profile.preferences || null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="config-card">
|
<div className="config-profile-center">
|
||||||
<RenderFields obj={profile} path="" editing={editProfile} onChange={updateField} t={t} />
|
<div className="config-card">
|
||||||
<div className="config-card-actions">
|
<div className="section-title">{t('config.profileInfo') || 'Informations personnelles'}</div>
|
||||||
{editProfile ? (
|
<RenderFields obj={personalObj} path="" editing={editProfile} onChange={updateField} t={t} />
|
||||||
<>
|
</div>
|
||||||
<button className="primary sm" onClick={handleSaveProfile}>{t('config.save')}</button>
|
<div className="config-card">
|
||||||
<button className="ghost sm" onClick={() => setEditProfile(false)}>{t('config.cancel')}</button>
|
<div className="section-title">{t('config.profilePrefs') || 'Préférences'}</div>
|
||||||
</>
|
{preferences ? (
|
||||||
|
<RenderFields obj={preferences} path="preferences" editing={editProfile} onChange={updateField} t={t} />
|
||||||
) : (
|
) : (
|
||||||
<button className="primary sm" onClick={() => {
|
<div className="config-card-row"><span className="config-card-value" style={{ color: 'var(--text-disabled)' }}>—</span></div>
|
||||||
setProfileForm(config.profile ? JSON.parse(JSON.stringify(config.profile)) : {})
|
|
||||||
setEditProfile(true)
|
|
||||||
}}>{t('config.editProfile')}</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="config-card">
|
||||||
|
<div className="config-card-actions" style={{ justifyContent: 'center' }}>
|
||||||
|
{editProfile ? (
|
||||||
|
<>
|
||||||
|
<button className="primary sm" onClick={handleSaveProfile}>{t('config.save')}</button>
|
||||||
|
<button className="ghost sm" onClick={() => setEditProfile(false)}>{t('config.cancel')}</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button className="primary sm" onClick={() => {
|
||||||
|
setProfileForm(config.profile ? JSON.parse(JSON.stringify(config.profile)) : {})
|
||||||
|
setEditProfile(true)
|
||||||
|
}}>{t('config.editProfile')}</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -249,19 +268,10 @@ function PanelProfile({ config, editProfile, profileForm, setProfileForm, setEdi
|
|||||||
function RenderFields({ obj, path, editing, onChange, t }) {
|
function RenderFields({ obj, path, editing, onChange, t }) {
|
||||||
if (!obj || typeof obj !== 'object') return null
|
if (!obj || typeof obj !== 'object') return null
|
||||||
|
|
||||||
return Object.entries(obj).map(([key, value]) => {
|
return Object.entries(obj).filter(([, v]) => v === null || typeof v !== 'object').map(([key, value]) => {
|
||||||
const fieldPath = path ? `${path}.${key}` : key
|
const fieldPath = path ? `${path}.${key}` : key
|
||||||
const label = getFieldLabel(key, t)
|
const label = getFieldLabel(key, t)
|
||||||
|
|
||||||
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
||||||
return (
|
|
||||||
<div key={key} className="config-card-group">
|
|
||||||
<span className="config-card-group-label">{label}</span>
|
|
||||||
<RenderFields obj={value} path={fieldPath} editing={editing} onChange={onChange} t={t} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editing) {
|
if (editing) {
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
return (
|
return (
|
||||||
@@ -353,8 +363,7 @@ function PanelProviders({ providers, editProvider, providerForm, setProviderForm
|
|||||||
<div className="provider-card-top">
|
<div className="provider-card-top">
|
||||||
<div className="provider-card-identity">
|
<div className="provider-card-identity">
|
||||||
<span className="provider-card-name">{p.name}</span>
|
<span className="provider-card-name">{p.name}</span>
|
||||||
{p.apiKey && <span className="badge ok">{t('config.keyConfigured')}</span>}
|
{p.active && <span className="badge ok" style={{ marginLeft: 6 }}>active</span>}
|
||||||
{!p.apiKey && <span className="badge error">{t('config.noKey')}</span>}
|
|
||||||
{isValidationTarget && validationStatus?.valid && <span className="badge ok">{t('config.keyValid')}</span>}
|
{isValidationTarget && validationStatus?.valid && <span className="badge ok">{t('config.keyValid')}</span>}
|
||||||
{isValidationTarget && !validationStatus?.valid && <span className="badge error">{validationStatus?.error}</span>}
|
{isValidationTarget && !validationStatus?.valid && <span className="badge error">{validationStatus?.error}</span>}
|
||||||
</div>
|
</div>
|
||||||
@@ -462,6 +471,10 @@ function PanelUpdates({ updates, checking, updating, needsUpdateCount, installed
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PanelLocale({ language, keyboard, layouts, api, t }) {
|
function PanelLocale({ language, keyboard, layouts, api, t }) {
|
||||||
|
const { setLanguage, setKeyboard } = useI18n()
|
||||||
|
const [editLocale, setEditLocale] = useState(false)
|
||||||
|
const [draftLang, setDraftLang] = useState(language)
|
||||||
|
const [draftKbd, setDraftKbd] = useState(keyboard)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [toast, setToast] = useState(null)
|
const [toast, setToast] = useState(null)
|
||||||
|
|
||||||
@@ -473,7 +486,10 @@ function PanelLocale({ language, keyboard, layouts, api, t }) {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
await api.savePreferences({ language, keyboard_layout: keyboard })
|
await api.savePreferences({ language: draftLang, keyboard_layout: draftKbd })
|
||||||
|
setLanguage(draftLang)
|
||||||
|
setKeyboard(draftKbd)
|
||||||
|
setEditLocale(false)
|
||||||
showToast(t('config.saved'))
|
showToast(t('config.saved'))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(`${t('config.error')}: ${err.message}`)
|
showToast(`${t('config.error')}: ${err.message}`)
|
||||||
@@ -481,41 +497,67 @@ function PanelLocale({ language, keyboard, layouts, api, t }) {
|
|||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentLang = LANGUAGES.find(l => l.id === language)
|
||||||
|
const currentKbd = layouts.find(l => l.id === keyboard)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="config-card">
|
<div className="config-profile-center">
|
||||||
{toast && <div className="config-toast">{toast}</div>}
|
{toast && <div className="config-toast">{toast}</div>}
|
||||||
<div className="config-card-group">
|
<div className="config-card">
|
||||||
<span className="config-card-group-label">{t('config.language')}</span>
|
<div className="config-card-row">
|
||||||
<div className="chip-row">
|
<span className="config-card-label">{t('config.language')}</span>
|
||||||
{LANGUAGES.map(lang => (
|
<span className="config-card-value">{currentLang?.name || language}</span>
|
||||||
<div
|
</div>
|
||||||
key={lang.id}
|
<div className="config-card-row">
|
||||||
className={`chip ${language === lang.id ? 'active' : ''}`}
|
<span className="config-card-label">{t('config.keyboardLayout')}</span>
|
||||||
onClick={() => setLanguage(lang.id)}
|
<span className="config-card-value">{currentKbd?.name || keyboard}</span>
|
||||||
>
|
|
||||||
{lang.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="config-card-group">
|
{editLocale && (
|
||||||
<span className="config-card-group-label">{t('config.keyboardLayout')}</span>
|
<div className="config-card">
|
||||||
<div className="chip-row">
|
<div className="config-card-group">
|
||||||
{layouts.map(l => (
|
<span className="config-card-group-label">{t('config.language')}</span>
|
||||||
<div
|
<div className="chip-row">
|
||||||
key={l.id}
|
{LANGUAGES.map(lang => (
|
||||||
className={`chip ${keyboard === l.id ? 'active' : ''}`}
|
<div
|
||||||
onClick={() => setKeyboard(l.id)}
|
key={lang.id}
|
||||||
>
|
className={`chip ${draftLang === lang.id ? 'active' : ''}`}
|
||||||
{l.name}
|
onClick={() => setDraftLang(lang.id)}
|
||||||
|
>
|
||||||
|
{lang.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
<div className="config-card-group">
|
||||||
|
<span className="config-card-group-label">{t('config.keyboardLayout')}</span>
|
||||||
|
<div className="chip-row">
|
||||||
|
{layouts.map(l => (
|
||||||
|
<div
|
||||||
|
key={l.id}
|
||||||
|
className={`chip ${draftKbd === l.id ? 'active' : ''}`}
|
||||||
|
onClick={() => setDraftKbd(l.id)}
|
||||||
|
>
|
||||||
|
{l.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="config-card">
|
||||||
|
<div className="config-card-actions" style={{ justifyContent: 'center' }}>
|
||||||
|
{editLocale ? (
|
||||||
|
<>
|
||||||
|
<button className="primary sm" onClick={handleSave} disabled={saving}>
|
||||||
|
{saving ? t('config.saving') : t('config.save')}
|
||||||
|
</button>
|
||||||
|
<button className="ghost sm" onClick={() => setEditLocale(false)}>{t('config.cancel')}</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button className="primary sm" onClick={() => { setDraftLang(language); setDraftKbd(keyboard); setEditLocale(true) }}>{t('config.editProfile')}</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="config-card-actions">
|
|
||||||
<button className="primary sm" onClick={handleSave} disabled={saving}>
|
|
||||||
{saving ? t('config.saving') : t('config.save')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -285,6 +285,8 @@ export default function Studio({ api }) {
|
|||||||
const [streamToolCalls, setStreamToolCalls] = useState([])
|
const [streamToolCalls, setStreamToolCalls] = useState([])
|
||||||
const [loaded, setLoaded] = useState(false)
|
const [loaded, setLoaded] = useState(false)
|
||||||
const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 })
|
const [tokenInfo, setTokenInfo] = useState({ used: 0, max: 100000, summarizeAt: 80000 })
|
||||||
|
const [contextCollapsed, setContextCollapsed] = useState(false)
|
||||||
|
const [messagesCollapsed, setMessagesCollapsed] = useState(false)
|
||||||
const messagesEnd = useRef(null)
|
const messagesEnd = useRef(null)
|
||||||
const textareaRef = useRef(null)
|
const textareaRef = useRef(null)
|
||||||
const abortRef = useRef(null)
|
const abortRef = useRef(null)
|
||||||
@@ -336,12 +338,18 @@ export default function Studio({ api }) {
|
|||||||
|
|
||||||
const handleSummarize = useCallback(async () => {
|
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() }])
|
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'system', content: 'Résumé de la conversation en cours...', time: new Date().toISOString() }])
|
||||||
|
setContextCollapsed('animating')
|
||||||
try {
|
try {
|
||||||
const data = await api.summarizeChat()
|
const data = await api.summarizeChat()
|
||||||
setTokenInfo(prev => ({ ...prev, used: data.tokens || 0 }))
|
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() }])
|
setTimeout(() => {
|
||||||
|
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(), compressed: true }])
|
||||||
|
setContextCollapsed(true)
|
||||||
|
setMessagesCollapsed(true)
|
||||||
|
}, 600)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: `Erreur de résumé: ${err.message}`, time: new Date().toISOString() }])
|
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), role: 'system', content: `Erreur de résumé: ${err.message}`, time: new Date().toISOString() }])
|
||||||
|
setContextCollapsed(false)
|
||||||
}
|
}
|
||||||
}, [api])
|
}, [api])
|
||||||
|
|
||||||
@@ -396,7 +404,7 @@ export default function Studio({ api }) {
|
|||||||
if (text === '/model') {
|
if (text === '/model') {
|
||||||
api.getProviders().then(data => {
|
api.getProviders().then(data => {
|
||||||
const active = data.providers?.find(p => p.active)
|
const active = data.providers?.find(p => p.active)
|
||||||
const modelMsg = active ? `Provider: ${active.name}\nModèle: ${active.model}` : 'Aucun provider actif configuré'
|
const modelMsg = active ? active.name : 'Aucun provider actif configuré'
|
||||||
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: modelMsg, time: new Date().toISOString() }])
|
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: modelMsg, time: new Date().toISOString() }])
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'Erreur: impossible de récupérer les providers', time: new Date().toISOString() }])
|
setMessages(prev => [...prev, { id: Date.now().toString(), role: 'assistant', content: 'Erreur: impossible de récupérer les providers', time: new Date().toISOString() }])
|
||||||
@@ -525,6 +533,34 @@ export default function Studio({ api }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleToggleCollapsed = useCallback(() => {
|
||||||
|
setMessagesCollapsed(prev => !prev)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const renderMessages = () => {
|
||||||
|
if (messagesCollapsed && messages.length > 4) {
|
||||||
|
const visibleCount = 4
|
||||||
|
const hiddenCount = messages.length - visibleCount
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{messages.slice(0, visibleCount).map(msg => (
|
||||||
|
<FeedItem key={msg.id} msg={msg} />
|
||||||
|
))}
|
||||||
|
<div className="feed-collapsed-messages" onClick={handleToggleCollapsed}>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||||
|
</svg>
|
||||||
|
<span className="feed-collapsed-text">{hiddenCount} messages antérieurs compressés</span>
|
||||||
|
<span className="feed-collapsed-count">clic pour développer</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return messages.map(msg => (
|
||||||
|
<FeedItem key={msg.id} msg={msg} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return (
|
return (
|
||||||
<div className="studio-feed-layout">
|
<div className="studio-feed-layout">
|
||||||
@@ -540,27 +576,31 @@ export default function Studio({ api }) {
|
|||||||
return (
|
return (
|
||||||
<div className="studio-feed-layout">
|
<div className="studio-feed-layout">
|
||||||
<div className="studio-feed">
|
<div className="studio-feed">
|
||||||
{messages.map(msg => (
|
{renderMessages()}
|
||||||
<FeedItem key={msg.id} msg={msg} />
|
|
||||||
))}
|
|
||||||
{(streaming || streamThinking || loading || streamToolCalls.length > 0) && (
|
{(streaming || streamThinking || loading || streamToolCalls.length > 0) && (
|
||||||
<StreamingItem content={streaming} thinking={streamThinking} toolCalls={streamToolCalls} />
|
<StreamingItem content={streaming} thinking={streamThinking} toolCalls={streamToolCalls} />
|
||||||
)}
|
)}
|
||||||
<div ref={messagesEnd} />
|
<div ref={messagesEnd} style={{ height: '24px' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="studio-input-area">
|
<div className="studio-input-area">
|
||||||
<div className="studio-token-bar">
|
<div className={`studio-token-bar ${contextCollapsed === true ? 'compressed' : ''}`}>
|
||||||
<div className="studio-token-track">
|
<div className={`studio-token-track ${contextCollapsed === true ? 'compressed' : ''}`}>
|
||||||
<div
|
<div
|
||||||
className={`studio-token-fill ${tokenInfo.used >= tokenInfo.summarizeAt ? 'warn' : ''}`}
|
className={`studio-token-fill ${tokenInfo.used >= tokenInfo.summarizeAt ? 'warn' : ''} ${contextCollapsed === true ? 'compressed' : ''} ${contextCollapsed === 'animating' ? 'animating' : ''}`}
|
||||||
style={{ width: `${Math.min(100, (tokenInfo.used / tokenInfo.max) * 100)}%` }}
|
style={{ width: `${Math.min(100, (tokenInfo.used / tokenInfo.max) * 100)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="studio-token-text">
|
<span className={`studio-token-text ${contextCollapsed === true ? 'compressed' : ''}`}>
|
||||||
{(tokenInfo.used / 1000).toFixed(1)}k / {(tokenInfo.max / 1000).toFixed(0)}k tokens
|
{(tokenInfo.used / 1000).toFixed(1)}k / {(tokenInfo.max / 1000).toFixed(0)}k tokens
|
||||||
{tokenInfo.used >= tokenInfo.summarizeAt && ' · résumé automatique déclenché'}
|
{contextCollapsed === true && ' · compressé'}
|
||||||
|
{tokenInfo.used >= tokenInfo.summarizeAt && contextCollapsed !== true && ' · résumé auto.'}
|
||||||
</span>
|
</span>
|
||||||
|
{contextCollapsed === true && (
|
||||||
|
<button className="ghost sm" onClick={handleToggleCollapsed} style={{ marginLeft: '8px', fontSize: '10px' }}>
|
||||||
|
voir plus
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="studio-input-row">
|
<div className="studio-input-row">
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ const en = {
|
|||||||
installed: 'Installed',
|
installed: 'Installed',
|
||||||
missing: 'Missing',
|
missing: 'Missing',
|
||||||
editProfile: 'Edit',
|
editProfile: 'Edit',
|
||||||
|
profileInfo: 'Personal Info',
|
||||||
|
profilePrefs: 'Preferences',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
editProvider: 'Configure',
|
editProvider: 'Configure',
|
||||||
validateKey: 'Validate',
|
validateKey: 'Validate',
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ const fr = {
|
|||||||
installed: 'Install\u00e9',
|
installed: 'Install\u00e9',
|
||||||
missing: 'Manquant',
|
missing: 'Manquant',
|
||||||
editProfile: 'Modifier',
|
editProfile: 'Modifier',
|
||||||
|
profileInfo: 'Informations personnelles',
|
||||||
|
profilePrefs: 'Préférences',
|
||||||
editProvider: 'Configurer',
|
editProvider: 'Configurer',
|
||||||
validateKey: 'Valider',
|
validateKey: 'Valider',
|
||||||
validating: 'V\u00e9rification...',
|
validating: 'V\u00e9rification...',
|
||||||
|
|||||||
@@ -435,6 +435,10 @@ input::placeholder { color: var(--text-disabled); }
|
|||||||
|
|
||||||
.config-panel-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
.config-panel-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
.config-panel-body { flex: 1; overflow-y: auto; padding: 16px 28px 28px; }
|
.config-panel-body { flex: 1; overflow-y: auto; padding: 16px 28px 28px; }
|
||||||
|
.config-profile-center {
|
||||||
|
max-width: 540px; margin: 0 auto; width: 100%;
|
||||||
|
display: flex; flex-direction: column; gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.config-card {
|
.config-card {
|
||||||
background: var(--bg-card); border: 1px solid var(--border);
|
background: var(--bg-card); border: 1px solid var(--border);
|
||||||
@@ -622,6 +626,7 @@ input::placeholder { color: var(--text-disabled); }
|
|||||||
.dash-cmd-text {
|
.dash-cmd-text {
|
||||||
font-size: 11px; font-family: var(--font-mono); color: var(--text-secondary);
|
font-size: 11px; font-family: var(--font-mono); color: var(--text-secondary);
|
||||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
flex: 1; min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Services */
|
/* Services */
|
||||||
@@ -804,6 +809,12 @@ input::placeholder { color: var(--text-disabled); }
|
|||||||
.studio-token-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.4s, background 0.3s; }
|
.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-fill.warn { background: var(--warning); }
|
||||||
.studio-token-fill.compressed { height: 2px; }
|
.studio-token-fill.compressed { height: 2px; }
|
||||||
|
.studio-token-fill.animating { animation: compress-pulse 0.6s ease-in-out; }
|
||||||
|
@keyframes compress-pulse {
|
||||||
|
0% { height: 3px; opacity: 1; }
|
||||||
|
50% { height: 5px; opacity: 0.8; background: var(--accent-light); }
|
||||||
|
100% { height: 2px; opacity: 1; }
|
||||||
|
}
|
||||||
.studio-token-text { font-size: 10px; font-family: var(--font-mono); color: var(--text-tertiary); white-space: nowrap; }
|
.studio-token-text { font-size: 10px; font-family: var(--font-mono); color: var(--text-tertiary); white-space: nowrap; }
|
||||||
.studio-token-text.compressed { font-size: 9px; }
|
.studio-token-text.compressed { font-size: 9px; }
|
||||||
.studio-token-track.compressed { height: 2px; }
|
.studio-token-track.compressed { height: 2px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user