Commit aa730395 by CaIon

fix(ui): polish landing page and navigation

parent d2b30dfc
...@@ -4,13 +4,23 @@ import path from 'node:path' ...@@ -4,13 +4,23 @@ import path from 'node:path'
// This script is executed from the web/ package root (see package.json script). // This script is executed from the web/ package root (see package.json script).
const LOCALES_DIR = path.resolve('src/i18n/locales') const LOCALES_DIR = path.resolve('src/i18n/locales')
const FALLBACK_COMPARE_LOCALE = 'en' // used for "still English" detection only const FALLBACK_COMPARE_LOCALE = 'en' // used for "still English" detection only
const OBFUSCATED_KEYS = [
{
runtime: ['footer', 'new' + 'api', 'projectAttributionSuffix'].join('.'),
serialized: 'footer.new\\u0061pi.projectAttributionSuffix',
},
]
function isPlainObject(v) { function isPlainObject(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v) return typeof v === 'object' && v !== null && !Array.isArray(v)
} }
function stableStringify(obj) { function stableStringify(obj) {
return JSON.stringify(obj, null, 2) + '\n' let text = JSON.stringify(obj, null, 2)
for (const key of OBFUSCATED_KEYS) {
text = text.replaceAll(`"${key.runtime}":`, `"${key.serialized}":`)
}
return text + '\n'
} }
function countLeafKeys(obj) { function countLeafKeys(obj) {
......
...@@ -22,6 +22,12 @@ interface FooterProps { ...@@ -22,6 +22,12 @@ interface FooterProps {
className?: string className?: string
} }
const NEW_API_FOOTER_ATTRIBUTION_KEY = [
'footer',
'new' + 'api',
'projectAttributionSuffix',
].join('.')
function FooterLinkItem(props: { link: FooterLink }) { function FooterLinkItem(props: { link: FooterLink }) {
const { t } = useTranslation() const { t } = useTranslation()
const isExternal = props.link.href.startsWith('http') const isExternal = props.link.href.startsWith('http')
...@@ -50,6 +56,27 @@ function FooterLinkItem(props: { link: FooterLink }) { ...@@ -50,6 +56,27 @@ function FooterLinkItem(props: { link: FooterLink }) {
) )
} }
function ProjectAttribution(props: { currentYear: number }) {
const { t } = useTranslation()
return (
<div className='text-muted-foreground/45 text-center text-xs sm:text-right'>
<span className='text-muted-foreground/45'>
&copy; {props.currentYear}{' '}
<a
href='https://github.com/QuantumNous/new-api'
target='_blank'
rel='noopener noreferrer'
className='text-foreground/70 font-medium transition-colors hover:text-foreground'
>
{t('New API')}
</a>
. {t(NEW_API_FOOTER_ATTRIBUTION_KEY)}
</span>
</div>
)
}
export function Footer(props: FooterProps) { export function Footer(props: FooterProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
...@@ -125,10 +152,19 @@ export function Footer(props: FooterProps) { ...@@ -125,10 +152,19 @@ export function Footer(props: FooterProps) {
if (footerHtml) { if (footerHtml) {
return ( return (
<footer className={cn('border-border/40 relative z-10 border-t', props.className)}>
<div className='mx-auto w-full max-w-6xl px-6 py-5'>
<div className='bg-muted/20 border-border/50 flex flex-col items-center justify-between gap-4 rounded-2xl border px-4 py-4 backdrop-blur-sm sm:flex-row sm:px-5'>
<div <div
className='custom-footer w-full' className='custom-footer text-muted-foreground min-w-0 text-center text-sm sm:text-left'
dangerouslySetInnerHTML={{ __html: footerHtml }} dangerouslySetInnerHTML={{ __html: footerHtml }}
/> />
<div className='border-border/60 w-full border-t pt-4 sm:w-auto sm:border-t-0 sm:border-l sm:pt-0 sm:pl-5'>
<ProjectAttribution currentYear={currentYear} />
</div>
</div>
</div>
</footer>
) )
} }
...@@ -182,19 +218,7 @@ export function Footer(props: FooterProps) { ...@@ -182,19 +218,7 @@ export function Footer(props: FooterProps) {
&copy; {currentYear} {displayName}.{' '} &copy; {currentYear} {displayName}.{' '}
{props.copyright ?? t('footer.defaultCopyright')} {props.copyright ?? t('footer.defaultCopyright')}
</p> </p>
<div className='flex items-center gap-2'> <ProjectAttribution currentYear={currentYear} />
<span className='text-muted-foreground/40 text-xs'>
{t('Designed and Developed by')}{' '}
</span>
<a
href='https://github.com/QuantumNous/new-api'
target='_blank'
rel='noopener noreferrer'
className='text-primary text-xs font-medium hover:underline'
>
{t('New API')}
</a>
</div>
</div> </div>
</div> </div>
</footer> </footer>
......
...@@ -42,13 +42,7 @@ export function HowItWorks() { ...@@ -42,13 +42,7 @@ export function HowItWorks() {
</h2> </h2>
</AnimateInView> </AnimateInView>
<div className='relative grid gap-8 md:grid-cols-3 md:gap-12'> <div className='grid gap-8 md:grid-cols-3 md:gap-12'>
{/* Connecting line (desktop) */}
<div
aria-hidden
className='from-border/0 via-border to-border/0 absolute top-12 right-[20%] left-[20%] hidden h-px bg-gradient-to-r md:block'
/>
{steps.map((step, i) => ( {steps.map((step, i) => (
<AnimateInView <AnimateInView
key={step.num} key={step.num}
......
...@@ -47,6 +47,31 @@ const DEFAULT_SIDEBAR_MODULES: SidebarModulesAdminConfig = { ...@@ -47,6 +47,31 @@ const DEFAULT_SIDEBAR_MODULES: SidebarModulesAdminConfig = {
}, },
} }
const mergeWithDefaultSidebarModules = (
config: SidebarModulesAdminConfig
): SidebarModulesAdminConfig => {
const merged: SidebarModulesAdminConfig = { ...config }
Object.entries(DEFAULT_SIDEBAR_MODULES).forEach(
([sectionKey, defaultSection]) => {
const existingSection = merged[sectionKey]
if (!existingSection) {
merged[sectionKey] = { ...defaultSection }
return
}
merged[sectionKey] = { ...defaultSection, ...existingSection }
Object.keys(defaultSection).forEach((moduleKey) => {
if (merged[sectionKey][moduleKey] === undefined) {
merged[sectionKey][moduleKey] = defaultSection[moduleKey]
}
})
}
)
return merged
}
/** /**
* Mapping from URL to configuration keys * Mapping from URL to configuration keys
*/ */
...@@ -87,15 +112,7 @@ function parseSidebarConfig( ...@@ -87,15 +112,7 @@ function parseSidebarConfig(
try { try {
const parsed = JSON.parse(value) as SidebarModulesAdminConfig const parsed = JSON.parse(value) as SidebarModulesAdminConfig
// Ensure chat section and its modules are correctly initialized if missing return mergeWithDefaultSidebarModules(parsed)
if (!parsed.chat) {
parsed.chat = { enabled: true, playground: true, chat: true }
} else {
if (parsed.chat.enabled === undefined) parsed.chat.enabled = true
if (parsed.chat.playground === undefined) parsed.chat.playground = true
if (parsed.chat.chat === undefined) parsed.chat.chat = true
}
return parsed
} catch { } catch {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('Failed to parse sidebar modules configuration') console.error('Failed to parse sidebar modules configuration')
......
...@@ -1593,6 +1593,7 @@ ...@@ -1593,6 +1593,7 @@
"footer.columns.related.links.oneApi": "One API", "footer.columns.related.links.oneApi": "One API",
"footer.columns.related.title": "Related Projects", "footer.columns.related.title": "Related Projects",
"footer.defaultCopyright": "All rights reserved.", "footer.defaultCopyright": "All rights reserved.",
"footer.new\u0061pi.projectAttributionSuffix": "All rights reserved. Designed and developed by the project contributors.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment", "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment",
"For private deployments, format: https://fastgpt.run/api/openapi": "For private deployments, format: https://fastgpt.run/api/openapi", "For private deployments, format: https://fastgpt.run/api/openapi": "For private deployments, format: https://fastgpt.run/api/openapi",
"Force AUTH LOGIN": "Force AUTH LOGIN", "Force AUTH LOGIN": "Force AUTH LOGIN",
......
...@@ -1593,6 +1593,7 @@ ...@@ -1593,6 +1593,7 @@
"footer.columns.related.links.oneApi": "One API", "footer.columns.related.links.oneApi": "One API",
"footer.columns.related.title": "Projets liés", "footer.columns.related.title": "Projets liés",
"footer.defaultCopyright": "Tous droits réservés.", "footer.defaultCopyright": "Tous droits réservés.",
"footer.new\u0061pi.projectAttributionSuffix": "Tous droits réservés. Conçu et développé par les contributeurs du projet.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Pour les canaux ajoutés après le 10 mai 2025, pas besoin de supprimer \".\" des noms de modèles lors du déploiement", "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Pour les canaux ajoutés après le 10 mai 2025, pas besoin de supprimer \".\" des noms de modèles lors du déploiement",
"For private deployments, format: https://fastgpt.run/api/openapi": "Pour les déploiements privés, format : https://fastgpt.run/api/openapi", "For private deployments, format: https://fastgpt.run/api/openapi": "Pour les déploiements privés, format : https://fastgpt.run/api/openapi",
"Force AUTH LOGIN": "Forcer AUTH LOGIN", "Force AUTH LOGIN": "Forcer AUTH LOGIN",
......
...@@ -1593,6 +1593,7 @@ ...@@ -1593,6 +1593,7 @@
"footer.columns.related.links.oneApi": "1つのAPI", "footer.columns.related.links.oneApi": "1つのAPI",
"footer.columns.related.title": "関連プロジェクト", "footer.columns.related.title": "関連プロジェクト",
"footer.defaultCopyright": "すべての権利を留保します。", "footer.defaultCopyright": "すべての権利を留保します。",
"footer.new\u0061pi.projectAttributionSuffix": "すべての権利を留保します。プロジェクトコントリビューターにより設計・開発されています。",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "2025 年 5 月 10 日以降に追加されたチャネルの場合、デプロイ時にモデル名から「.」を削除する必要はありません", "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "2025 年 5 月 10 日以降に追加されたチャネルの場合、デプロイ時にモデル名から「.」を削除する必要はありません",
"For private deployments, format: https://fastgpt.run/api/openapi": "プライベートデプロイメントの場合、形式: https://fastgpt.run/api/openapi", "For private deployments, format: https://fastgpt.run/api/openapi": "プライベートデプロイメントの場合、形式: https://fastgpt.run/api/openapi",
"Force AUTH LOGIN": "AUTH LOGINを強制", "Force AUTH LOGIN": "AUTH LOGINを強制",
......
...@@ -1593,6 +1593,7 @@ ...@@ -1593,6 +1593,7 @@
"footer.columns.related.links.oneApi": "Один API", "footer.columns.related.links.oneApi": "Один API",
"footer.columns.related.title": "Связанные проекты", "footer.columns.related.title": "Связанные проекты",
"footer.defaultCopyright": "Все права защищены.", "footer.defaultCopyright": "Все права защищены.",
"footer.new\u0061pi.projectAttributionSuffix": "Все права защищены. Разработано участниками проекта.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Для каналов, добавленных после 10 мая 2025 г., не нужно удалять \".\" из имён моделей при развёртывании", "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Для каналов, добавленных после 10 мая 2025 г., не нужно удалять \".\" из имён моделей при развёртывании",
"For private deployments, format: https://fastgpt.run/api/openapi": "Для частных развертываний, формат: https://fastgpt.run/api/openapi", "For private deployments, format: https://fastgpt.run/api/openapi": "Для частных развертываний, формат: https://fastgpt.run/api/openapi",
"Force AUTH LOGIN": "Принудительный AUTH LOGIN", "Force AUTH LOGIN": "Принудительный AUTH LOGIN",
......
...@@ -1593,6 +1593,7 @@ ...@@ -1593,6 +1593,7 @@
"footer.columns.related.links.oneApi": "One API", "footer.columns.related.links.oneApi": "One API",
"footer.columns.related.title": "Các Dự Án Liên Quan", "footer.columns.related.title": "Các Dự Án Liên Quan",
"footer.defaultCopyright": "Bản quyền được bảo lưu.", "footer.defaultCopyright": "Bản quyền được bảo lưu.",
"footer.new\u0061pi.projectAttributionSuffix": "Bản quyền được bảo lưu. Được thiết kế và phát triển bởi các cộng tác viên dự án.",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Đối với các kênh được thêm sau ngày 10 tháng 5 năm 2025, không cần loại bỏ \".\" khỏi tên mô hình trong quá trình triển khai", "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "Đối với các kênh được thêm sau ngày 10 tháng 5 năm 2025, không cần loại bỏ \".\" khỏi tên mô hình trong quá trình triển khai",
"For private deployments, format: https://fastgpt.run/api/openapi": "Đối với các triển khai riêng tư, định dạng: https://fastgpt.run/api/openapi", "For private deployments, format: https://fastgpt.run/api/openapi": "Đối với các triển khai riêng tư, định dạng: https://fastgpt.run/api/openapi",
"Force AUTH LOGIN": "Bắt buộc AUTH LOGIN", "Force AUTH LOGIN": "Bắt buộc AUTH LOGIN",
......
...@@ -1593,6 +1593,7 @@ ...@@ -1593,6 +1593,7 @@
"footer.columns.related.links.oneApi": "One API", "footer.columns.related.links.oneApi": "One API",
"footer.columns.related.title": "相关项目", "footer.columns.related.title": "相关项目",
"footer.defaultCopyright": "版权所有。", "footer.defaultCopyright": "版权所有。",
"footer.new\u0061pi.projectAttributionSuffix": "版权所有,由项目贡献者设计与开发。",
"For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "对于 2025 年 5 月 10 日之后添加的渠道,在部署时无需从模型名称中移除 \".\"", "For channels added after May 10, 2025, no need to remove \".\" from model names during deployment": "对于 2025 年 5 月 10 日之后添加的渠道,在部署时无需从模型名称中移除 \".\"",
"For private deployments, format: https://fastgpt.run/api/openapi": "对于私有部署,格式为:https://fastgpt.run/api/openapi", "For private deployments, format: https://fastgpt.run/api/openapi": "对于私有部署,格式为:https://fastgpt.run/api/openapi",
"Force AUTH LOGIN": "强制 AUTH LOGIN", "Force AUTH LOGIN": "强制 AUTH LOGIN",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment