mirror of
https://github.com/mudler/LocalAI
synced 2026-04-21 13:27:21 +00:00
chore(ui): improve visibility of forms, color palette
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
parent
833b7e8557
commit
410d100cc3
20 changed files with 1227 additions and 538 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -65,7 +65,7 @@ export default function App() {
|
|||
<div className="app-footer-inner">
|
||||
{version && (
|
||||
<span className="app-footer-version">
|
||||
LocalAI <span style={{ color: 'var(--color-primary)', fontWeight: 500 }}>{version}</span>
|
||||
LocalAI <span style={{ fontWeight: 500 }}>{version}</span>
|
||||
</span>
|
||||
)}
|
||||
<div className="app-footer-links">
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
export default function SettingRow({ label, description, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: 'var(--spacing-sm) 0',
|
||||
borderBottom: '1px solid var(--color-border-subtle)',
|
||||
}}>
|
||||
<div style={{ flex: 1, marginRight: 'var(--spacing-md)' }}>
|
||||
<div style={{ fontSize: '0.875rem', fontWeight: 500 }}>{label}</div>
|
||||
{description && <div style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', marginTop: 2 }}>{description}</div>}
|
||||
<div className="form-row">
|
||||
<div className="form-row__label">
|
||||
<div className="form-row__label-text">{label}</div>
|
||||
{description && <div className="form-row__hint">{description}</div>}
|
||||
</div>
|
||||
<div style={{ flexShrink: 0 }}>{children}</div>
|
||||
<div className="form-row__control">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,14 @@
|
|||
export default function Toggle({ checked, onChange, disabled }) {
|
||||
return (
|
||||
<label style={{
|
||||
position: 'relative', display: 'inline-block', width: 40, height: 22, cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}>
|
||||
<label className={`toggle${checked ? ' toggle--on' : ''}${disabled ? ' toggle--disabled' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked || false}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
disabled={disabled}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<span style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 22,
|
||||
background: checked ? 'var(--color-primary)' : 'var(--color-toggle-off)',
|
||||
transition: 'background 200ms',
|
||||
}}>
|
||||
<span style={{
|
||||
position: 'absolute', top: 2, left: checked ? 20 : 2,
|
||||
width: 18, height: 18, borderRadius: '50%',
|
||||
background: 'var(--color-text-inverse)', transition: 'left 200ms',
|
||||
boxShadow: 'var(--shadow-sm)',
|
||||
}} />
|
||||
<span className="toggle__track">
|
||||
<span className="toggle__thumb" />
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ html {
|
|||
|
||||
body {
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--leading-normal);
|
||||
min-height: 100%;
|
||||
background-color: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
|
|
@ -35,8 +38,14 @@ body {
|
|||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.3;
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
h1 { font-size: var(--text-2xl); font-weight: var(--font-weight-semibold); }
|
||||
h2 { font-size: var(--text-xl); font-weight: var(--font-weight-semibold); }
|
||||
h3 { font-size: var(--text-lg); font-weight: var(--font-weight-semibold); }
|
||||
h4 { font-size: var(--text-base); font-weight: var(--font-weight-semibold); }
|
||||
h5, h6 { font-size: var(--text-sm); font-weight: var(--font-weight-semibold); }
|
||||
|
||||
code, pre {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
|
|
|
|||
|
|
@ -356,7 +356,15 @@ export default function AgentChat() {
|
|||
}, [input, processing, name, activeId, addToast, userId, addMessage, nextId])
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
!e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.nativeEvent?.isComposing &&
|
||||
e.keyCode !== 229
|
||||
) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ function wsUrl(path) {
|
|||
}
|
||||
|
||||
const STREAM_BADGE = {
|
||||
stdout: { bg: 'rgba(59,130,246,0.15)', color: '#60a5fa', label: 'stdout' },
|
||||
stderr: { bg: 'rgba(239,68,68,0.15)', color: '#f87171', label: 'stderr' },
|
||||
stdout: { bg: 'var(--color-info-light)', color: 'var(--color-log-info)', label: 'stdout' },
|
||||
stderr: { bg: 'var(--color-error-light)', color: 'var(--color-log-stderr)', label: 'stderr' },
|
||||
}
|
||||
|
||||
// Detail view: log lines for a specific model
|
||||
|
|
|
|||
|
|
@ -262,13 +262,13 @@ export default function Backends() {
|
|||
marginBottom: 'var(--spacing-md)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: 'var(--spacing-sm) var(--spacing-md)',
|
||||
background: 'var(--color-warning-bg, #fef3cd)',
|
||||
border: '1px solid var(--color-warning, #ffc107)',
|
||||
background: 'var(--color-warning-light)',
|
||||
border: '1px solid var(--color-warning-border)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)' }}>
|
||||
<i className="fas fa-arrow-up" style={{ color: 'var(--color-warning, #856404)' }} />
|
||||
<span style={{ color: 'var(--color-warning, #856404)', fontWeight: 500, fontSize: '0.875rem' }}>
|
||||
<i className="fas fa-arrow-up" style={{ color: 'var(--color-warning)' }} />
|
||||
<span style={{ color: 'var(--color-warning)', fontWeight: 500, fontSize: 'var(--text-sm)' }}>
|
||||
{Object.keys(upgrades).length} backend{Object.keys(upgrades).length > 1 ? 's have' : ' has'} updates available
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -447,20 +447,18 @@ export default function Backends() {
|
|||
{/* Status */}
|
||||
<td>
|
||||
{isProcessing ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-xs)' }}>
|
||||
<div style={{
|
||||
width: 80, height: 6, background: 'var(--color-bg-tertiary)',
|
||||
borderRadius: 3, overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${op.progress || 0}%`, height: '100%',
|
||||
background: 'var(--color-primary)',
|
||||
borderRadius: 3, transition: 'width 300ms',
|
||||
}} />
|
||||
<div className="inline-install">
|
||||
<div className="inline-install__row">
|
||||
<div className="operation-spinner" />
|
||||
<span className="inline-install__label">
|
||||
{op.isDeletion ? 'Deleting...' : op.isQueued ? 'Queued' : `Installing${op.progress > 0 ? ` · ${Math.round(op.progress)}%` : '...'}`}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: '0.6875rem', color: 'var(--color-text-muted)' }}>
|
||||
{op.isDeletion ? 'Deleting...' : op.isQueued ? 'Queued' : 'Installing...'}
|
||||
</span>
|
||||
{op.progress > 0 && (
|
||||
<div className="operation-bar-container" style={{ flex: 'none', width: '120px', marginTop: 4 }}>
|
||||
<div className="operation-bar" style={{ width: `${op.progress}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : b.installed ? (
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
|
|
@ -468,14 +466,14 @@ export default function Backends() {
|
|||
<i className="fas fa-check" style={{ fontSize: '0.5rem', marginRight: 2 }} /> Installed
|
||||
</span>
|
||||
{upgrades[b.name] && (
|
||||
<span className="badge" style={{ fontSize: '0.625rem', background: '#fef3cd', color: '#856404' }}>
|
||||
<span className="badge" style={{ fontSize: '0.625rem', background: 'var(--color-warning-light)', color: 'var(--color-warning)' }}>
|
||||
<i className="fas fa-arrow-up" style={{ fontSize: '0.5rem', marginRight: 2 }} />
|
||||
{upgrades[b.name].available_version ? `v${upgrades[b.name].available_version}` : 'Update'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="badge" style={{ background: 'var(--color-bg-tertiary)', color: 'var(--color-text-muted)' }}>
|
||||
<span className="badge" style={{ background: 'var(--color-surface-sunken)', color: 'var(--color-text-muted)', border: '1px solid var(--color-border-default)' }}>
|
||||
<i className="fas fa-circle" style={{ fontSize: '0.5rem', marginRight: 2 }} /> Not Installed
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -697,7 +697,17 @@ export default function Chat() {
|
|||
}, [activeChat, isStreaming, sendMessage, updateChatSettings])
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
// Only Enter (no modifiers, no IME composition) sends.
|
||||
// Shift+Enter, Ctrl+Enter, Meta+Enter, Alt+Enter all fall through to default textarea behavior (newline).
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
!e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey &&
|
||||
!e.altKey &&
|
||||
!e.nativeEvent?.isComposing &&
|
||||
e.keyCode !== 229
|
||||
) {
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -420,7 +420,7 @@ export default function Home() {
|
|||
<span className="home-loaded-dot" />
|
||||
<span className="home-loaded-text">{loadedCount} model{loadedCount !== 1 ? 's' : ''} loaded</span>
|
||||
<div className="home-loaded-list">
|
||||
{loadedModels.map(m => (
|
||||
{[...loadedModels].sort((a, b) => a.id.localeCompare(b.id)).map(m => (
|
||||
<span key={m.id} className="home-loaded-item">
|
||||
{m.id}
|
||||
<button onClick={() => handleStopModel(m.id)} title="Stop model">
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export default function ImageGen() {
|
|||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-image" style={{ marginRight: 8, color: 'var(--color-accent)' }} />Image Generation</h1>
|
||||
<h1 className="page-title"><i className="fas fa-image" /> Image Generation</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
|
|
@ -100,10 +100,10 @@ export default function ImageGen() {
|
|||
<textarea className="textarea" value={negativePrompt} onChange={(e) => setNegativePrompt(e.target.value)} placeholder="What to avoid..." rows={2} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-sm)' }}>
|
||||
<div className="form-grid-2col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Size</label>
|
||||
<select className="model-selector" value={size} onChange={(e) => setSize(e.target.value)} style={{ width: '100%' }}>
|
||||
<select className="input btn-full" value={size} onChange={(e) => setSize(e.target.value)}>
|
||||
{SIZES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -117,7 +117,7 @@ export default function ImageGen() {
|
|||
<i className="fas fa-chevron-right" /> Advanced Settings
|
||||
</div>
|
||||
{showAdvanced && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-sm)', marginBottom: 'var(--spacing-md)' }}>
|
||||
<div className="form-grid-2col">
|
||||
<div className="form-group"><label className="form-label">Steps</label><input className="input" type="number" value={steps} onChange={(e) => setSteps(e.target.value)} placeholder="20" /></div>
|
||||
<div className="form-group"><label className="form-label">Seed</label><input className="input" type="number" value={seed} onChange={(e) => setSeed(e.target.value)} placeholder="Random" /></div>
|
||||
</div>
|
||||
|
|
@ -127,17 +127,17 @@ export default function ImageGen() {
|
|||
<i className="fas fa-chevron-right" /> Image Inputs
|
||||
</div>
|
||||
{showImageInputs && (
|
||||
<div style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
<>
|
||||
<div className="form-group"><label className="form-label">Source Image (img2img)</label><input ref={sourceRef} type="file" accept="image/*" onChange={handleSourceImage} className="input" /></div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Reference Images</label>
|
||||
<input ref={refRef} type="file" accept="image/*" multiple onChange={handleRefImages} className="input" />
|
||||
{refImages.length > 0 && <span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>{refImages.length} image(s) added</span>}
|
||||
{refImages.length > 0 && <span className="form-field__hint">{refImages.length} image(s) added</span>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button type="submit" className="btn btn-primary" disabled={loading} style={{ width: '100%' }}>
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? <><LoadingSpinner size="sm" /> Generating...</> : <><i className="fas fa-wand-magic-sparkles" /> Generate</>}
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -38,20 +38,20 @@ const URI_FORMATS = [
|
|||
],
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-box', color: '#22d3ee', title: 'OCI Registry',
|
||||
icon: 'fas fa-box', color: 'var(--color-data-8)', title: 'OCI Registry',
|
||||
examples: [
|
||||
{ prefix: 'oci://', suffix: 'registry.example.com/model:tag', desc: 'OCI container registry' },
|
||||
{ prefix: 'ocifile://', suffix: '/path/to/image.tar', desc: 'Local OCI tarball file' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-cube', color: '#818cf8', title: 'Ollama',
|
||||
icon: 'fas fa-cube', color: 'var(--color-data-1)', title: 'Ollama',
|
||||
examples: [
|
||||
{ prefix: 'ollama://', suffix: 'llama2:7b', desc: 'Ollama model format' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-code', color: '#f472b6', title: 'YAML Configuration Files',
|
||||
icon: 'fas fa-code', color: 'var(--color-data-7)', title: 'YAML Configuration Files',
|
||||
examples: [
|
||||
{ prefix: '', suffix: 'https://example.com/model.yaml', desc: 'Remote YAML config file' },
|
||||
{ prefix: 'file://', suffix: '/path/to/config.yaml', desc: 'Local YAML config file' },
|
||||
|
|
@ -437,7 +437,7 @@ export default function ImportModel() {
|
|||
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||||
<div style={{ padding: 'var(--spacing-md)', borderBottom: '1px solid var(--color-border-default)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<i className="fas fa-code" style={{ color: '#d946ef' }} />
|
||||
<i className="fas fa-code" style={{ color: 'var(--color-data-3)' }} />
|
||||
YAML Configuration Editor
|
||||
</h2>
|
||||
<button className="btn btn-secondary" style={{ fontSize: '0.75rem' }} onClick={() => { navigator.clipboard.writeText(yamlContent); addToast('Copied to clipboard', 'success') }}>
|
||||
|
|
|
|||
|
|
@ -257,8 +257,8 @@ export default function Models() {
|
|||
<div className="page">
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h1 className="page-title">Model Gallery</h1>
|
||||
<p className="page-subtitle">Discover and install AI models for your workflows</p>
|
||||
<h1 className="page-title">Install Models</h1>
|
||||
<p className="page-subtitle">Browse and install AI models from the gallery</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', fontSize: '0.8125rem' }}>
|
||||
|
|
@ -461,15 +461,14 @@ export default function Models() {
|
|||
{/* Status */}
|
||||
<td>
|
||||
{installing ? (
|
||||
<div>
|
||||
<span style={{ fontSize: '0.75rem', color: 'var(--color-primary)' }}>
|
||||
<i className="fas fa-spinner fa-spin" /> Installing...
|
||||
</span>
|
||||
<div className="inline-install">
|
||||
<div className="inline-install__row">
|
||||
<div className="operation-spinner" />
|
||||
<span className="inline-install__label">Installing{progress > 0 ? ` · ${Math.round(progress)}%` : '...'}</span>
|
||||
</div>
|
||||
{progress > 0 && (
|
||||
<div style={{ marginTop: '4px', width: '100%', maxWidth: '120px' }}>
|
||||
<div style={{ height: 3, background: 'var(--color-bg-tertiary)', borderRadius: 2, overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: `${progress}%`, background: 'var(--color-primary)', borderRadius: 2, transition: 'width 300ms' }} />
|
||||
</div>
|
||||
<div className="operation-bar-container" style={{ flex: 'none', width: '120px', marginTop: 4 }}>
|
||||
<div className="operation-bar" style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -478,7 +477,7 @@ export default function Models() {
|
|||
<i className="fas fa-check-circle" /> Installed
|
||||
</span>
|
||||
) : (
|
||||
<span className="badge" style={{ background: 'var(--color-bg-tertiary)', color: 'var(--color-text-muted)' }}>
|
||||
<span className="badge" style={{ background: 'var(--color-surface-sunken)', color: 'var(--color-text-muted)', border: '1px solid var(--color-border-default)' }}>
|
||||
<i className="fas fa-circle" /> Not Installed
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ function wsUrl(path) {
|
|||
}
|
||||
|
||||
const STREAM_BADGE = {
|
||||
stdout: { bg: 'rgba(59,130,246,0.15)', color: '#60a5fa', label: 'stdout' },
|
||||
stderr: { bg: 'rgba(239,68,68,0.15)', color: '#f87171', label: 'stderr' },
|
||||
stdout: { bg: 'var(--color-info-light)', color: 'var(--color-log-info)', label: 'stdout' },
|
||||
stderr: { bg: 'var(--color-error-light)', color: 'var(--color-log-stderr)', label: 'stderr' },
|
||||
}
|
||||
|
||||
export default function NodeBackendLogs() {
|
||||
|
|
|
|||
|
|
@ -20,11 +20,14 @@ const statusBadgeClass = {
|
|||
|
||||
function FormSection({ icon, title, children }) {
|
||||
return (
|
||||
<div style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
<h4 style={{ marginBottom: 'var(--spacing-sm)', display: 'flex', alignItems: 'center', gap: '0.5em' }}>
|
||||
{icon && <i className={icon} />} {title}
|
||||
</h4>
|
||||
{children}
|
||||
<div className="form-group">
|
||||
<div className="form-group__title">
|
||||
{icon && <i className={icon} />}
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
<div className="form-group__body">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -62,43 +65,37 @@ function ProgressMonitor({ job, onClose }) {
|
|||
const message = latestEvent?.message ?? job.message ?? ''
|
||||
|
||||
return (
|
||||
<div className="card" style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-sm)' }}>
|
||||
<h4 style={{ margin: 0 }}>
|
||||
<i className="fas fa-chart-line" style={{ marginRight: '0.5em' }} />
|
||||
Progress: {job.model}
|
||||
<div className="card quantize-progress-card">
|
||||
<div className="quantize-progress-card__header">
|
||||
<h4 className="quantize-progress-card__title">
|
||||
<i className="fas fa-chart-line" />
|
||||
<span>Progress: {job.model}</span>
|
||||
</h4>
|
||||
<button className="btn btn-sm" onClick={onClose}><i className="fas fa-times" /></button>
|
||||
<button type="button" className="btn btn-ghost btn-sm" onClick={onClose} title="Close">
|
||||
<i className="fas fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
<div className="quantize-progress-card__status">
|
||||
<span className={`badge ${statusBadgeClass[status] || ''}`}>{status}</span>
|
||||
<span style={{ marginLeft: '1em', fontSize: '0.9em', color: 'var(--color-text-secondary)' }}>{message}</span>
|
||||
{message && <span className="quantize-progress-card__message">{message}</span>}
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
width: '100%', height: '24px', borderRadius: '12px',
|
||||
background: 'var(--color-bg-tertiary)', overflow: 'hidden', marginBottom: 'var(--spacing-sm)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${Math.min(progress, 100)}%`, height: '100%',
|
||||
background: status === 'failed' ? 'var(--color-error)' : 'var(--color-primary)',
|
||||
transition: 'width 0.3s ease', borderRadius: '12px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: '0.75em', fontWeight: 'bold', color: '#fff',
|
||||
}}>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className={`progress-bar__fill${status === 'failed' ? ' progress-bar__fill--error' : ''}`}
|
||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||
>
|
||||
{progress > 8 ? `${progress.toFixed(1)}%` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log tail */}
|
||||
<div style={{
|
||||
maxHeight: '150px', overflow: 'auto', fontSize: '0.8em',
|
||||
background: 'var(--color-bg-secondary)', borderRadius: '6px', padding: '0.5em',
|
||||
fontFamily: 'monospace',
|
||||
}}>
|
||||
<div className="log-tail">
|
||||
{events.slice(-20).map((ev, i) => (
|
||||
<div key={i} style={{ color: ev.status === 'failed' ? 'var(--color-error)' : 'var(--color-text-secondary)' }}>
|
||||
<div
|
||||
key={i}
|
||||
className={`log-tail__line${ev.status === 'failed' ? ' log-tail__line--error' : ''}`}
|
||||
>
|
||||
[{ev.status}] {ev.message}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -140,59 +137,52 @@ function ImportPanel({ job, onRefresh }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="card" style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
<h4 style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
<i className="fas fa-file-export" style={{ marginRight: '0.5em' }} />
|
||||
Output
|
||||
<div className="card quantize-import-card">
|
||||
<h4 className="quantize-import-card__title">
|
||||
<i className="fas fa-file-export" />
|
||||
<span>Output</span>
|
||||
</h4>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-error" style={{ marginBottom: 'var(--spacing-sm)' }}>{error}</div>
|
||||
)}
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', flexWrap: 'wrap', alignItems: 'flex-end' }}>
|
||||
{/* Download */}
|
||||
<div className="quantize-import-card__row">
|
||||
<a
|
||||
href={quantizationApi.downloadUrl(job.id)}
|
||||
className="btn btn-secondary"
|
||||
download
|
||||
>
|
||||
<i className="fas fa-download" style={{ marginRight: '0.4em' }} />
|
||||
Download GGUF
|
||||
<i className="fas fa-download" />
|
||||
<span>Download GGUF</span>
|
||||
</a>
|
||||
|
||||
{/* Import */}
|
||||
{job.import_status === 'completed' ? (
|
||||
<a href={`/app/chat/${encodeURIComponent(job.import_model_name)}`} className="btn btn-success">
|
||||
<i className="fas fa-comments" style={{ marginRight: '0.4em' }} />
|
||||
Chat with {job.import_model_name}
|
||||
<a href={`/app/chat/${encodeURIComponent(job.import_model_name)}`} className="btn btn-primary">
|
||||
<i className="fas fa-comments" />
|
||||
<span>Chat with {job.import_model_name}</span>
|
||||
</a>
|
||||
) : job.import_status === 'importing' ? (
|
||||
<button className="btn" disabled>
|
||||
<i className="fas fa-spinner fa-spin" style={{ marginRight: '0.4em' }} />
|
||||
Importing... {job.import_message}
|
||||
<button type="button" className="btn btn-secondary" disabled>
|
||||
<i className="fas fa-spinner fa-spin" />
|
||||
<span>Importing... {job.import_message}</span>
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
className="input"
|
||||
className="input quantize-import-card__name"
|
||||
placeholder="Model name (auto-generated if empty)"
|
||||
value={modelName}
|
||||
onChange={e => setModelName(e.target.value)}
|
||||
style={{ maxWidth: '280px' }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={handleImport} disabled={importing}>
|
||||
<i className="fas fa-file-import" style={{ marginRight: '0.4em' }} />
|
||||
Import to LocalAI
|
||||
<button type="button" className="btn btn-primary" onClick={handleImport} disabled={importing}>
|
||||
<i className="fas fa-file-import" />
|
||||
<span>Import to LocalAI</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{job.import_status === 'failed' && (
|
||||
<div className="alert alert-error" style={{ marginTop: 'var(--spacing-sm)' }}>
|
||||
Import failed: {job.import_message}
|
||||
</div>
|
||||
<div className="alert alert-error">Import failed: {job.import_message}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -293,37 +283,38 @@ export default function Quantize() {
|
|||
const effectiveQuantType = useCustomQuant ? customQuantType : quantType
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '900px', margin: '0 auto', padding: 'var(--spacing-md)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)', marginBottom: 'var(--spacing-md)' }}>
|
||||
<h2 style={{ margin: 0 }}>
|
||||
<i className="fas fa-compress" style={{ marginRight: '0.4em' }} />
|
||||
Model Quantization
|
||||
</h2>
|
||||
<span className="badge badge-warning" style={{ fontSize: '0.7em' }}>Experimental</span>
|
||||
<div className="page quantize-page">
|
||||
<div className="page-header quantize-page__header">
|
||||
<div>
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-compress" /> Model Quantization
|
||||
</h1>
|
||||
<p className="page-subtitle">Quantize and import GGUF models directly into LocalAI</p>
|
||||
</div>
|
||||
<span className="badge badge-warning">Experimental</span>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="card" style={{ borderColor: 'var(--color-error)', marginBottom: 'var(--spacing-md)' }}>
|
||||
<div style={{ color: 'var(--color-error)' }}><i className="fas fa-exclamation-triangle" /> {error}</div>
|
||||
<div className="alert alert-error">
|
||||
<i className="fas fa-exclamation-triangle" /> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── New Job Form ── */}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="card" style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
<FormSection icon="fas fa-cube" title="Model">
|
||||
<input
|
||||
className="input"
|
||||
placeholder="HuggingFace model name (e.g. meta-llama/Llama-3.2-1B) or local path"
|
||||
value={model}
|
||||
onChange={e => setModel(e.target.value)}
|
||||
required
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</FormSection>
|
||||
<form onSubmit={handleSubmit} className="card quantize-form">
|
||||
<FormSection icon="fas fa-cube" title="Model">
|
||||
<input
|
||||
className="input btn-full"
|
||||
placeholder="HuggingFace model name (e.g. meta-llama/Llama-3.2-1B) or local path"
|
||||
value={model}
|
||||
onChange={e => setModel(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
<div className="form-grid-2col">
|
||||
<FormSection icon="fas fa-sliders-h" title="Quantization Type">
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<div className="quantize-form__quant-row">
|
||||
<select
|
||||
className="input"
|
||||
value={useCustomQuant ? '__custom__' : quantType}
|
||||
|
|
@ -335,7 +326,6 @@ export default function Quantize() {
|
|||
setQuantType(e.target.value)
|
||||
}
|
||||
}}
|
||||
style={{ maxWidth: '200px' }}
|
||||
>
|
||||
{QUANT_PRESETS.map(q => (
|
||||
<option key={q} value={q}>{q}</option>
|
||||
|
|
@ -345,11 +335,10 @@ export default function Quantize() {
|
|||
{useCustomQuant && (
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Enter custom quantization type"
|
||||
placeholder="Custom quantization type"
|
||||
value={customQuantType}
|
||||
onChange={e => setCustomQuantType(e.target.value)}
|
||||
required
|
||||
style={{ maxWidth: '220px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -357,37 +346,37 @@ export default function Quantize() {
|
|||
|
||||
<FormSection icon="fas fa-server" title="Backend">
|
||||
<select
|
||||
className="input"
|
||||
className="input btn-full"
|
||||
value={backend}
|
||||
onChange={e => setBackend(e.target.value)}
|
||||
style={{ maxWidth: '280px' }}
|
||||
>
|
||||
{backends.map(b => (
|
||||
<option key={b.name || b} value={b.name || b}>{b.name || b}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormSection>
|
||||
</div>
|
||||
|
||||
<FormSection icon="fas fa-key" title="HuggingFace Token (optional)">
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
placeholder="hf_... (required for gated models)"
|
||||
value={hfToken}
|
||||
onChange={e => setHfToken(e.target.value)}
|
||||
style={{ maxWidth: '400px' }}
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection icon="fas fa-key" title="HuggingFace Token (optional)">
|
||||
<input
|
||||
className="input btn-full"
|
||||
type="password"
|
||||
placeholder="hf_... (required for gated models)"
|
||||
value={hfToken}
|
||||
onChange={e => setHfToken(e.target.value)}
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
<div className="form-group__actions">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={submitting || !model || (useCustomQuant && !customQuantType)}
|
||||
>
|
||||
{submitting ? (
|
||||
<><i className="fas fa-spinner fa-spin" style={{ marginRight: '0.4em' }} /> Starting...</>
|
||||
<><i className="fas fa-spinner fa-spin" /> <span>Starting...</span></>
|
||||
) : (
|
||||
<><i className="fas fa-play" style={{ marginRight: '0.4em' }} /> Quantize ({effectiveQuantType})</>
|
||||
<><i className="fas fa-play" /> <span>Quantize ({effectiveQuantType})</span></>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -413,20 +402,20 @@ export default function Quantize() {
|
|||
|
||||
{/* ── Jobs List ── */}
|
||||
{jobs.length > 0 && (
|
||||
<div className="card">
|
||||
<h4 style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
<i className="fas fa-list" style={{ marginRight: '0.5em' }} />
|
||||
Jobs
|
||||
<div className="card quantize-jobs">
|
||||
<h4 className="quantize-jobs__title">
|
||||
<i className="fas fa-list" />
|
||||
<span>Jobs</span>
|
||||
</h4>
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.9em' }}>
|
||||
<div className="quantize-jobs__scroll">
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid var(--color-border)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '0.5em' }}>Model</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.5em' }}>Quant</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.5em' }}>Status</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.5em' }}>Created</th>
|
||||
<th style={{ textAlign: 'right', padding: '0.5em' }}>Actions</th>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th>Quant</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th style={{ textAlign: 'right' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -436,37 +425,29 @@ export default function Quantize() {
|
|||
return (
|
||||
<tr
|
||||
key={job.id}
|
||||
style={{
|
||||
borderBottom: '1px solid var(--color-border)',
|
||||
background: isSelected ? 'var(--color-bg-secondary)' : undefined,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
className={isSelected ? 'is-selected' : ''}
|
||||
onClick={() => setSelectedJob(job)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<td style={{ padding: '0.5em', maxWidth: '250px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{job.model}
|
||||
</td>
|
||||
<td style={{ padding: '0.5em' }}>
|
||||
<code>{job.quantization_type}</code>
|
||||
</td>
|
||||
<td style={{ padding: '0.5em' }}>
|
||||
<td className="data-table__truncate">{job.model}</td>
|
||||
<td><code>{job.quantization_type}</code></td>
|
||||
<td>
|
||||
<span className={`badge ${statusBadgeClass[job.status] || ''}`}>{job.status}</span>
|
||||
{job.import_status === 'completed' && (
|
||||
<span className="badge badge-success" style={{ marginLeft: '0.3em' }}>imported</span>
|
||||
<span className="badge badge-success" style={{ marginLeft: 'var(--spacing-xs)' }}>imported</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '0.5em', fontSize: '0.85em', color: 'var(--color-text-secondary)' }}>
|
||||
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-secondary)' }}>
|
||||
{new Date(job.created_at).toLocaleString()}
|
||||
</td>
|
||||
<td style={{ padding: '0.5em', textAlign: 'right' }}>
|
||||
<div style={{ display: 'flex', gap: '0.3em', justifyContent: 'flex-end' }} onClick={e => e.stopPropagation()}>
|
||||
{isActive && (
|
||||
<button className="btn btn-sm btn-error" onClick={() => handleStop(job.id)} title="Stop">
|
||||
<td>
|
||||
<div className="data-table__actions" onClick={e => e.stopPropagation()}>
|
||||
{isActive ? (
|
||||
<button type="button" className="btn btn-sm btn-danger" onClick={() => handleStop(job.id)} title="Stop">
|
||||
<i className="fas fa-stop" />
|
||||
</button>
|
||||
)}
|
||||
{!isActive && (
|
||||
<button className="btn btn-sm" onClick={() => handleDelete(job.id)} title="Delete">
|
||||
) : (
|
||||
<button type="button" className="btn btn-sm btn-ghost" onClick={() => handleDelete(job.id)} title="Delete">
|
||||
<i className="fas fa-trash" />
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export default function Sound() {
|
|||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-music" style={{ marginRight: 8, color: 'var(--color-accent)' }} />Sound Generation</h1>
|
||||
<h1 className="page-title"><i className="fas fa-music" /> Sound Generation</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
|
|
@ -89,11 +89,9 @@ export default function Sound() {
|
|||
</div>
|
||||
|
||||
{/* Mode toggle */}
|
||||
<div className="form-group">
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)' }}>
|
||||
<button type="button" className={`filter-btn ${mode === 'simple' ? 'active' : ''}`} onClick={() => setMode('simple')}>Simple</button>
|
||||
<button type="button" className={`filter-btn ${mode === 'advanced' ? 'active' : ''}`} onClick={() => setMode('advanced')}>Advanced</button>
|
||||
</div>
|
||||
<div className="segmented">
|
||||
<button type="button" className={`segmented__item${mode === 'simple' ? ' is-active' : ''}`} onClick={() => setMode('simple')}>Simple</button>
|
||||
<button type="button" className={`segmented__item${mode === 'advanced' ? ' is-active' : ''}`} onClick={() => setMode('advanced')}>Advanced</button>
|
||||
</div>
|
||||
|
||||
{mode === 'simple' ? (
|
||||
|
|
@ -102,12 +100,14 @@ export default function Sound() {
|
|||
<label className="form-label">Description</label>
|
||||
<textarea className="textarea" value={text} onChange={(e) => setText(e.target.value)} placeholder="Describe the sound..." rows={3} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', alignItems: 'center', marginBottom: 'var(--spacing-md)' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.875rem', color: 'var(--color-text-secondary)' }}>
|
||||
<input type="checkbox" checked={instrumental} onChange={(e) => setInstrumental(e.target.checked)} /> Instrumental
|
||||
<div className="form-grid-2col">
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={instrumental} onChange={(e) => setInstrumental(e.target.checked)} />
|
||||
<span>Instrumental</span>
|
||||
</label>
|
||||
<div className="form-group" style={{ flex: 1, margin: 0 }}>
|
||||
<input className="input" value={vocalLanguage} onChange={(e) => setVocalLanguage(e.target.value)} placeholder="Vocal language" />
|
||||
<div className="form-group">
|
||||
<label className="form-label">Vocal language</label>
|
||||
<input className="input" value={vocalLanguage} onChange={(e) => setVocalLanguage(e.target.value)} placeholder="e.g. English" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -121,20 +121,21 @@ export default function Sound() {
|
|||
<label className="form-label">Lyrics</label>
|
||||
<textarea className="textarea" value={lyrics} onChange={(e) => setLyrics(e.target.value)} rows={3} />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-sm)' }}>
|
||||
<div className="form-grid-2col">
|
||||
<div className="form-group"><label className="form-label">BPM</label><input className="input" type="number" value={bpm} onChange={(e) => setBpm(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Duration (s)</label><input className="input" type="number" step="0.1" value={duration} onChange={(e) => setDuration(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Key/Scale</label><input className="input" value={keyscale} onChange={(e) => setKeyscale(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Language</label><input className="input" value={language} onChange={(e) => setLanguage(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Time Signature</label><input className="input" value={timesignature} onChange={(e) => setTimesignature(e.target.value)} /></div>
|
||||
</div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.875rem', color: 'var(--color-text-secondary)', marginBottom: 'var(--spacing-md)' }}>
|
||||
<input type="checkbox" checked={think} onChange={(e) => setThink(e.target.checked)} /> Think mode
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={think} onChange={(e) => setThink(e.target.checked)} />
|
||||
<span>Think mode</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button type="submit" className="btn btn-primary" disabled={loading} style={{ width: '100%' }}>
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? <><LoadingSpinner size="sm" /> Generating...</> : <><i className="fas fa-music" /> Generate Sound</>}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -148,22 +149,20 @@ export default function Sound() {
|
|||
) : error ? (
|
||||
<ErrorWithTraceLink message={error} />
|
||||
) : selectedEntry ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-md)', width: '100%' }}>
|
||||
<audio controls src={selectedEntry.results[0]?.url} style={{ width: '100%', maxWidth: '400px' }} data-testid="history-audio" />
|
||||
<div style={{ padding: 'var(--spacing-sm)', background: 'var(--color-bg-tertiary)', borderRadius: 'var(--radius-md)', color: 'var(--color-text-secondary)', fontStyle: 'italic', textAlign: 'center' }}>
|
||||
"{selectedEntry.prompt}"
|
||||
</div>
|
||||
<div className="audio-result">
|
||||
<audio controls src={selectedEntry.results[0]?.url} className="audio-result__player" data-testid="history-audio" />
|
||||
<div className="result-quote">"{selectedEntry.prompt}"</div>
|
||||
</div>
|
||||
) : audioUrl ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-md)', width: '100%' }}>
|
||||
<audio ref={audioRef} controls src={audioUrl} style={{ width: '100%', maxWidth: '400px' }} />
|
||||
<div className="audio-result">
|
||||
<audio ref={audioRef} controls src={audioUrl} className="audio-result__player" />
|
||||
<a href={audioUrl} download={`sound-${new Date().toISOString().slice(0, 10)}.wav`} className="btn btn-primary btn-sm">
|
||||
<i className="fas fa-download" /> Download
|
||||
<i className="fas fa-download" /> <span>Download</span>
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', color: 'var(--color-text-muted)' }}>
|
||||
<i className="fas fa-music" style={{ fontSize: '3rem', marginBottom: 'var(--spacing-md)', opacity: 0.4 }} />
|
||||
<div className="media-empty">
|
||||
<i className="fas fa-music media-empty__icon" />
|
||||
<p>Generated sound will appear here</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export default function TTS() {
|
|||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-headphones" style={{ marginRight: 8, color: 'var(--color-accent)' }} />Text to Speech</h1>
|
||||
<h1 className="page-title"><i className="fas fa-headphones" /> Text to Speech</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
|
|
@ -67,7 +67,7 @@ export default function TTS() {
|
|||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading} style={{ width: '100%' }}>
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? <><LoadingSpinner size="sm" /> Generating...</> : <><i className="fas fa-headphones" /> Generate Audio</>}
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -81,30 +81,26 @@ export default function TTS() {
|
|||
) : error ? (
|
||||
<ErrorWithTraceLink message={error} />
|
||||
) : selectedEntry ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-md)', width: '100%' }}>
|
||||
<audio controls src={selectedEntry.results[0]?.url} style={{ width: '100%' }} data-testid="history-audio" />
|
||||
<div style={{ padding: 'var(--spacing-sm)', background: 'var(--color-bg-tertiary)', borderRadius: 'var(--radius-md)', color: 'var(--color-text-secondary)', fontStyle: 'italic', textAlign: 'center' }}>
|
||||
"{selectedEntry.prompt}"
|
||||
</div>
|
||||
<div className="audio-result">
|
||||
<audio controls src={selectedEntry.results[0]?.url} className="audio-result__player" data-testid="history-audio" />
|
||||
<div className="result-quote">"{selectedEntry.prompt}"</div>
|
||||
</div>
|
||||
) : audioUrl ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--spacing-md)', width: '100%' }}>
|
||||
<audio ref={audioRef} controls src={audioUrl} style={{ width: '100%' }} />
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)' }}>
|
||||
<div className="audio-result">
|
||||
<audio ref={audioRef} controls src={audioUrl} className="audio-result__player" />
|
||||
<div className="audio-result__actions">
|
||||
<a href={audioUrl} download={`tts-${model}-${new Date().toISOString().slice(0, 10)}.mp3`} className="btn btn-primary btn-sm">
|
||||
<i className="fas fa-download" /> Download
|
||||
<i className="fas fa-download" /> <span>Download</span>
|
||||
</a>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => audioRef.current?.play()}>
|
||||
<i className="fas fa-rotate-right" /> Replay
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={() => audioRef.current?.play()}>
|
||||
<i className="fas fa-rotate-right" /> <span>Replay</span>
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: 'var(--spacing-sm)', background: 'var(--color-bg-tertiary)', borderRadius: 'var(--radius-md)', color: 'var(--color-text-secondary)', fontStyle: 'italic', textAlign: 'center' }}>
|
||||
"{text}"
|
||||
</div>
|
||||
<div className="result-quote">"{text}"</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', color: 'var(--color-text-muted)' }}>
|
||||
<i className="fas fa-headphones" style={{ fontSize: '3rem', marginBottom: 'var(--spacing-md)', opacity: 0.4 }} />
|
||||
<div className="media-empty">
|
||||
<i className="fas fa-headphones media-empty__icon" />
|
||||
<p>Generated audio will appear here</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -61,17 +61,17 @@ function truncateValue(value, maxLen) {
|
|||
}
|
||||
|
||||
const TYPE_COLORS = {
|
||||
llm: { bg: 'rgba(59,130,246,0.15)', color: '#60a5fa' },
|
||||
embedding: { bg: 'rgba(168,85,247,0.15)', color: '#c084fc' },
|
||||
transcription: { bg: 'rgba(234,179,8,0.15)', color: '#facc15' },
|
||||
image_generation: { bg: 'rgba(34,197,94,0.15)', color: '#4ade80' },
|
||||
video_generation: { bg: 'rgba(236,72,153,0.15)', color: '#f472b6' },
|
||||
tts: { bg: 'rgba(249,115,22,0.15)', color: '#fb923c' },
|
||||
sound_generation: { bg: 'rgba(20,184,166,0.15)', color: '#2dd4bf' },
|
||||
rerank: { bg: 'rgba(99,102,241,0.15)', color: '#818cf8' },
|
||||
tokenize: { bg: 'rgba(107,114,128,0.15)', color: '#9ca3af' },
|
||||
detection: { bg: 'rgba(14,165,233,0.15)', color: '#38bdf8' },
|
||||
model_load: { bg: 'rgba(239,68,68,0.15)', color: '#f87171' },
|
||||
llm: { bg: 'var(--color-primary-light)', color: 'var(--color-data-1)' },
|
||||
embedding: { bg: 'var(--color-accent-light)', color: 'var(--color-data-3)' },
|
||||
transcription: { bg: 'var(--color-warning-light)', color: 'var(--color-data-4)' },
|
||||
image_generation: { bg: 'var(--color-success-light)', color: 'var(--color-data-5)' },
|
||||
video_generation: { bg: 'var(--color-accent-light)', color: 'var(--color-data-7)' },
|
||||
tts: { bg: 'var(--color-warning-light)', color: 'var(--color-data-6)' },
|
||||
sound_generation: { bg: 'var(--color-info-light)', color: 'var(--color-data-8)' },
|
||||
rerank: { bg: 'var(--color-primary-light)', color: 'var(--color-data-1)' },
|
||||
tokenize: { bg: 'var(--color-secondary-light)', color: 'var(--color-text-muted)' },
|
||||
detection: { bg: 'var(--color-info-light)', color: 'var(--color-data-8)' },
|
||||
model_load: { bg: 'var(--color-error-light)', color: 'var(--color-data-2)' },
|
||||
}
|
||||
|
||||
function typeBadgeStyle(type) {
|
||||
|
|
@ -335,7 +335,11 @@ export default function Traces() {
|
|||
setBackendCount(backend.length)
|
||||
setTraces(activeTab === 'api' ? api : backend)
|
||||
} catch (err) {
|
||||
addToast(`Failed to load traces: ${err.message}`, 'error')
|
||||
// Tracing disabled is the default state, not an error — the in-page banner covers it.
|
||||
const disabled = /disabled|not enabled|404|not found/i.test(err?.message || '')
|
||||
if (!disabled) {
|
||||
addToast(`Failed to load traces: ${err.message}`, 'error')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
|
@ -395,10 +399,11 @@ export default function Traces() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', marginBottom: 'var(--spacing-md)' }}>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-sm)', marginBottom: 'var(--spacing-md)', alignItems: 'center' }}>
|
||||
<button className="btn btn-secondary btn-sm" onClick={fetchTraces}><i className="fas fa-rotate" /> Refresh</button>
|
||||
<button className="btn btn-danger btn-sm" onClick={handleClear}><i className="fas fa-trash" /> Clear</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleExport} disabled={traces.length === 0}><i className="fas fa-download" /> Export</button>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn btn-danger btn-sm" onClick={handleClear} disabled={traces.length === 0}><i className="fas fa-trash" /> Clear</button>
|
||||
</div>
|
||||
|
||||
{settings && (() => {
|
||||
|
|
@ -457,7 +462,7 @@ export default function Traces() {
|
|||
onChange={(v) => setSettings(prev => ({ ...prev, enable_backend_logging: v }))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 'var(--spacing-sm)' }}>
|
||||
<div className="form-group__actions" style={{ justifyContent: 'flex-end' }}>
|
||||
<button className="btn btn-primary btn-sm" onClick={handleSaveSettings} disabled={saving}>
|
||||
{saving ? <><LoadingSpinner size="sm" /> Saving...</> : <><i className="fas fa-save" /> Save</>}
|
||||
</button>
|
||||
|
|
@ -473,8 +478,20 @@ export default function Traces() {
|
|||
) : traces.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-state-icon"><i className="fas fa-wave-square" /></div>
|
||||
<h2 className="empty-state-title">No traces</h2>
|
||||
<p className="empty-state-text">Traces will appear here as requests are made.</p>
|
||||
<h2 className="empty-state-title">
|
||||
{activeTab === 'api'
|
||||
? (tracingEnabled ? 'No API traces yet' : 'API tracing is off')
|
||||
: (backendLoggingEnabled ? 'No backend traces yet' : 'Backend logging is off')}
|
||||
</h2>
|
||||
<p className="empty-state-text">
|
||||
{activeTab === 'api'
|
||||
? (tracingEnabled
|
||||
? 'Traces will appear here as API requests are made.'
|
||||
: 'Enable Tracing above to start recording API requests, responses, and backend operations.')
|
||||
: (backendLoggingEnabled
|
||||
? 'Backend operations will appear here as models run.'
|
||||
: 'Enable Backend Logging above to capture per-model process output.')}
|
||||
</p>
|
||||
</div>
|
||||
) : activeTab === 'api' ? (
|
||||
<div className="table-container">
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default function VideoGen() {
|
|||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-video" style={{ marginRight: 8, color: 'var(--color-accent)' }} />Video Generation</h1>
|
||||
<h1 className="page-title"><i className="fas fa-video" /> Video Generation</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
|
|
@ -97,10 +97,10 @@ export default function VideoGen() {
|
|||
<textarea className="textarea" value={negativePrompt} onChange={(e) => setNegativePrompt(e.target.value)} rows={2} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 'var(--spacing-sm)' }}>
|
||||
<div className="form-grid-3col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">Size</label>
|
||||
<select className="model-selector" value={size} onChange={(e) => setSize(e.target.value)} style={{ width: '100%' }}>
|
||||
<select className="input btn-full" value={size} onChange={(e) => setSize(e.target.value)}>
|
||||
{SIZES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -118,7 +118,7 @@ export default function VideoGen() {
|
|||
<i className="fas fa-chevron-right" /> Advanced
|
||||
</div>
|
||||
{showAdvanced && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 'var(--spacing-sm)', marginBottom: 'var(--spacing-md)' }}>
|
||||
<div className="form-grid-3col">
|
||||
<div className="form-group"><label className="form-label">Steps</label><input className="input" type="number" value={steps} onChange={(e) => setSteps(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Seed</label><input className="input" type="number" value={seed} onChange={(e) => setSeed(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">CFG Scale</label><input className="input" type="number" step="0.1" value={cfgScale} onChange={(e) => setCfgScale(e.target.value)} /></div>
|
||||
|
|
@ -129,13 +129,13 @@ export default function VideoGen() {
|
|||
<i className="fas fa-chevron-right" /> Image Inputs
|
||||
</div>
|
||||
{showImageInputs && (
|
||||
<div style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
<div className="form-grid-2col">
|
||||
<div className="form-group"><label className="form-label">Start Image</label><input type="file" accept="image/*" onChange={(e) => handleImageUpload(e, setStartImage)} className="input" /></div>
|
||||
<div className="form-group"><label className="form-label">End Image</label><input type="file" accept="image/*" onChange={(e) => handleImageUpload(e, setEndImage)} className="input" /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button type="submit" className="btn btn-primary" disabled={loading} style={{ width: '100%' }}>
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? <><LoadingSpinner size="sm" /> Generating...</> : <><i className="fas fa-video" /> Generate Video</>}
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,60 +1,84 @@
|
|||
/* LocalAI Theme - CSS Variables System */
|
||||
:root,
|
||||
[data-theme="dark"] {
|
||||
--color-bg-primary: #0f1117;
|
||||
--color-bg-secondary: #161921;
|
||||
--color-bg-tertiary: #1c1f2a;
|
||||
--color-bg-overlay: rgba(15, 17, 23, 0.95);
|
||||
--color-bg-primary: #0b0d14;
|
||||
--color-bg-secondary: #14172099;
|
||||
--color-bg-secondary: #161923;
|
||||
--color-bg-tertiary: #232838;
|
||||
--color-bg-overlay: rgba(11, 13, 20, 0.95);
|
||||
|
||||
--color-primary: #4f7df5;
|
||||
--color-primary-hover: #3d6be3;
|
||||
--color-primary-active: #2f5ad1;
|
||||
--color-primary-text: #FFFFFF;
|
||||
--color-primary-light: rgba(79, 125, 245, 0.08);
|
||||
--color-primary-border: rgba(79, 125, 245, 0.15);
|
||||
--color-surface-raised: #1a1e2c;
|
||||
--color-surface-sunken: #090b12;
|
||||
--color-surface-hover: #242938;
|
||||
|
||||
--color-secondary: #64748B;
|
||||
--color-secondary-hover: #475569;
|
||||
--color-secondary-light: rgba(100, 116, 139, 0.1);
|
||||
--color-primary: #f1f3f8;
|
||||
--color-primary-hover: #ffffff;
|
||||
--color-primary-active: #d8dce6;
|
||||
--color-primary-text: #0a0c12;
|
||||
--color-primary-light: rgba(241, 243, 248, 0.08);
|
||||
--color-primary-border: rgba(241, 243, 248, 0.22);
|
||||
|
||||
--color-accent: #7c9cf5;
|
||||
--color-accent-hover: #6889e3;
|
||||
--color-accent-light: rgba(124, 156, 245, 0.1);
|
||||
--color-secondary: #6b7487;
|
||||
--color-secondary-hover: #525b6e;
|
||||
--color-secondary-light: rgba(107, 116, 135, 0.12);
|
||||
|
||||
--color-text-primary: #E5E7EB;
|
||||
--color-text-secondary: #94A3B8;
|
||||
--color-text-muted: #64748B;
|
||||
--color-text-disabled: #475569;
|
||||
--color-accent: #e8a87c;
|
||||
--color-accent-hover: #d89668;
|
||||
--color-accent-light: rgba(232, 168, 124, 0.14);
|
||||
|
||||
--color-text-primary: #f1f3f8;
|
||||
--color-text-secondary: #a8adbd;
|
||||
--color-text-muted: #6c7084;
|
||||
--color-text-disabled: #3e4253;
|
||||
--color-text-inverse: #FFFFFF;
|
||||
|
||||
--color-border-subtle: rgba(255, 255, 255, 0.08);
|
||||
--color-border-subtle: rgba(255, 255, 255, 0.06);
|
||||
--color-border-default: rgba(255, 255, 255, 0.12);
|
||||
--color-border-strong: rgba(79, 125, 245, 0.3);
|
||||
--color-border-strong: rgba(255, 255, 255, 0.22);
|
||||
--color-border-divider: rgba(255, 255, 255, 0.05);
|
||||
--color-border-primary: rgba(79, 125, 245, 0.2);
|
||||
--color-border-focus: rgba(79, 125, 245, 0.4);
|
||||
--color-border-primary: rgba(255, 255, 255, 0.22);
|
||||
--color-border-focus: rgba(241, 243, 248, 0.38);
|
||||
|
||||
--color-success: #22C55E;
|
||||
--color-success-light: rgba(34, 197, 94, 0.1);
|
||||
--color-success-border: rgba(34, 197, 94, 0.3);
|
||||
--color-success-light: rgba(34, 197, 94, 0.14);
|
||||
--color-success-border: rgba(34, 197, 94, 0.32);
|
||||
--color-warning: #F59E0B;
|
||||
--color-warning-light: rgba(245, 158, 11, 0.1);
|
||||
--color-warning-border: rgba(245, 158, 11, 0.3);
|
||||
--color-warning-light: rgba(245, 158, 11, 0.14);
|
||||
--color-warning-border: rgba(245, 158, 11, 0.32);
|
||||
--color-error: #EF4444;
|
||||
--color-error-light: rgba(239, 68, 68, 0.1);
|
||||
--color-error-border: rgba(239, 68, 68, 0.3);
|
||||
--color-info: #4f7df5;
|
||||
--color-info-light: rgba(79, 125, 245, 0.1);
|
||||
--color-info-border: rgba(79, 125, 245, 0.3);
|
||||
--color-accent-border: rgba(124, 156, 245, 0.3);
|
||||
--color-modal-backdrop: rgba(0, 0, 0, 0.6);
|
||||
--color-error-light: rgba(239, 68, 68, 0.14);
|
||||
--color-error-border: rgba(239, 68, 68, 0.32);
|
||||
--color-info: #7dc7d1;
|
||||
--color-info-light: rgba(125, 199, 209, 0.14);
|
||||
--color-info-border: rgba(125, 199, 209, 0.32);
|
||||
--color-accent-border: rgba(232, 168, 124, 0.32);
|
||||
--color-modal-backdrop: rgba(0, 0, 0, 0.7);
|
||||
|
||||
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.35);
|
||||
--shadow-glow: 0 0 0 1px rgba(79, 125, 245, 0.15), 0 0 12px rgba(79, 125, 245, 0.2);
|
||||
--shadow-sidebar: 1px 0 3px rgba(0, 0, 0, 0.25);
|
||||
/* Data viz palette — distinct hues, readable on dark */
|
||||
--color-data-1: #7dc7d1;
|
||||
--color-data-2: #f87171;
|
||||
--color-data-3: #c084fc;
|
||||
--color-data-4: #fbbf24;
|
||||
--color-data-5: #34d399;
|
||||
--color-data-6: #fb923c;
|
||||
--color-data-7: #f472b6;
|
||||
--color-data-8: #22d3ee;
|
||||
|
||||
/* Log streams */
|
||||
--color-log-stdout: #d1d5db;
|
||||
--color-log-stderr: #fca5a5;
|
||||
--color-log-info: #93c5fd;
|
||||
--color-log-warn: #fcd34d;
|
||||
|
||||
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.45);
|
||||
--shadow-md: 0 6px 18px rgba(0, 0, 0, 0.5);
|
||||
--shadow-lg: 0 16px 32px rgba(0, 0, 0, 0.55);
|
||||
--shadow-glow: var(--shadow-md);
|
||||
--shadow-sidebar: 1px 0 0 rgba(255, 255, 255, 0.06);
|
||||
|
||||
/* Inset top-edge highlight — fakes elevation in dark mode */
|
||||
--shadow-inset-top: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 200ms;
|
||||
|
|
@ -66,51 +90,77 @@
|
|||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
--radius-lg: 10px;
|
||||
--radius-xl: 14px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* Typography scale */
|
||||
--text-xs: 0.6875rem;
|
||||
--text-sm: 0.8125rem;
|
||||
--text-base: 0.9375rem;
|
||||
--text-lg: 1.0625rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 1.875rem;
|
||||
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--leading-tight: 1.25;
|
||||
--leading-snug: 1.4;
|
||||
--leading-normal: 1.55;
|
||||
--leading-relaxed: 1.7;
|
||||
|
||||
--sidebar-width: 200px;
|
||||
--sidebar-width-collapsed: 52px;
|
||||
--color-toggle-off: #475569;
|
||||
--color-toggle-off: #3a4152;
|
||||
--color-toggle-on: var(--color-primary);
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--color-bg-primary: #f5f6f9;
|
||||
--color-bg-secondary: #FFFFFF;
|
||||
--color-bg-tertiary: #f0f1f5;
|
||||
--color-bg-overlay: rgba(245, 246, 249, 0.9);
|
||||
/* Pearl palette — soft cool off-white, clean layered surfaces */
|
||||
--color-bg-primary: #f5f6f8; /* page: soft pearl */
|
||||
--color-bg-secondary: #ffffff; /* raised surface */
|
||||
--color-bg-tertiary: #e7e9ef; /* sunken wells */
|
||||
--color-bg-overlay: rgba(245, 246, 248, 0.92);
|
||||
|
||||
--color-primary: #4068d9;
|
||||
--color-primary-hover: #3358c7;
|
||||
--color-primary-active: #2a4ab5;
|
||||
--color-surface-raised: #ffffff;
|
||||
--color-surface-sunken: #eef0f4;
|
||||
--color-surface-hover: #edeff4;
|
||||
|
||||
--color-primary: #14161f; /* near-black (cool-leaning) */
|
||||
--color-primary-hover: #000000;
|
||||
--color-primary-active: #2a2e3a;
|
||||
--color-primary-text: #FFFFFF;
|
||||
--color-primary-light: rgba(64, 104, 217, 0.08);
|
||||
--color-primary-border: rgba(64, 104, 217, 0.2);
|
||||
--color-primary-light: rgba(20, 22, 31, 0.06);
|
||||
--color-primary-border: rgba(20, 22, 31, 0.2);
|
||||
|
||||
--color-secondary: #475569;
|
||||
--color-secondary-hover: #334155;
|
||||
--color-secondary-light: rgba(71, 85, 105, 0.1);
|
||||
--color-secondary: #525966;
|
||||
--color-secondary-hover: #3b414c;
|
||||
--color-secondary-light: rgba(82, 89, 102, 0.1);
|
||||
|
||||
--color-accent: #5a78c9;
|
||||
--color-accent-hover: #4a67b7;
|
||||
--color-accent-light: rgba(90, 120, 201, 0.1);
|
||||
--color-accent: #c97b5a; /* muted terracotta accent */
|
||||
--color-accent-hover: #b56a4a;
|
||||
--color-accent-light: rgba(201, 123, 90, 0.12);
|
||||
|
||||
--color-text-primary: #1E293B;
|
||||
--color-text-secondary: #64748B;
|
||||
--color-text-muted: #94A3B8;
|
||||
--color-text-disabled: #CBD5E1;
|
||||
--color-text-primary: #14161f; /* crisp near-black, cool-leaning */
|
||||
--color-text-secondary: #555a68;
|
||||
--color-text-muted: #8a8e9b;
|
||||
--color-text-disabled: #c2c5cf;
|
||||
--color-text-inverse: #FFFFFF;
|
||||
|
||||
--color-border-subtle: rgba(15, 23, 42, 0.06);
|
||||
--color-border-default: rgba(15, 23, 42, 0.1);
|
||||
--color-border-strong: rgba(64, 104, 217, 0.3);
|
||||
--color-border-divider: rgba(15, 23, 42, 0.04);
|
||||
--color-border-primary: rgba(64, 104, 217, 0.2);
|
||||
--color-border-focus: rgba(64, 104, 217, 0.4);
|
||||
--color-border-subtle: rgba(20, 22, 31, 0.07);
|
||||
--color-border-default: rgba(20, 22, 31, 0.14);
|
||||
--color-border-strong: rgba(20, 22, 31, 0.32);
|
||||
--color-border-divider: rgba(20, 22, 31, 0.05);
|
||||
--color-border-primary: rgba(20, 22, 31, 0.22);
|
||||
--color-border-focus: rgba(20, 22, 31, 0.45);
|
||||
|
||||
--color-success: #16A34A;
|
||||
--color-success-light: rgba(22, 163, 74, 0.1);
|
||||
|
|
@ -119,19 +169,38 @@
|
|||
--color-warning-light: rgba(217, 119, 6, 0.1);
|
||||
--color-warning-border: rgba(217, 119, 6, 0.3);
|
||||
--color-error: #DC2626;
|
||||
--color-error-light: rgba(220, 38, 38, 0.1);
|
||||
--color-error-light: rgba(220, 38, 38, 0.09);
|
||||
--color-error-border: rgba(220, 38, 38, 0.3);
|
||||
--color-info: #4068d9;
|
||||
--color-info-light: rgba(64, 104, 217, 0.1);
|
||||
--color-info-border: rgba(64, 104, 217, 0.3);
|
||||
--color-accent-border: rgba(90, 120, 201, 0.3);
|
||||
--color-modal-backdrop: rgba(0, 0, 0, 0.5);
|
||||
--color-info: #0f7583;
|
||||
--color-info-light: rgba(15, 117, 131, 0.10);
|
||||
--color-info-border: rgba(15, 117, 131, 0.3);
|
||||
--color-accent-border: rgba(201, 123, 90, 0.3);
|
||||
--color-modal-backdrop: rgba(20, 22, 31, 0.5);
|
||||
|
||||
--shadow-subtle: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08);
|
||||
--shadow-glow: 0 0 0 1px rgba(64, 104, 217, 0.15), 0 0 8px rgba(64, 104, 217, 0.2);
|
||||
--shadow-sidebar: 1px 0 3px rgba(0, 0, 0, 0.08);
|
||||
--color-toggle-off: #CBD5E1;
|
||||
/* Data viz palette */
|
||||
--color-data-1: #0f7583;
|
||||
--color-data-2: #c97b5a;
|
||||
--color-data-3: #8b5cf6;
|
||||
--color-data-4: #d97706;
|
||||
--color-data-5: #059669;
|
||||
--color-data-6: #dc2626;
|
||||
--color-data-7: #db2777;
|
||||
--color-data-8: #0891b2;
|
||||
|
||||
--color-log-stdout: #14161f;
|
||||
--color-log-stderr: #b91c1c;
|
||||
--color-log-info: #0f7583;
|
||||
--color-log-warn: #b45309;
|
||||
|
||||
--shadow-subtle: 0 1px 2px rgba(20, 22, 31, 0.05);
|
||||
--shadow-sm: 0 2px 6px rgba(20, 22, 31, 0.07);
|
||||
--shadow-md: 0 8px 20px rgba(20, 22, 31, 0.08);
|
||||
--shadow-lg: 0 20px 38px rgba(20, 22, 31, 0.1);
|
||||
--shadow-glow: var(--shadow-md);
|
||||
--shadow-sidebar: 4px 0 24px -12px rgba(20, 22, 31, 0.1), 1px 0 0 rgba(20, 22, 31, 0.06);
|
||||
|
||||
--shadow-inset-top: inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
|
||||
--color-toggle-off: #c5c8d2;
|
||||
--color-toggle-on: var(--color-primary);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue