mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-18 13:49:09 -04:00
refactor(ui): adopt PageHeader on ops/admin/media pages (batch B)
Replace hand-rolled .page-header title blocks with the shared editorial PageHeader component across 14 pages (Manage, Middleware, Models, NodeBackendLogs, Nodes, P2P, SkillEdit, Skills, Sound, Traces, TTS, Usage, Users, VideoGen). Title/subtitle move into PageHeader; header-own action clusters (Models stats+buttons, Skills search+buttons) move into the actions slot. Tabs, filters, stat cards, ResourceMonitor and page body stay as siblings. Eyebrow is left to auto-derive from the route. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
@@ -3,6 +3,7 @@ import { useNavigate, useOutletContext, useSearchParams, useLocation } from 'rea
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { fromState } from '../utils/editorNav'
|
||||
import ResourceMonitor from '../components/ResourceMonitor'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import NodeDistributionChip from '../components/NodeDistributionChip'
|
||||
import FilterBar from '../components/FilterBar'
|
||||
@@ -448,10 +449,7 @@ export default function Manage() {
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">{t('manage.title')}</h1>
|
||||
<p className="page-subtitle">{t('manage.subtitle')}</p>
|
||||
</div>
|
||||
<PageHeader title={t('manage.title')} supporting={t('manage.subtitle')} />
|
||||
|
||||
{/* Resource Monitor */}
|
||||
<ResourceMonitor />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fromState } from '../utils/editorNav'
|
||||
import { settingsApi, modelsApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import Toggle from '../components/Toggle'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
|
||||
// Middleware admin page. Three tabs:
|
||||
// - Filtering: per-model resolved PII state + per-model detector list
|
||||
@@ -129,12 +130,10 @@ export default function Middleware() {
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header" style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
<h1 className="page-title">Middleware</h1>
|
||||
<p className="page-subtitle">
|
||||
Inspect and configure routing-module middleware: PII filtering and intelligent routing.
|
||||
</p>
|
||||
</div>
|
||||
<PageHeader
|
||||
title="Middleware"
|
||||
supporting="Inspect and configure routing-module middleware: PII filtering and intelligent routing."
|
||||
/>
|
||||
|
||||
{/* Tab bar */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-xs)', marginBottom: 'var(--spacing-md)', flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useDebouncedCallback } from '../hooks/useDebounce'
|
||||
import { useOperations } from '../hooks/useOperations'
|
||||
import { useResources } from '../hooks/useResources'
|
||||
import SearchableSelect from '../components/SearchableSelect'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import GalleryLoader from '../components/GalleryLoader'
|
||||
import Toggle from '../components/Toggle'
|
||||
@@ -271,32 +272,32 @@ export default function Models() {
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{t('subtitle')}</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', fontSize: '0.8125rem' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-primary)' }}>{stats.total}</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>{t('stats.available')}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<a onClick={() => navigate('/app/manage')} style={{ cursor: 'pointer' }}>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-success)' }}>{stats.installed}</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>{t('stats.installed')}</div>
|
||||
</a>
|
||||
<PageHeader
|
||||
title={t('title')}
|
||||
supporting={t('subtitle')}
|
||||
actions={
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 'var(--spacing-md)', fontSize: '0.8125rem' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-primary)' }}>{stats.total}</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>{t('stats.available')}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<a onClick={() => navigate('/app/manage')} style={{ cursor: 'pointer' }}>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-success)' }}>{stats.installed}</div>
|
||||
<div style={{ color: 'var(--color-text-muted)' }}>{t('stats.installed')}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => navigate('/app/model-editor', { state: fromState(location, t('models')) })}>
|
||||
<i className="fas fa-plus" /> {t('actions.addModel')}
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate('/app/import-model')}>
|
||||
<i className="fas fa-upload" /> {t('actions.importModel')}
|
||||
</button>
|
||||
</div>
|
||||
<button className="btn btn-primary btn-sm" onClick={() => navigate('/app/model-editor', { state: fromState(location, t('models')) })}>
|
||||
<i className="fas fa-plus" /> {t('actions.addModel')}
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => navigate('/app/import-model')}>
|
||||
<i className="fas fa-upload" /> {t('actions.importModel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Search */}
|
||||
<div className="search-bar" style={{ marginBottom: 'var(--spacing-md)' }}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { nodesApi } from '../utils/api'
|
||||
import { formatTimestamp } from '../utils/format'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
|
||||
function wsUrl(path) {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
@@ -176,9 +177,9 @@ export default function NodeBackendLogs() {
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title" style={{ marginBottom: 0 }}>
|
||||
<PageHeader
|
||||
title={
|
||||
<>
|
||||
<i className="fas fa-terminal" style={{ fontSize: '0.8em', marginRight: 'var(--spacing-sm)' }} />
|
||||
{baseModelName}
|
||||
{!isMerged && (
|
||||
@@ -217,13 +218,15 @@ export default function NodeBackendLogs() {
|
||||
merged · {replicas.length} replicas
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
<p className="page-subtitle" style={{ marginTop: 'var(--spacing-xs)' }}>
|
||||
</>
|
||||
}
|
||||
supporting={
|
||||
<>
|
||||
Backend logs from node <strong>{nodeName || nodeId}</strong>
|
||||
{' '}<Link to="/app/nodes" style={{ color: 'var(--color-primary)', fontSize: '0.8125rem' }}>(back to nodes)</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{showReplicaToggle && (
|
||||
<div role="radiogroup" aria-label="Replica scope" className="segmented" style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useOutletContext, useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { nodesApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import ActionMenu from '../components/ActionMenu'
|
||||
import SearchableModelSelect from '../components/SearchableModelSelect'
|
||||
@@ -995,15 +996,15 @@ export default function Nodes() {
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-network-wired" style={{ marginRight: 'var(--spacing-sm)' }} />
|
||||
{t('nodes.title')}
|
||||
</h1>
|
||||
<p className="page-subtitle">
|
||||
{t('nodes.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<PageHeader
|
||||
title={
|
||||
<>
|
||||
<i className="fas fa-network-wired" style={{ marginRight: 'var(--spacing-sm)' }} />
|
||||
{t('nodes.title')}
|
||||
</>
|
||||
}
|
||||
supporting={t('nodes.subtitle')}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="tabs" style={{ marginBottom: 'var(--spacing-lg)' }}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { p2pApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import ImageSelector, { useImageSelector, dockerImage, dockerFlags } from '../components/ImageSelector'
|
||||
|
||||
function NodeCard({ node, label, iconColor, iconBg }) {
|
||||
@@ -295,20 +296,24 @@ export default function P2P() {
|
||||
|
||||
return (
|
||||
<div className="page page--narrow">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-circle-nodes" style={{ marginRight: 'var(--spacing-sm)' }} />
|
||||
{t('p2p.title')}
|
||||
</h1>
|
||||
<p className="page-subtitle">
|
||||
{t('p2p.subtitle')}
|
||||
{' '}
|
||||
<a href="https://localai.io/features/distribute/" target="_blank" rel="noopener noreferrer"
|
||||
style={{ color: 'var(--color-primary)' }}>
|
||||
<i className="fas fa-circle-info" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<PageHeader
|
||||
title={
|
||||
<>
|
||||
<i className="fas fa-circle-nodes" style={{ marginRight: 'var(--spacing-sm)' }} />
|
||||
{t('p2p.title')}
|
||||
</>
|
||||
}
|
||||
supporting={
|
||||
<>
|
||||
{t('p2p.subtitle')}
|
||||
{' '}
|
||||
<a href="https://localai.io/features/distribute/" target="_blank" rel="noopener noreferrer"
|
||||
style={{ color: 'var(--color-primary)' }}>
|
||||
<i className="fas fa-circle-info" />
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Network Token */}
|
||||
<div style={{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate, useLocation, useOutletContext, useSearchParams } from 'react-router-dom'
|
||||
import { skillsApi } from '../utils/api'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
|
||||
const RESOURCE_PREFIXES = ['scripts/', 'references/', 'assets/']
|
||||
function isValidResourcePath(path) {
|
||||
@@ -498,11 +499,13 @@ export default function SkillEdit() {
|
||||
<a className="skilledit-back-link" onClick={() => navigate('/app/skills')}>
|
||||
<i className="fas fa-arrow-left" /> Back to skills
|
||||
</a>
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">
|
||||
<i className="fas fa-book" style={{ marginRight: 'var(--spacing-xs)' }} /> {isNew ? 'New skill' : `Edit: ${name}`}
|
||||
</h1>
|
||||
</div>
|
||||
<PageHeader
|
||||
title={
|
||||
<>
|
||||
<i className="fas fa-book" style={{ marginRight: 'var(--spacing-xs)' }} /> {isNew ? 'New skill' : `Edit: ${name}`}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="card" style={{ marginTop: 'var(--spacing-md)' }}>
|
||||
<div className="skilledit-layout">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { skillsApi } from '../utils/api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useUserMap } from '../hooks/useUserMap'
|
||||
import UserGroupSection from '../components/UserGroupSection'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
|
||||
export default function Skills() {
|
||||
@@ -207,10 +208,7 @@ export default function Skills() {
|
||||
if (unavailable) {
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{t('unavailable.subtitle')}</p>
|
||||
</div>
|
||||
<PageHeader title={t('title')} supporting={t('unavailable.subtitle')} />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--spacing-xl)' }}>
|
||||
<button className="btn btn-primary" onClick={() => { setUnavailable(false); fetchSkills() }}>
|
||||
<i className="fas fa-redo" /> {t('unavailable.retry')}
|
||||
@@ -312,41 +310,41 @@ export default function Skills() {
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div>
|
||||
<h1 className="page-title">{t('title')}</h1>
|
||||
<p className="page-subtitle">{t('subtitle')}</p>
|
||||
</div>
|
||||
<div className="skills-header-actions">
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder={t('search.placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/app/skills/new')}>
|
||||
<i className="fas fa-plus" /> {t('actions.newSkill')}
|
||||
</button>
|
||||
<label className="btn btn-secondary" style={{ cursor: 'pointer' }}>
|
||||
<i className="fas fa-file-import" /> {importing ? t('actions.importing') : t('actions.import')}
|
||||
<PageHeader
|
||||
title={t('title')}
|
||||
supporting={t('subtitle')}
|
||||
actions={
|
||||
<div className="skills-header-actions">
|
||||
<input
|
||||
type="file"
|
||||
accept=".tar.gz"
|
||||
className="skills-import-input"
|
||||
onChange={handleImport}
|
||||
disabled={importing}
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder={t('search.placeholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{ width: '200px' }}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className={`btn ${showGitRepos ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setShowGitRepos((v) => !v)}
|
||||
>
|
||||
<i className="fas fa-code-branch" /> {t('actions.gitRepos')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/app/skills/new')}>
|
||||
<i className="fas fa-plus" /> {t('actions.newSkill')}
|
||||
</button>
|
||||
<label className="btn btn-secondary" style={{ cursor: 'pointer' }}>
|
||||
<i className="fas fa-file-import" /> {importing ? t('actions.importing') : t('actions.import')}
|
||||
<input
|
||||
type="file"
|
||||
accept=".tar.gz"
|
||||
className="skills-import-input"
|
||||
onChange={handleImport}
|
||||
disabled={importing}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className={`btn ${showGitRepos ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => setShowGitRepos((v) => !v)}
|
||||
>
|
||||
<i className="fas fa-code-branch" /> {t('actions.gitRepos')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{showGitRepos && (
|
||||
<div className="skills-git-section">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams, useOutletContext } from 'react-router-dom'
|
||||
import ModelSelector from '../components/ModelSelector'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import { CAP_SOUND_GENERATION } from '../utils/capabilities'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import ErrorWithTraceLink from '../components/ErrorWithTraceLink'
|
||||
@@ -77,9 +78,7 @@ export default function Sound() {
|
||||
return (
|
||||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-music" /> Sound Generation</h1>
|
||||
</div>
|
||||
<PageHeader title={<><i className="fas fa-music" /> Sound Generation</>} />
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
<div className="form-group">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { useParams, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelSelector from '../components/ModelSelector'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import { CAP_TTS } from '../utils/capabilities'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import ErrorWithTraceLink from '../components/ErrorWithTraceLink'
|
||||
@@ -49,9 +50,7 @@ export default function TTS() {
|
||||
return (
|
||||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-headphones" /> {t('tts.title')}</h1>
|
||||
</div>
|
||||
<PageHeader title={<><i className="fas fa-headphones" /> {t('tts.title')}</>} />
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
<div className="form-group">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { tracesApi, settingsApi } from '../utils/api'
|
||||
import { formatTimestamp } from '../utils/format'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import Toggle from '../components/Toggle'
|
||||
import SettingRow from '../components/SettingRow'
|
||||
import WaveformPlayer from '../components/audio/WaveformPlayer'
|
||||
@@ -407,10 +408,7 @@ export default function Traces() {
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">{t('traces.title')}</h1>
|
||||
<p className="page-subtitle">{t('traces.subtitle')}</p>
|
||||
</div>
|
||||
<PageHeader title={t('traces.title')} supporting={t('traces.subtitle')} />
|
||||
|
||||
<div className="tabs">
|
||||
<button className={`tab ${activeTab === 'api' ? 'tab-active' : ''}`} onClick={() => setActiveTab('api')}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { apiUrl } from '../utils/basePath'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import SourcesTab from './Usage/SourcesTab'
|
||||
|
||||
const PERIODS = [
|
||||
@@ -707,10 +708,7 @@ export default function Usage() {
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header" style={{ marginBottom: 'var(--spacing-sm)' }}>
|
||||
<h1 className="page-title">{t('usage.title')}</h1>
|
||||
<p className="page-subtitle">{t('usage.subtitle')}</p>
|
||||
</div>
|
||||
<PageHeader title={t('usage.title')} supporting={t('usage.subtitle')} />
|
||||
|
||||
{/* Period selector + tabs */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-xs)', marginBottom: 'var(--spacing-md)', flexWrap: 'wrap' }}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { adminUsersApi, adminInvitesApi } from '../utils/api'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import Modal from '../components/Modal'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import Toggle from '../components/Toggle'
|
||||
@@ -804,10 +805,7 @@ export default function Users() {
|
||||
|
||||
return (
|
||||
<div className="page page--wide">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title">{t('users.title')}</h1>
|
||||
<p className="page-subtitle">{t('users.subtitle')}</p>
|
||||
</div>
|
||||
<PageHeader title={t('users.title')} supporting={t('users.subtitle')} />
|
||||
|
||||
{/* Tab bar */}
|
||||
<div className="auth-tab-bar">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
||||
import { useParams, useOutletContext } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelSelector from '../components/ModelSelector'
|
||||
import PageHeader from '../components/PageHeader'
|
||||
import { CAP_VIDEO } from '../utils/capabilities'
|
||||
import LoadingSpinner from '../components/LoadingSpinner'
|
||||
import ErrorWithTraceLink from '../components/ErrorWithTraceLink'
|
||||
@@ -81,9 +82,7 @@ export default function VideoGen() {
|
||||
return (
|
||||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<div className="page-header">
|
||||
<h1 className="page-title"><i className="fas fa-video" /> {t('video.title')}</h1>
|
||||
</div>
|
||||
<PageHeader title={<><i className="fas fa-video" /> {t('video.title')}</>} />
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
<div className="form-group">
|
||||
|
||||
Reference in New Issue
Block a user