feat: multi-feature update
This commit is contained in:
Vendored
+2
-2
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>New API</title>
|
||||
<meta name="title" content="New API" />
|
||||
<title>BBLBB</title>
|
||||
<meta name="title" content="BBLBB" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Unified AI API gateway and admin dashboard."
|
||||
|
||||
+93
-51
@@ -17,6 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import * as React from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -150,10 +151,101 @@ export function ComboboxInput({
|
||||
item?.scrollIntoView({ block: 'nearest' })
|
||||
}, [highlightedIndex])
|
||||
|
||||
const [dropdownPos, setDropdownPos] = React.useState<{
|
||||
top: number
|
||||
left: number
|
||||
width: number
|
||||
} | null>(null)
|
||||
|
||||
const updateDropdownPos = React.useCallback(() => {
|
||||
if (!containerRef.current) return
|
||||
const rect = containerRef.current.getBoundingClientRect()
|
||||
setDropdownPos({
|
||||
top: rect.bottom + 4,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Update dropdown position when open
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setDropdownPos(null)
|
||||
return
|
||||
}
|
||||
updateDropdownPos()
|
||||
const handleScroll = () => updateDropdownPos()
|
||||
window.addEventListener('scroll', handleScroll, true)
|
||||
window.addEventListener('resize', handleScroll)
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll, true)
|
||||
window.removeEventListener('resize', handleScroll)
|
||||
}
|
||||
}, [open, updateDropdownPos])
|
||||
|
||||
const showDropdown =
|
||||
open &&
|
||||
(filteredOptions.length > 0 || (allowCustomValue && searchValue.trim()))
|
||||
|
||||
const dropdownContent = showDropdown && dropdownPos ? (
|
||||
<div
|
||||
className='bg-popover text-popover-foreground fixed z-[100] rounded-md border shadow-md'
|
||||
style={{
|
||||
top: dropdownPos.top,
|
||||
left: dropdownPos.left,
|
||||
width: dropdownPos.width,
|
||||
}}
|
||||
>
|
||||
{filteredOptions.length > 0 ? (
|
||||
<ul
|
||||
ref={listRef}
|
||||
role='listbox'
|
||||
className='max-h-[200px] overflow-y-auto p-1'
|
||||
>
|
||||
{filteredOptions.map((option, index) => (
|
||||
<li
|
||||
key={option.value}
|
||||
role='option'
|
||||
aria-selected={value === option.value}
|
||||
data-highlighted={index === highlightedIndex}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm select-none',
|
||||
index === highlightedIndex &&
|
||||
'bg-accent text-accent-foreground',
|
||||
value === option.value && 'font-medium'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent blur
|
||||
handleSelect(option.value)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'size-4 shrink-0',
|
||||
value === option.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{option.icon && <span>{option.icon}</span>}
|
||||
<span className='truncate'>{option.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className='px-2 py-6 text-center text-sm'>
|
||||
{t(emptyText)}
|
||||
{allowCustomValue && searchValue.trim() && (
|
||||
<div className='text-muted-foreground mt-1 text-xs'>
|
||||
{t('Press Enter to use "{{value}}"', {
|
||||
value: searchValue.trim(),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='relative'>
|
||||
<Input
|
||||
@@ -184,57 +276,7 @@ export function ComboboxInput({
|
||||
/>
|
||||
<ChevronsUpDown className='pointer-events-none absolute top-1/2 right-3 size-4 shrink-0 -translate-y-1/2 opacity-50' />
|
||||
|
||||
{showDropdown && (
|
||||
<div className='bg-popover text-popover-foreground absolute top-full z-100 mt-1 w-full rounded-md border shadow-md'>
|
||||
{filteredOptions.length > 0 ? (
|
||||
<ul
|
||||
ref={listRef}
|
||||
role='listbox'
|
||||
className='max-h-[200px] overflow-y-auto p-1'
|
||||
>
|
||||
{filteredOptions.map((option, index) => (
|
||||
<li
|
||||
key={option.value}
|
||||
role='option'
|
||||
aria-selected={value === option.value}
|
||||
data-highlighted={index === highlightedIndex}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm select-none',
|
||||
index === highlightedIndex &&
|
||||
'bg-accent text-accent-foreground',
|
||||
value === option.value && 'font-medium'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault() // Prevent blur
|
||||
handleSelect(option.value)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'size-4 shrink-0',
|
||||
value === option.value ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
{option.icon && <span>{option.icon}</span>}
|
||||
<span className='truncate'>{option.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className='px-2 py-6 text-center text-sm'>
|
||||
{t(emptyText)}
|
||||
{allowCustomValue && searchValue.trim() && (
|
||||
<div className='text-muted-foreground mt-1 text-xs'>
|
||||
{t('Press Enter to use "{{value}}"', {
|
||||
value: searchValue.trim(),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{dropdownContent && createPortal(dropdownContent, document.body)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,31 @@ import {
|
||||
} from '../lib/oauth'
|
||||
import type { SystemStatus, CustomOAuthProviderInfo } from '../types'
|
||||
|
||||
/**
|
||||
* Generate a random code verifier for PKCE
|
||||
*/
|
||||
function generateCodeVerifier(): string {
|
||||
const array = new Uint8Array(32)
|
||||
crypto.getRandomValues(array)
|
||||
return btoa(String.fromCharCode(...array))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code challenge from code verifier using SHA-256
|
||||
*/
|
||||
async function generateCodeChallenge(verifier: string): Promise<string> {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(verifier)
|
||||
const digest = await crypto.subtle.digest('SHA-256', data)
|
||||
return btoa(String.fromCharCode(...new Uint8Array(digest)))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
}
|
||||
|
||||
type LogoutRequestConfig = AxiosRequestConfig & {
|
||||
skipErrorHandler?: boolean
|
||||
}
|
||||
@@ -211,6 +236,16 @@ export function useOAuthLogin(status: SystemStatus | null) {
|
||||
url.searchParams.set('scope', provider.scopes)
|
||||
}
|
||||
|
||||
// Add PKCE support if enabled
|
||||
if (provider.pkce_enabled) {
|
||||
const codeVerifier = generateCodeVerifier()
|
||||
const codeChallenge = await generateCodeChallenge(codeVerifier)
|
||||
// Store code_verifier in sessionStorage keyed by state
|
||||
sessionStorage.setItem(`pkce_verifier_${state}`, codeVerifier)
|
||||
url.searchParams.set('code_challenge', codeChallenge)
|
||||
url.searchParams.set('code_challenge_method', 'S256')
|
||||
}
|
||||
|
||||
window.open(url.toString(), '_self')
|
||||
} catch (_error) {
|
||||
toast.error(
|
||||
|
||||
+1
@@ -195,6 +195,7 @@ export interface CustomOAuthProviderInfo {
|
||||
client_id: string
|
||||
authorization_endpoint: string
|
||||
scopes: string
|
||||
pkce_enabled: boolean
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Vendored
+9
-1
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { api } from '@/lib/api'
|
||||
import type { HomePageContentResponse } from './types'
|
||||
import type { HomePageContentResponse, HomeStatsResponse } from './types'
|
||||
|
||||
// ============================================================================
|
||||
// Home Page APIs
|
||||
@@ -31,3 +31,11 @@ export async function getHomePageContent(): Promise<HomePageContentResponse> {
|
||||
const res = await api.get('/api/home_page_content')
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getHomeStats(): Promise<HomeStatsResponse> {
|
||||
const res = await api.get('/api/home_stats', {
|
||||
skipBusinessError: true,
|
||||
skipErrorHandler: true,
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
+150
-202
@@ -16,226 +16,174 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { CherryStudio } from '@lobehub/icons'
|
||||
import { ArrowRight, BookOpen } from 'lucide-react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStatus } from '@/hooks/use-status'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { HeroTerminalDemo } from '../hero-terminal-demo'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getHomeStats } from '../../api'
|
||||
import type { HomeStats } from '../../types'
|
||||
import { HeroButtons } from '../hero-buttons'
|
||||
|
||||
interface HeroProps {
|
||||
className?: string
|
||||
isAuthenticated?: boolean
|
||||
}
|
||||
|
||||
// Stylized three-dots indicator representing "More"
|
||||
const MoreIcon = () => (
|
||||
<svg
|
||||
className='text-muted-foreground/60 group-hover:text-foreground size-6 shrink-0 transition-colors'
|
||||
viewBox='0 0 24 24'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<circle cx='6' cy='12' r='2' fill='currentColor' />
|
||||
<circle cx='12' cy='12' r='2' fill='currentColor' />
|
||||
<circle cx='18' cy='12' r='2' fill='currentColor' />
|
||||
</svg>
|
||||
)
|
||||
interface UsageMetricProps {
|
||||
label: string
|
||||
value: string
|
||||
percent?: number
|
||||
}
|
||||
|
||||
function clampPercent(value: number | undefined): number {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return 0
|
||||
return Math.max(0, Math.min(100, value))
|
||||
}
|
||||
|
||||
function formatPercent(value: number | undefined): string {
|
||||
return `${clampPercent(value).toFixed(2)}%`
|
||||
}
|
||||
|
||||
function formatBytes(value: number | undefined): string {
|
||||
if (!value) return '--'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = value
|
||||
let unitIndex = 0
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex += 1
|
||||
}
|
||||
|
||||
return `${size.toFixed(size >= 10 ? 0 : 1)}${units[unitIndex]}`
|
||||
}
|
||||
|
||||
function formatCompactNumber(value: number | undefined): string {
|
||||
if (typeof value !== 'number') return '--'
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
function getUsageColor(percent: number): string {
|
||||
if (percent >= 85) return 'bg-red-500'
|
||||
if (percent >= 65) return 'bg-orange-400'
|
||||
return 'bg-green-500'
|
||||
}
|
||||
|
||||
function UsageMetric(props: UsageMetricProps) {
|
||||
const percent = clampPercent(props.percent)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col whitespace-nowrap sm:w-20 lg:w-16'>
|
||||
<p className='text-muted-foreground text-xs'>{props.label}</p>
|
||||
<div className='flex items-center gap-1 text-xs font-semibold'>{props.value}</div>
|
||||
{typeof props.percent === 'number' ? (
|
||||
<div
|
||||
aria-label={`${props.label} Server Usage Bar`}
|
||||
aria-valuemax={100}
|
||||
aria-valuemin={0}
|
||||
aria-valuenow={percent}
|
||||
className='bg-secondary relative mt-1 h-[3px] w-full overflow-hidden rounded-sm'
|
||||
role='progressbar'
|
||||
>
|
||||
<div
|
||||
className={cn('h-full w-full flex-1 transition-all', getUsageColor(percent))}
|
||||
style={{ transform: `translateX(-${100 - percent}%)` }}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ServerCard(props: {
|
||||
name: string
|
||||
region: string
|
||||
stats?: HomeStats
|
||||
tokenValue: string
|
||||
memoryValue: string
|
||||
}) {
|
||||
const cpuUsage = clampPercent(props.stats?.cpu_usage)
|
||||
const memoryUsage = clampPercent(props.stats?.memory_usage)
|
||||
|
||||
return (
|
||||
<div
|
||||
className='bg-card text-card-foreground flex cursor-pointer flex-col items-center justify-start gap-3 rounded-lg p-3 shadow-md shadow-stone-200/50 ring ring-stone-200 transition-all hover:shadow-sm hover:ring-stone-300 md:px-5 dark:shadow-none dark:ring-stone-800 dark:hover:ring-stone-700 lg:flex-row'
|
||||
>
|
||||
<section className='grid items-center gap-2 lg:w-44 [grid-template-columns:auto_auto_1fr]'>
|
||||
<span className='h-2 w-2 shrink-0 self-center rounded-full bg-green-500' />
|
||||
<div className='flex min-w-[24px] items-center justify-center rounded-sm bg-muted px-1 text-[10px] font-semibold text-muted-foreground'>
|
||||
{props.region}
|
||||
</div>
|
||||
<div className='relative flex flex-col'>
|
||||
<p className='break-normal text-xs font-bold tracking-tight'>{props.name}</p>
|
||||
<p className='hidden text-[11px] text-muted-foreground lg:block'>Online</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className='-mt-2 flex items-center gap-2 lg:hidden' />
|
||||
|
||||
<div className='flex flex-col items-center gap-2 lg:items-start'>
|
||||
<section className='flex flex-wrap items-center gap-x-4 gap-y-2 sm:gap-x-5'>
|
||||
<UsageMetric label='CPU' value={formatPercent(cpuUsage)} percent={cpuUsage} />
|
||||
<UsageMetric label='MEM' value={formatPercent(memoryUsage)} percent={memoryUsage} />
|
||||
<UsageMetric label='RAM' value={props.memoryValue} />
|
||||
<UsageMetric label='Tokens' value={props.tokenValue} />
|
||||
<UsageMetric label='Mode' value='Proxy' />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Hero(props: HeroProps) {
|
||||
const { t } = useTranslation()
|
||||
const { status } = useStatus()
|
||||
const docsUrl =
|
||||
(status?.docs_link as string | undefined) || 'https://docs.newapi.pro'
|
||||
|
||||
const renderDocsButton = () => {
|
||||
const isExternal = docsUrl.startsWith('http')
|
||||
if (isExternal) {
|
||||
return (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='group border-border/50 hover:border-border hover:bg-muted/50 inline-flex h-11 items-center gap-1.5 rounded-lg px-5 text-sm font-medium'
|
||||
render={
|
||||
<a href={docsUrl} target='_blank' rel='noopener noreferrer' />
|
||||
}
|
||||
>
|
||||
<BookOpen className='text-muted-foreground/80 group-hover:text-foreground size-4 transition-colors duration-200' />
|
||||
<span>{t('Docs')}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='group border-border/50 hover:border-border hover:bg-muted/50 inline-flex h-11 items-center gap-1.5 rounded-lg px-5 text-sm font-medium'
|
||||
render={<Link to={docsUrl} />}
|
||||
>
|
||||
<BookOpen className='text-muted-foreground/80 group-hover:text-foreground size-4 transition-colors duration-200' />
|
||||
<span>{t('Docs')}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
const statsQuery = useQuery({
|
||||
queryKey: ['home-stats'],
|
||||
queryFn: async () => {
|
||||
const response = await getHomeStats()
|
||||
return response.data
|
||||
},
|
||||
refetchInterval: 3 * 1000,
|
||||
staleTime: 2 * 1000,
|
||||
})
|
||||
const memoryValue = `${formatBytes(statsQuery.data?.memory_used)} / ${formatBytes(statsQuery.data?.memory_total)}`
|
||||
const tokenValue = formatCompactNumber(statsQuery.data?.total_tokens)
|
||||
|
||||
return (
|
||||
<section className='relative z-10 overflow-hidden px-6 pt-24 pb-16 md:pt-32 md:pb-24 lg:pt-36 lg:pb-28'>
|
||||
{/* Radial gradient background */}
|
||||
<div
|
||||
aria-hidden
|
||||
className='pointer-events-none absolute inset-0 -z-10 opacity-25 dark:opacity-[0.12]'
|
||||
style={{
|
||||
background: [
|
||||
'radial-gradient(ellipse 60% 50% at 20% 20%, oklch(0.72 0.18 250 / 80%) 0%, transparent 70%)',
|
||||
'radial-gradient(ellipse 50% 40% at 80% 15%, oklch(0.65 0.15 200 / 60%) 0%, transparent 70%)',
|
||||
'radial-gradient(ellipse 40% 35% at 40% 80%, oklch(0.70 0.12 280 / 40%) 0%, transparent 70%)',
|
||||
].join(', '),
|
||||
}}
|
||||
/>
|
||||
{/* Grid pattern */}
|
||||
<div
|
||||
aria-hidden
|
||||
className='absolute inset-0 -z-10 bg-[linear-gradient(to_right,var(--border)_1px,transparent_1px),linear-gradient(to_bottom,var(--border)_1px,transparent_1px)] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_30%,black_20%,transparent_100%)] bg-[size:4rem_4rem] opacity-[0.08]'
|
||||
/>
|
||||
|
||||
<div className='mx-auto grid max-w-6xl grid-cols-1 items-start gap-12 lg:grid-cols-12 lg:gap-8'>
|
||||
{/* Left Column: Title, description, action buttons and application support */}
|
||||
<div className='flex flex-col items-start text-left lg:col-span-6'>
|
||||
{/* Top Pill Badge */}
|
||||
<div
|
||||
className='landing-animate-fade-up mb-5 inline-flex items-center gap-1.5 rounded-full border border-blue-500/20 bg-blue-500/5 px-3 py-1.5 text-[11px] font-medium text-blue-600 opacity-0 shadow-xs dark:border-blue-400/20 dark:bg-blue-400/5 dark:text-blue-400'
|
||||
style={{ animationDelay: '0ms' }}
|
||||
>
|
||||
<span className='relative flex size-1.5'>
|
||||
<span className='absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-75' />
|
||||
<span className='relative inline-flex size-1.5 rounded-full bg-blue-500 dark:bg-blue-400' />
|
||||
</span>
|
||||
<span>{t('AI Application Infrastructure Foundation')}</span>
|
||||
<section
|
||||
className={cn(
|
||||
'relative min-h-screen overflow-hidden bg-stone-50 text-foreground dark:bg-stone-950',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<div className='absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(120,113,108,0.16),transparent_34%)] dark:bg-[radial-gradient(circle_at_top,rgba(120,113,108,0.12),transparent_34%)]' />
|
||||
<div className='container relative mx-auto flex min-h-screen flex-col items-center justify-center px-6 py-24'>
|
||||
<div className='mx-auto max-w-3xl text-center'>
|
||||
<div className='mx-auto mb-5 inline-flex items-center gap-2 rounded-full bg-card px-3 py-1 text-xs text-muted-foreground shadow-sm ring ring-stone-200 dark:ring-stone-800'>
|
||||
<span className='h-2 w-2 rounded-full bg-green-500' />
|
||||
{statsQuery.isLoading ? t('Syncing') : t('Server Status')}
|
||||
</div>
|
||||
|
||||
<h1
|
||||
className='landing-animate-fade-up text-[clamp(2.25rem,4.5vw,3.25rem)] leading-[1.15] font-bold tracking-tight'
|
||||
style={{ animationDelay: '60ms' }}
|
||||
>
|
||||
{t('Unified API Gateway for')}
|
||||
<br />
|
||||
<span className='bg-gradient-to-r from-blue-400 via-violet-400 to-purple-500 bg-clip-text text-transparent'>
|
||||
{t('Vast Range of AI Models')}
|
||||
</span>
|
||||
<h1 className='text-5xl font-black tracking-tight text-stone-950 md:text-7xl dark:text-stone-50'>
|
||||
Universe Federation
|
||||
</h1>
|
||||
<p
|
||||
className='landing-animate-fade-up text-muted-foreground/80 mt-5 max-w-xl text-base leading-relaxed opacity-0 md:text-[15px]'
|
||||
style={{ animationDelay: '120ms' }}
|
||||
>
|
||||
{t(
|
||||
'Access a vast selection of models via a standard, unified API protocol. Power AI applications, manage digital assets, and connect the Future.'
|
||||
)}
|
||||
<p className='mt-4 text-xl font-semibold text-stone-700 md:text-2xl dark:text-stone-300'>
|
||||
{t('伟大无需多言')}
|
||||
</p>
|
||||
|
||||
<div
|
||||
className='landing-animate-fade-up mt-8 flex flex-wrap items-center gap-3 opacity-0'
|
||||
style={{ animationDelay: '180ms' }}
|
||||
>
|
||||
{props.isAuthenticated ? (
|
||||
<>
|
||||
<Button
|
||||
className='group h-11 rounded-lg px-5 text-sm font-medium'
|
||||
render={<Link to='/dashboard' />}
|
||||
>
|
||||
{t('Go to Dashboard')}
|
||||
<ArrowRight className='ml-1.5 size-4 transition-transform duration-200 group-hover:translate-x-0.5' />
|
||||
</Button>
|
||||
{renderDocsButton()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
className='group h-11 rounded-lg px-5 text-sm font-medium'
|
||||
render={<Link to='/sign-up' />}
|
||||
>
|
||||
{t('Get Started')}
|
||||
<ArrowRight className='ml-1.5 size-4 transition-transform duration-200 group-hover:translate-x-0.5' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='border-border/50 hover:border-border hover:bg-muted/50 h-11 rounded-lg px-5 text-sm font-medium'
|
||||
render={<Link to='/pricing' />}
|
||||
>
|
||||
{t('View Pricing')}
|
||||
</Button>
|
||||
{renderDocsButton()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Supported Apps (参考图二样式,进行卡片化和信息扩充设计,增加视觉高度) */}
|
||||
<div
|
||||
className='landing-animate-fade-up mt-10 w-full max-w-xl opacity-0'
|
||||
style={{ animationDelay: '240ms' }}
|
||||
>
|
||||
<div className='mb-4 flex flex-col gap-1'>
|
||||
<span className='text-muted-foreground/50 text-[10px] font-bold tracking-[0.15em] uppercase'>
|
||||
{t('Supported Applications')}
|
||||
</span>
|
||||
<p className='text-muted-foreground/60 text-xs leading-relaxed'>
|
||||
{t(
|
||||
'Supports one-click configuration and perfectly adapts to NewAPI multi-protocol configuration.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center gap-3'>
|
||||
{/* Cherry Studio */}
|
||||
<a
|
||||
href='https://cherry-ai.com'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group border-border/40 bg-muted/15 text-foreground/80 hover:border-border hover:bg-muted/30 hover:text-foreground flex items-center gap-3 rounded-full border px-5 py-2.5 text-sm font-medium shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:scale-[1.02]'
|
||||
>
|
||||
<CherryStudio.Color size={24} className='shrink-0' />
|
||||
<span>Cherry Studio</span>
|
||||
</a>
|
||||
|
||||
{/* CC Switch */}
|
||||
<a
|
||||
href='https://ccswitch.io'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group border-border/40 bg-muted/15 text-foreground/80 hover:border-border hover:bg-muted/30 hover:text-foreground flex items-center gap-3 rounded-full border px-5 py-2.5 text-sm font-medium shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:scale-[1.02]'
|
||||
>
|
||||
<img
|
||||
src='https://ccswitch.io/favicon.png'
|
||||
alt='CC Switch'
|
||||
className='size-6 shrink-0 rounded-md object-contain'
|
||||
onError={(e) => {
|
||||
// Fallback to a styled text avatar if the remote favicon fails to load in sandbox or local environments
|
||||
e.currentTarget.style.display = 'none'
|
||||
const fallback = e.currentTarget.nextSibling as HTMLElement
|
||||
if (fallback) fallback.style.display = 'flex'
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{ display: 'none' }}
|
||||
className='size-6 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-[10px] font-bold text-blue-600 dark:bg-blue-400/10 dark:text-blue-400'
|
||||
>
|
||||
CC
|
||||
</span>
|
||||
<span>CC Switch</span>
|
||||
</a>
|
||||
|
||||
{/* "更多" */}
|
||||
<div className='group border-border/40 bg-muted/15 text-foreground/55 hover:border-border hover:bg-muted/30 hover:text-foreground flex cursor-default items-center gap-2.5 rounded-full border px-5 py-2.5 text-sm font-medium shadow-[0_1px_2.5px_rgba(0,0,0,0.01)] backdrop-blur-xs transition-all duration-300 hover:scale-[1.02]'>
|
||||
<MoreIcon />
|
||||
<span>{t('More Apps')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-7 flex flex-col justify-center gap-3 sm:flex-row'>
|
||||
<HeroButtons isAuthenticated={!!props.isAuthenticated} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Hero Terminal API Demo */}
|
||||
<div
|
||||
className='landing-animate-fade-up flex w-full justify-center opacity-0 lg:col-span-6'
|
||||
style={{ animationDelay: '320ms' }}
|
||||
>
|
||||
<HeroTerminalDemo className='mt-8 lg:mt-0' />
|
||||
<div className='mt-12 grid w-full max-w-4xl gap-3'>
|
||||
<ServerCard
|
||||
name='BBLBB'
|
||||
region='CN'
|
||||
stats={statsQuery.data}
|
||||
tokenValue={tokenValue}
|
||||
memoryValue={memoryValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+1
-7
@@ -20,8 +20,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { Markdown } from '@/components/ui/markdown'
|
||||
import { PublicLayout } from '@/components/layout'
|
||||
import { Footer } from '@/components/layout/components/footer'
|
||||
import { CTA, Features, Hero, HowItWorks, Stats } from './components'
|
||||
import { Hero } from './components'
|
||||
import { useHomePageContent } from './hooks'
|
||||
|
||||
export function Home() {
|
||||
@@ -63,11 +62,6 @@ export function Home() {
|
||||
return (
|
||||
<PublicLayout showMainContainer={false}>
|
||||
<Hero isAuthenticated={isAuthenticated} />
|
||||
<Stats />
|
||||
<Features />
|
||||
<HowItWorks />
|
||||
<CTA isAuthenticated={isAuthenticated} />
|
||||
<Footer />
|
||||
</PublicLayout>
|
||||
)
|
||||
}
|
||||
|
||||
+14
@@ -37,3 +37,17 @@ export interface HomePageContentResult {
|
||||
isLoaded: boolean
|
||||
isUrl: boolean
|
||||
}
|
||||
|
||||
export interface HomeStats {
|
||||
cpu_usage: number
|
||||
memory_usage: number
|
||||
memory_total: number
|
||||
memory_used: number
|
||||
total_tokens: number
|
||||
}
|
||||
|
||||
export interface HomeStatsResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: HomeStats
|
||||
}
|
||||
|
||||
Vendored
+24
@@ -96,6 +96,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
||||
email_field: '',
|
||||
well_known: '',
|
||||
auth_style: 0,
|
||||
pkce_enabled: false,
|
||||
access_policy: '',
|
||||
access_denied_message: '',
|
||||
},
|
||||
@@ -120,6 +121,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
||||
email_field: props.provider.email_field || '',
|
||||
well_known: props.provider.well_known || '',
|
||||
auth_style: props.provider.auth_style ?? 0,
|
||||
pkce_enabled: props.provider.pkce_enabled ?? false,
|
||||
access_policy: props.provider.access_policy || '',
|
||||
access_denied_message: props.provider.access_denied_message || '',
|
||||
})
|
||||
@@ -141,6 +143,7 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
||||
email_field: '',
|
||||
well_known: '',
|
||||
auth_style: 0,
|
||||
pkce_enabled: false,
|
||||
access_policy: '',
|
||||
access_denied_message: '',
|
||||
})
|
||||
@@ -373,6 +376,27 @@ export function ProviderFormDialog(props: ProviderFormDialogProps) {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='pkce_enabled'
|
||||
render={({ field }) => (
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Enable PKCE')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t('Use PKCE (Proof Key for Code Exchange) for enhanced security. Required for some providers like Mastodon/Akkoma.')}
|
||||
</FormDescription>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface CustomOAuthProvider {
|
||||
email_field: string
|
||||
well_known: string
|
||||
auth_style: number // 0=auto, 1=params, 2=header
|
||||
pkce_enabled: boolean
|
||||
access_policy: string
|
||||
access_denied_message: string
|
||||
}
|
||||
@@ -73,6 +74,7 @@ export const customOAuthFormSchema = z.object({
|
||||
email_field: z.string().optional().default(''),
|
||||
well_known: z.string().optional().default(''),
|
||||
auth_style: z.number().int().min(0).max(2).default(0),
|
||||
pkce_enabled: z.boolean().default(false),
|
||||
access_policy: z.string().optional().default(''),
|
||||
access_denied_message: z.string().optional().default(''),
|
||||
})
|
||||
|
||||
Vendored
+20
-3
@@ -93,6 +93,7 @@
|
||||
"30 Days": "30 Days",
|
||||
"30 days ago": "30 days ago",
|
||||
"30d change": "30d change",
|
||||
"30s refresh": "30s refresh",
|
||||
"5 minutes": "5 minutes",
|
||||
"5-Hour Window": "5-Hour Window",
|
||||
"50 / page": "50 / page",
|
||||
@@ -101,6 +102,7 @@
|
||||
"80,443,8080": "80,443,8080",
|
||||
"A billing multiplier. Lower ratios mean lower API call costs.": "A billing multiplier. Lower ratios mean lower API call costs.",
|
||||
"A focused home for keys, balance, routing, and service health.": "A focused home for keys, balance, routing, and service health.",
|
||||
"A high-throughput AI API gateway with real-time capacity, resilient routing, and transparent token consumption at the very first glance.": "A high-throughput AI API gateway with real-time capacity, resilient routing, and transparent token consumption at the very first glance.",
|
||||
"About": "About",
|
||||
"About {{days}} days left": "About {{days}} days left",
|
||||
"Accept Unpriced Models": "Accept Unpriced Models",
|
||||
@@ -462,6 +464,7 @@
|
||||
"Availability (last 24h)": "Availability (last 24h)",
|
||||
"Available": "Available",
|
||||
"Available disk space": "Available disk space",
|
||||
"Available headroom": "Available headroom",
|
||||
"Available Models": "Available Models",
|
||||
"Available Rewards": "Available Rewards",
|
||||
"Average latency": "Average latency",
|
||||
@@ -815,6 +818,7 @@
|
||||
"Compliance confirmation required": "Compliance confirmation required",
|
||||
"Compliance confirmed": "Compliance confirmed",
|
||||
"Compliance confirmed successfully": "Compliance confirmed successfully",
|
||||
"Compute usage": "Compute usage",
|
||||
"Concatenate channel system prompt with user's prompt": "Concatenate channel system prompt with user's prompt",
|
||||
"Condition Path": "Condition Path",
|
||||
"Condition Settings": "Condition Settings",
|
||||
@@ -1348,6 +1352,7 @@
|
||||
"edit_this": "edit_this",
|
||||
"Editor mode": "Editor mode",
|
||||
"Education": "Education",
|
||||
"Elastic compute headroom": "Elastic compute headroom",
|
||||
"Email": "Email",
|
||||
"Email (required for verification)": "Email (required for verification)",
|
||||
"Email Address": "Email Address",
|
||||
@@ -1528,8 +1533,8 @@
|
||||
"Exists": "Exists",
|
||||
"Expand": "Expand",
|
||||
"Expand All": "Expand All",
|
||||
"Expected a JSON array.": "Expected a JSON array.",
|
||||
"Expected a JSON array of group identifiers": "Expected a JSON array of group identifiers",
|
||||
"Expected a JSON array.": "Expected a JSON array.",
|
||||
"Experiment with prompts and models in real time.": "Experiment with prompts and models in real time.",
|
||||
"Expiration Time": "Expiration Time",
|
||||
"expired": "expired",
|
||||
@@ -1816,6 +1821,7 @@
|
||||
"Full width": "Full width",
|
||||
"Function calling": "Function calling",
|
||||
"Functions": "Functions",
|
||||
"Gateway Load": "Gateway Load",
|
||||
"GC Count": "GC Count",
|
||||
"GC executed": "GC executed",
|
||||
"GC execution failed": "GC execution failed",
|
||||
@@ -1925,6 +1931,7 @@
|
||||
"header. Anthropic-formatted endpoints accept the": "header. Anthropic-formatted endpoints accept the",
|
||||
"Health": "Health",
|
||||
"Healthy": "Healthy",
|
||||
"HHHL AI Gateway": "Universe Federation",
|
||||
"Hidden — verify to reveal": "Hidden — verify to reveal",
|
||||
"Hide": "Hide",
|
||||
"Hide API key": "Hide API key",
|
||||
@@ -2198,6 +2205,9 @@
|
||||
"List of models supported by this channel. Use comma to separate multiple models.": "List of models supported by this channel. Use comma to separate multiple models.",
|
||||
"List of origins (one per line) allowed for Passkey registration and authentication.": "List of origins (one per line) allowed for Passkey registration and authentication.",
|
||||
"List view": "List view",
|
||||
"Live capacity telemetry": "Live capacity telemetry",
|
||||
"Live resource telemetry": "Live resource telemetry",
|
||||
"Live Status": "Live Status",
|
||||
"LLM Leaderboard": "LLM Leaderboard",
|
||||
"LLM prompt helper": "LLM prompt helper",
|
||||
"Load Balancing": "Load Balancing",
|
||||
@@ -2295,6 +2305,7 @@
|
||||
"Media pricing": "Media pricing",
|
||||
"Median time-to-first-token (TTFT) sampled hourly per group": "Median time-to-first-token (TTFT) sampled hourly per group",
|
||||
"Medical Q&A, mental health support": "Medical Q&A, mental health support",
|
||||
"Memory Capacity": "Memory Capacity",
|
||||
"Memory Hits": "Memory Hits",
|
||||
"Memory Threshold (%)": "Memory Threshold (%)",
|
||||
"Merchant ID": "Merchant ID",
|
||||
@@ -2646,6 +2657,7 @@
|
||||
"No Users Found": "No Users Found",
|
||||
"No vendor data available": "No vendor data available",
|
||||
"No X Found": "No X Found",
|
||||
"Node": "Node",
|
||||
"Node Name": "Node Name",
|
||||
"Non-stream": "Non-stream",
|
||||
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.",
|
||||
@@ -3193,6 +3205,7 @@
|
||||
"Reasoning Effort": "Reasoning Effort",
|
||||
"Receive Upstream Model Update Notifications": "Receive Upstream Model Update Notifications",
|
||||
"Received": "Received",
|
||||
"Received amount": "Received amount",
|
||||
"Recently launched models": "Recently launched models",
|
||||
"Recently launched models gaining traction": "Recently launched models gaining traction",
|
||||
"Recharge": "Recharge",
|
||||
@@ -3209,7 +3222,6 @@
|
||||
"Redeem codes": "Redeem codes",
|
||||
"Redeemed By": "Redeemed By",
|
||||
"Redeemed:": "Redeemed:",
|
||||
"Received amount": "Received amount",
|
||||
"redemption code": "redemption code",
|
||||
"Redemption Code": "Redemption Code",
|
||||
"Redemption code deleted successfully": "Redemption code deleted successfully",
|
||||
@@ -3611,6 +3623,8 @@
|
||||
"Server IP": "Server IP",
|
||||
"Server Log Management": "Server Log Management",
|
||||
"Server logging is not enabled (log directory not configured)": "Server logging is not enabled (log directory not configured)",
|
||||
"Server Power Core": "Server Power Core",
|
||||
"Server Status": "Server Status",
|
||||
"Server Token": "Server Token",
|
||||
"Service account JSON file(s)": "Service account JSON file(s)",
|
||||
"Session expired!": "Session expired!",
|
||||
@@ -3828,6 +3842,7 @@
|
||||
"Sync Upstream": "Sync Upstream",
|
||||
"Sync Upstream Models": "Sync Upstream Models",
|
||||
"Synchronize models and vendors from an upstream source": "Synchronize models and vendors from an upstream source",
|
||||
"Syncing": "Syncing",
|
||||
"Syncing prices, please wait...": "Syncing prices, please wait...",
|
||||
"Syncing...": "Syncing...",
|
||||
"System": "System",
|
||||
@@ -4108,6 +4123,7 @@
|
||||
"Total requests made": "Total requests made",
|
||||
"Total tokens": "Total tokens",
|
||||
"Total Tokens": "Total Tokens",
|
||||
"Total Tokens Burned": "Total Tokens Burned",
|
||||
"Total Usage": "Total Usage",
|
||||
"Total:": "Total:",
|
||||
"TPM": "TPM",
|
||||
@@ -4538,6 +4554,7 @@
|
||||
"Zero retention": "Zero retention",
|
||||
"Zhipu": "Zhipu",
|
||||
"Zhipu V4": "Zhipu V4",
|
||||
"Zoom": "Zoom"
|
||||
"Zoom": "Zoom",
|
||||
"伟大无需多言": "Greatness Needs No Words"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+22
-5
@@ -93,6 +93,7 @@
|
||||
"30 Days": "30 jours",
|
||||
"30 days ago": "Il y a 30 jours",
|
||||
"30d change": "Variation 30 j",
|
||||
"30s refresh": "Actualisation 30 s",
|
||||
"5 minutes": "5 minutes",
|
||||
"5-Hour Window": "Fenêtre de 5 heures",
|
||||
"50 / page": "50 / page",
|
||||
@@ -101,6 +102,7 @@
|
||||
"80,443,8080": "80,443,8080",
|
||||
"A billing multiplier. Lower ratios mean lower API call costs.": "Un multiplicateur de facturation. Plus le ratio est faible, plus le coût des appels API est bas.",
|
||||
"A focused home for keys, balance, routing, and service health.": "Un accueil dédié aux clés, au solde, au routage et à l'état du service.",
|
||||
"A high-throughput AI API gateway with real-time capacity, resilient routing, and transparent token consumption at the very first glance.": "Une passerelle API IA à haut débit avec capacité en temps réel, routage résilient et consommation de tokens transparente dès le premier regard.",
|
||||
"About": "À propos",
|
||||
"About {{days}} days left": "Environ {{days}} jours restants",
|
||||
"Accept Unpriced Models": "Accepter les modèles non tarifés",
|
||||
@@ -462,6 +464,7 @@
|
||||
"Availability (last 24h)": "Disponibilité (dernières 24 h)",
|
||||
"Available": "Disponible",
|
||||
"Available disk space": "Espace disque disponible",
|
||||
"Available headroom": "Marge disponible",
|
||||
"Available Models": "Modèles disponibles",
|
||||
"Available Rewards": "Récompenses disponibles",
|
||||
"Average latency": "Latence moyenne",
|
||||
@@ -815,6 +818,7 @@
|
||||
"Compliance confirmation required": "Confirmation de conformité requise",
|
||||
"Compliance confirmed": "Conformité confirmée",
|
||||
"Compliance confirmed successfully": "Conformité confirmée avec succès",
|
||||
"Compute usage": "Utilisation du calcul",
|
||||
"Concatenate channel system prompt with user's prompt": "Concaténer l'invite système du canal avec l'invite de l'utilisateur",
|
||||
"Condition Path": "Chemin de condition",
|
||||
"Condition Settings": "Paramètres de condition",
|
||||
@@ -1348,6 +1352,7 @@
|
||||
"edit_this": "modifier_ceci",
|
||||
"Editor mode": "Mode éditeur",
|
||||
"Education": "Éducation",
|
||||
"Elastic compute headroom": "Marge de calcul élastique",
|
||||
"Email": "E-mail",
|
||||
"Email (required for verification)": "E-mail (requis pour la vérification)",
|
||||
"Email Address": "Adresse e-mail",
|
||||
@@ -1528,8 +1533,8 @@
|
||||
"Exists": "Existe",
|
||||
"Expand": "Développer",
|
||||
"Expand All": "Tout développer",
|
||||
"Expected a JSON array.": "Un tableau JSON est attendu.",
|
||||
"Expected a JSON array of group identifiers": "Un tableau JSON d'identifiants de groupe est attendu",
|
||||
"Expected a JSON array.": "Un tableau JSON est attendu.",
|
||||
"Experiment with prompts and models in real time.": "Expérimentez avec des prompts et des modèles en temps réel.",
|
||||
"Expiration Time": "Heure d'expiration",
|
||||
"expired": "expiré",
|
||||
@@ -1816,6 +1821,7 @@
|
||||
"Full width": "Pleine largeur",
|
||||
"Function calling": "Appel de fonction",
|
||||
"Functions": "Fonctions",
|
||||
"Gateway Load": "Charge de la passerelle",
|
||||
"GC Count": "Nombre de GC",
|
||||
"GC executed": "GC exécuté",
|
||||
"GC execution failed": "Échec de l'exécution du GC",
|
||||
@@ -1924,7 +1930,8 @@
|
||||
"Header Value (supports string or JSON mapping)": "Valeur de l'en-tête (chaîne ou mappage JSON)",
|
||||
"header. Anthropic-formatted endpoints accept the": ". Les points de terminaison au format Anthropic acceptent à la place",
|
||||
"Health": "Santé",
|
||||
"Healthy": "Normal",
|
||||
"Healthy": "Sain",
|
||||
"HHHL AI Gateway": "Fédération de l'Univers",
|
||||
"Hidden — verify to reveal": "Masqué — vérifiez pour révéler",
|
||||
"Hide": "Masquer",
|
||||
"Hide API key": "Masquer la clé API",
|
||||
@@ -2198,6 +2205,9 @@
|
||||
"List of models supported by this channel. Use comma to separate multiple models.": "Liste des modèles pris en charge par ce canal. Utilisez une virgule pour séparer plusieurs modèles.",
|
||||
"List of origins (one per line) allowed for Passkey registration and authentication.": "Liste des origines (une par ligne) autorisées pour l'enregistrement et l'authentification des clés d'accès (Passkey).",
|
||||
"List view": "Vue en liste",
|
||||
"Live capacity telemetry": "Télémétrie de capacité en direct",
|
||||
"Live resource telemetry": "Télémétrie des ressources en direct",
|
||||
"Live Status": "État en direct",
|
||||
"LLM Leaderboard": "Classement des LLM",
|
||||
"LLM prompt helper": "Assistant prompt LLM",
|
||||
"Load Balancing": "Équilibrage de charge",
|
||||
@@ -2295,6 +2305,7 @@
|
||||
"Media pricing": "Tarification multimédia",
|
||||
"Median time-to-first-token (TTFT) sampled hourly per group": "Latence médiane jusqu'au premier jeton (TTFT) échantillonnée par heure et par groupe",
|
||||
"Medical Q&A, mental health support": "Q&R médicales, soutien en santé mentale",
|
||||
"Memory Capacity": "Capacité mémoire",
|
||||
"Memory Hits": "Hits mémoire",
|
||||
"Memory Threshold (%)": "Seuil mémoire (%)",
|
||||
"Merchant ID": "ID du commerçant",
|
||||
@@ -2646,6 +2657,7 @@
|
||||
"No Users Found": "Aucun utilisateur trouvé",
|
||||
"No vendor data available": "Aucune donnée de fournisseur disponible",
|
||||
"No X Found": "Aucun X trouvé",
|
||||
"Node": "Nœud",
|
||||
"Node Name": "Nom du nœud",
|
||||
"Non-stream": "Non-streaming",
|
||||
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Les récompenses d’invitation non nulles nécessitent une confirmation de conformité dans les paramètres de la passerelle de paiement.",
|
||||
@@ -3193,6 +3205,7 @@
|
||||
"Reasoning Effort": "Effort de raisonnement",
|
||||
"Receive Upstream Model Update Notifications": "Recevoir les notifications de mise à jour des modèles en amont",
|
||||
"Received": "Reçu",
|
||||
"Received amount": "Montant reçu",
|
||||
"Recently launched models": "Modèles récemment lancés",
|
||||
"Recently launched models gaining traction": "Modèles récemment publiés et en forte progression",
|
||||
"Recharge": "Recharger",
|
||||
@@ -3209,7 +3222,6 @@
|
||||
"Redeem codes": "Échanger des codes",
|
||||
"Redeemed By": "Utilisé par",
|
||||
"Redeemed:": "Utilisé :",
|
||||
"Received amount": "Montant reçu",
|
||||
"redemption code": "code d'échange",
|
||||
"Redemption Code": "Code d'échange",
|
||||
"Redemption code deleted successfully": "Code d'échange supprimé avec succès",
|
||||
@@ -3232,7 +3244,7 @@
|
||||
"Referral Program": "Programme de parrainage",
|
||||
"Referral reward transfer is disabled until the administrator confirms compliance terms.": "Le transfert des récompenses de parrainage est désactivé jusqu’à ce que l’administrateur confirme les conditions de conformité.",
|
||||
"Refine models by provider, group, type, and tags.": "Affinez les modèles par fournisseur, groupe, type et tags.",
|
||||
"Refresh": "Actualiser",
|
||||
"Refresh": "Actualisation",
|
||||
"Refresh Cache": "Actualiser le cache",
|
||||
"Refresh credential": "Actualiser l'identifiant",
|
||||
"Refresh failed": "Échec de l'actualisation",
|
||||
@@ -3611,6 +3623,8 @@
|
||||
"Server IP": "IP du serveur",
|
||||
"Server Log Management": "Gestion des journaux serveur",
|
||||
"Server logging is not enabled (log directory not configured)": "La journalisation serveur n'est pas activée (répertoire non configuré)",
|
||||
"Server Power Core": "Cœur de puissance serveur",
|
||||
"Server Status": "État du serveur",
|
||||
"Server Token": "Jeton de serveur",
|
||||
"Service account JSON file(s)": "Fichier(s) JSON de compte de service",
|
||||
"Session expired!": "Session expirée !",
|
||||
@@ -3828,6 +3842,7 @@
|
||||
"Sync Upstream": "Synchroniser l'amont",
|
||||
"Sync Upstream Models": "Synchroniser les modèles amont",
|
||||
"Synchronize models and vendors from an upstream source": "Synchroniser les modèles et les fournisseurs à partir d'une source amont",
|
||||
"Syncing": "Synchronisation",
|
||||
"Syncing prices, please wait...": "Synchronisation des prix, veuillez patienter...",
|
||||
"Syncing...": "Synchronisation...",
|
||||
"System": "Système",
|
||||
@@ -4108,6 +4123,7 @@
|
||||
"Total requests made": "Requêtes totales effectuées",
|
||||
"Total tokens": "Jetons totaux",
|
||||
"Total Tokens": "Jetons totaux",
|
||||
"Total Tokens Burned": "Total des tokens consommés",
|
||||
"Total Usage": "Utilisation totale",
|
||||
"Total:": "Total :",
|
||||
"TPM": "TPM",
|
||||
@@ -4538,6 +4554,7 @@
|
||||
"Zero retention": "Aucune rétention",
|
||||
"Zhipu": "Zhipu",
|
||||
"Zhipu V4": "Zhipu V4",
|
||||
"Zoom": "Zoom"
|
||||
"Zoom": "Zoom",
|
||||
"伟大无需多言": "La grandeur se passe de mots"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+20
-3
@@ -93,6 +93,7 @@
|
||||
"30 Days": "30日",
|
||||
"30 days ago": "30日前",
|
||||
"30d change": "30日変化",
|
||||
"30s refresh": "30秒更新",
|
||||
"5 minutes": "5 分",
|
||||
"5-Hour Window": "5時間ウィンドウ",
|
||||
"50 / page": "50 / ページ",
|
||||
@@ -101,6 +102,7 @@
|
||||
"80,443,8080": "80,443,8080",
|
||||
"A billing multiplier. Lower ratios mean lower API call costs.": "課金倍率です。倍率が低いほど API 呼び出しコストは低くなります。",
|
||||
"A focused home for keys, balance, routing, and service health.": "キー、残高、ルーティング、サービス状態を集約したホームです。",
|
||||
"A high-throughput AI API gateway with real-time capacity, resilient routing, and transparent token consumption at the very first glance.": "リアルタイム容量、耐障害ルーティング、明瞭なトークン消費をファーストビューで示す高スループット AI API ゲートウェイです。",
|
||||
"About": "このサービスについて",
|
||||
"About {{days}} days left": "約 {{days}} 日分",
|
||||
"Accept Unpriced Models": "価格設定されていないモデルを許可",
|
||||
@@ -462,6 +464,7 @@
|
||||
"Availability (last 24h)": "可用性(過去 24 時間)",
|
||||
"Available": "空き",
|
||||
"Available disk space": "利用可能なディスク容量",
|
||||
"Available headroom": "利用可能な余力",
|
||||
"Available Models": "利用可能なモデル",
|
||||
"Available Rewards": "利用可能な報酬",
|
||||
"Average latency": "平均レイテンシ",
|
||||
@@ -815,6 +818,7 @@
|
||||
"Compliance confirmation required": "コンプライアンス確認が必要です",
|
||||
"Compliance confirmed": "コンプライアンス確認済み",
|
||||
"Compliance confirmed successfully": "コンプライアンス確認が完了しました",
|
||||
"Compute usage": "計算使用率",
|
||||
"Concatenate channel system prompt with user's prompt": "チャネルのシステムプロンプトをユーザーのプロンプトと連結する",
|
||||
"Condition Path": "条件パス",
|
||||
"Condition Settings": "条件設定",
|
||||
@@ -1348,6 +1352,7 @@
|
||||
"edit_this": "edit_this",
|
||||
"Editor mode": "エディターモード",
|
||||
"Education": "教育",
|
||||
"Elastic compute headroom": "弾力的な計算余力",
|
||||
"Email": "メールアドレス",
|
||||
"Email (required for verification)": "メールアドレス(認証に必須)",
|
||||
"Email Address": "メールアドレス",
|
||||
@@ -1528,8 +1533,8 @@
|
||||
"Exists": "存在",
|
||||
"Expand": "展開",
|
||||
"Expand All": "すべて展開",
|
||||
"Expected a JSON array.": "JSON 配列が必要です。",
|
||||
"Expected a JSON array of group identifiers": "グループ識別子の JSON 配列が必要です",
|
||||
"Expected a JSON array.": "JSON 配列が必要です。",
|
||||
"Experiment with prompts and models in real time.": "プロンプトとモデルをリアルタイムで実験する。",
|
||||
"Expiration Time": "有効期限",
|
||||
"expired": "期限切れ",
|
||||
@@ -1816,6 +1821,7 @@
|
||||
"Full width": "全幅",
|
||||
"Function calling": "関数呼び出し",
|
||||
"Functions": "関数",
|
||||
"Gateway Load": "ゲートウェイ負荷",
|
||||
"GC Count": "GC 回数",
|
||||
"GC executed": "GC 実行完了",
|
||||
"GC execution failed": "GC 実行失敗",
|
||||
@@ -1925,6 +1931,7 @@
|
||||
"header. Anthropic-formatted endpoints accept the": " ヘッダーが必要です。Anthropic 形式のエンドポイントでは",
|
||||
"Health": "ヘルスケア",
|
||||
"Healthy": "正常",
|
||||
"HHHL AI Gateway": "ユニバースフェデレーション",
|
||||
"Hidden — verify to reveal": "非表示 — 確認して表示",
|
||||
"Hide": "非表示にする",
|
||||
"Hide API key": "APIキーを非表示",
|
||||
@@ -2198,6 +2205,9 @@
|
||||
"List of models supported by this channel. Use comma to separate multiple models.": "このチャネルがサポートするモデルのリストです。複数のモデルはカンマで区切ってください。",
|
||||
"List of origins (one per line) allowed for Passkey registration and authentication.": "Passkeyの登録と認証が許可されているオリジン(1行に1つ)のリスト。",
|
||||
"List view": "リスト表示",
|
||||
"Live capacity telemetry": "ライブ容量テレメトリ",
|
||||
"Live resource telemetry": "ライブリソーステレメトリ",
|
||||
"Live Status": "ライブ状態",
|
||||
"LLM Leaderboard": "LLM リーダーボード",
|
||||
"LLM prompt helper": "LLMプロンプトヘルパー",
|
||||
"Load Balancing": "ロードバランシング",
|
||||
@@ -2295,6 +2305,7 @@
|
||||
"Media pricing": "メディア料金",
|
||||
"Median time-to-first-token (TTFT) sampled hourly per group": "グループ別に毎時サンプリングした最初のトークンまでの中央値レイテンシ (TTFT)",
|
||||
"Medical Q&A, mental health support": "医療Q&A・メンタルヘルスサポート",
|
||||
"Memory Capacity": "メモリ容量",
|
||||
"Memory Hits": "メモリヒット",
|
||||
"Memory Threshold (%)": "メモリ閾値 (%)",
|
||||
"Merchant ID": "マーチャントID",
|
||||
@@ -2646,6 +2657,7 @@
|
||||
"No Users Found": "ユーザーが見つかりません",
|
||||
"No vendor data available": "ベンダーデータがありません",
|
||||
"No X Found": "X が見つかりません",
|
||||
"Node": "ノード",
|
||||
"Node Name": "ノード名",
|
||||
"Non-stream": "非ストリーミング",
|
||||
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "0 以外の招待報酬には、支払いゲートウェイ設定でのコンプライアンス確認が必要です。",
|
||||
@@ -3193,6 +3205,7 @@
|
||||
"Reasoning Effort": "推論強度",
|
||||
"Receive Upstream Model Update Notifications": "アップストリームモデル更新通知を受け取る",
|
||||
"Received": "受信済み",
|
||||
"Received amount": "受け取り額",
|
||||
"Recently launched models": "最近リリースされたモデル",
|
||||
"Recently launched models gaining traction": "最近リリースされ勢いのあるモデル",
|
||||
"Recharge": "チャージ",
|
||||
@@ -3209,7 +3222,6 @@
|
||||
"Redeem codes": "コードを交換",
|
||||
"Redeemed By": "引き換え元",
|
||||
"Redeemed:": "引き換え済み:",
|
||||
"Received amount": "受け取り額",
|
||||
"redemption code": "引き換えコード",
|
||||
"Redemption Code": "引き換えコード",
|
||||
"Redemption code deleted successfully": "引き換えコードを正常に削除しました",
|
||||
@@ -3611,6 +3623,8 @@
|
||||
"Server IP": "サーバー IP",
|
||||
"Server Log Management": "サーバーログ管理",
|
||||
"Server logging is not enabled (log directory not configured)": "サーバーログが有効になっていません(ログディレクトリが未設定)",
|
||||
"Server Power Core": "サーバーパワーコア",
|
||||
"Server Status": "サーバー状態",
|
||||
"Server Token": "サーバートークン",
|
||||
"Service account JSON file(s)": "サービスアカウントJSONファイル",
|
||||
"Session expired!": "セッションが期限切れです!",
|
||||
@@ -3828,6 +3842,7 @@
|
||||
"Sync Upstream": "アップストリームを同期",
|
||||
"Sync Upstream Models": "アップストリームモデルを同期",
|
||||
"Synchronize models and vendors from an upstream source": "アップストリームソースからモデルとベンダーを同期",
|
||||
"Syncing": "同期中",
|
||||
"Syncing prices, please wait...": "価格を同期中、しばらくお待ちください...",
|
||||
"Syncing...": "同期中...",
|
||||
"System": "システム",
|
||||
@@ -4108,6 +4123,7 @@
|
||||
"Total requests made": "合計リクエスト数",
|
||||
"Total tokens": "合計トークン",
|
||||
"Total Tokens": "合計トークン",
|
||||
"Total Tokens Burned": "総消費トークン",
|
||||
"Total Usage": "総使用量",
|
||||
"Total:": "合計:",
|
||||
"TPM": "TPM",
|
||||
@@ -4538,6 +4554,7 @@
|
||||
"Zero retention": "データ保持なし",
|
||||
"Zhipu": "Zhipu",
|
||||
"Zhipu V4": "Zhipu V 4",
|
||||
"Zoom": "ズーム"
|
||||
"Zoom": "ズーム",
|
||||
"伟大无需多言": "偉大さに言葉はいらない"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+21
-4
@@ -93,6 +93,7 @@
|
||||
"30 Days": "30 дней",
|
||||
"30 days ago": "30 дней назад",
|
||||
"30d change": "Изменение за 30 дней",
|
||||
"30s refresh": "Обновление 30 с",
|
||||
"5 minutes": "5 минут",
|
||||
"5-Hour Window": "5-часовое окно",
|
||||
"50 / page": "50 / страница",
|
||||
@@ -101,6 +102,7 @@
|
||||
"80,443,8080": "80,443,8080",
|
||||
"A billing multiplier. Lower ratios mean lower API call costs.": "Множитель тарификации. Чем ниже коэффициент, тем ниже стоимость вызовов API.",
|
||||
"A focused home for keys, balance, routing, and service health.": "Единый экран для ключей, баланса, маршрутов и состояния сервиса.",
|
||||
"A high-throughput AI API gateway with real-time capacity, resilient routing, and transparent token consumption at the very first glance.": "Высокопроизводительный AI API-шлюз с емкостью в реальном времени, отказоустойчивой маршрутизацией и прозрачным расходом токенов уже на первом экране.",
|
||||
"About": "О проекте",
|
||||
"About {{days}} days left": "Примерно {{days}} дней",
|
||||
"Accept Unpriced Models": "Принимать модели без цены",
|
||||
@@ -462,6 +464,7 @@
|
||||
"Availability (last 24h)": "Доступность (последние 24 ч)",
|
||||
"Available": "Доступно",
|
||||
"Available disk space": "Доступное дисковое пространство",
|
||||
"Available headroom": "Доступный резерв",
|
||||
"Available Models": "Доступные модели",
|
||||
"Available Rewards": "Доступные награды",
|
||||
"Average latency": "Средняя задержка",
|
||||
@@ -815,6 +818,7 @@
|
||||
"Compliance confirmation required": "Требуется подтверждение соответствия",
|
||||
"Compliance confirmed": "Соответствие подтверждено",
|
||||
"Compliance confirmed successfully": "Соответствие успешно подтверждено",
|
||||
"Compute usage": "Использование вычислений",
|
||||
"Concatenate channel system prompt with user's prompt": "Объединить системный промпт канала с промптом пользователя",
|
||||
"Condition Path": "Путь условия",
|
||||
"Condition Settings": "Настройки условия",
|
||||
@@ -1348,6 +1352,7 @@
|
||||
"edit_this": "изменить_это",
|
||||
"Editor mode": "Режим редактора",
|
||||
"Education": "Образование",
|
||||
"Elastic compute headroom": "Эластичный запас вычислений",
|
||||
"Email": "Электронная почта",
|
||||
"Email (required for verification)": "Email (требуется для верификации)",
|
||||
"Email Address": "Адрес электронной почты",
|
||||
@@ -1528,8 +1533,8 @@
|
||||
"Exists": "Существует",
|
||||
"Expand": "Развернуть",
|
||||
"Expand All": "Развернуть все",
|
||||
"Expected a JSON array.": "Ожидается JSON-массив.",
|
||||
"Expected a JSON array of group identifiers": "Ожидается JSON-массив идентификаторов групп",
|
||||
"Expected a JSON array.": "Ожидается JSON-массив.",
|
||||
"Experiment with prompts and models in real time.": "Экспериментируйте с промптами и моделями в реальном времени.",
|
||||
"Expiration Time": "Время истечения срока действия",
|
||||
"expired": "истек",
|
||||
@@ -1816,6 +1821,7 @@
|
||||
"Full width": "Полная ширина",
|
||||
"Function calling": "Вызов функций",
|
||||
"Functions": "Функции",
|
||||
"Gateway Load": "Нагрузка шлюза",
|
||||
"GC Count": "Кол-во GC",
|
||||
"GC executed": "GC выполнен",
|
||||
"GC execution failed": "Ошибка выполнения GC",
|
||||
@@ -1925,6 +1931,7 @@
|
||||
"header. Anthropic-formatted endpoints accept the": ". Эндпоинты формата Anthropic вместо этого принимают",
|
||||
"Health": "Здоровье",
|
||||
"Healthy": "В норме",
|
||||
"HHHL AI Gateway": "Вселенская Федерация",
|
||||
"Hidden — verify to reveal": "Скрыто — подтвердите, чтобы показать",
|
||||
"Hide": "Скрыть",
|
||||
"Hide API key": "Скрыть API ключ",
|
||||
@@ -2198,6 +2205,9 @@
|
||||
"List of models supported by this channel. Use comma to separate multiple models.": "Список моделей, поддерживаемых этим каналом. Используйте запятую для разделения нескольких моделей.",
|
||||
"List of origins (one per line) allowed for Passkey registration and authentication.": "Список источников (один на строку), разрешенных для регистрации и аутентификации Passkey.",
|
||||
"List view": "Вид списка",
|
||||
"Live capacity telemetry": "Телеметрия емкости в реальном времени",
|
||||
"Live resource telemetry": "Телеметрия ресурсов в реальном времени",
|
||||
"Live Status": "Статус в реальном времени",
|
||||
"LLM Leaderboard": "Рейтинг LLM",
|
||||
"LLM prompt helper": "Помощник с промптом для LLM",
|
||||
"Load Balancing": "Балансировка нагрузки",
|
||||
@@ -2295,6 +2305,7 @@
|
||||
"Media pricing": "Цены для медиа",
|
||||
"Median time-to-first-token (TTFT) sampled hourly per group": "Медианная задержка первого токена (TTFT), измеряемая ежечасно по группам",
|
||||
"Medical Q&A, mental health support": "Медицинские Q&A, поддержка ментального здоровья",
|
||||
"Memory Capacity": "Объем памяти",
|
||||
"Memory Hits": "Попаданий памяти",
|
||||
"Memory Threshold (%)": "Порог памяти (%)",
|
||||
"Merchant ID": "ID мерчанта",
|
||||
@@ -2646,6 +2657,7 @@
|
||||
"No Users Found": "Пользователи не найдены",
|
||||
"No vendor data available": "Данных по поставщикам нет",
|
||||
"No X Found": "X не найдено",
|
||||
"Node": "Узел",
|
||||
"Node Name": "Имя узла",
|
||||
"Non-stream": "Не потоковый",
|
||||
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Ненулевые награды за приглашения требуют подтверждения соответствия в настройках платежного шлюза.",
|
||||
@@ -3193,6 +3205,7 @@
|
||||
"Reasoning Effort": "Интенсивность рассуждения",
|
||||
"Receive Upstream Model Update Notifications": "Получать уведомления об обновлениях вышестоящих моделей",
|
||||
"Received": "Получено",
|
||||
"Received amount": "Полученная сумма",
|
||||
"Recently launched models": "Недавно запущенные модели",
|
||||
"Recently launched models gaining traction": "Недавно вышедшие модели, набирающие популярность",
|
||||
"Recharge": "Пополнение",
|
||||
@@ -3209,7 +3222,6 @@
|
||||
"Redeem codes": "Активировать коды",
|
||||
"Redeemed By": "Активировано",
|
||||
"Redeemed:": "Активировано:",
|
||||
"Received amount": "Полученная сумма",
|
||||
"redemption code": "код активации",
|
||||
"Redemption Code": "Код активации",
|
||||
"Redemption code deleted successfully": "Код активации успешно удален",
|
||||
@@ -3232,7 +3244,7 @@
|
||||
"Referral Program": "Реферальная программа",
|
||||
"Referral reward transfer is disabled until the administrator confirms compliance terms.": "Перевод реферальных наград отключен, пока администратор не подтвердит условия соответствия.",
|
||||
"Refine models by provider, group, type, and tags.": "Уточняйте список моделей по поставщику, группе, типу и тегам.",
|
||||
"Refresh": "Обновить",
|
||||
"Refresh": "Обновление",
|
||||
"Refresh Cache": "Обновить кэш",
|
||||
"Refresh credential": "Обновить учётные данные",
|
||||
"Refresh failed": "Ошибка обновления",
|
||||
@@ -3611,6 +3623,8 @@
|
||||
"Server IP": "IP сервера",
|
||||
"Server Log Management": "Управление журналами сервера",
|
||||
"Server logging is not enabled (log directory not configured)": "Журналирование сервера не включено (каталог журналов не настроен)",
|
||||
"Server Power Core": "Силовое ядро сервера",
|
||||
"Server Status": "Статус сервера",
|
||||
"Server Token": "Токен сервера",
|
||||
"Service account JSON file(s)": "JSON-файл сервисного аккаунта",
|
||||
"Session expired!": "Сессия истекла!",
|
||||
@@ -3828,6 +3842,7 @@
|
||||
"Sync Upstream": "Синхронизировать Upstream",
|
||||
"Sync Upstream Models": "Синхронизировать модели Upstream",
|
||||
"Synchronize models and vendors from an upstream source": "Синхронизировать модели и поставщиков из upstream источника",
|
||||
"Syncing": "Синхронизация",
|
||||
"Syncing prices, please wait...": "Синхронизация цен, подождите...",
|
||||
"Syncing...": "Синхронизация...",
|
||||
"System": "Система",
|
||||
@@ -4108,6 +4123,7 @@
|
||||
"Total requests made": "Всего сделанных запросов",
|
||||
"Total tokens": "Всего токенов",
|
||||
"Total Tokens": "Всего токенов",
|
||||
"Total Tokens Burned": "Всего израсходовано токенов",
|
||||
"Total Usage": "Общее использование",
|
||||
"Total:": "Всего:",
|
||||
"TPM": "TPM",
|
||||
@@ -4538,6 +4554,7 @@
|
||||
"Zero retention": "Без хранения данных",
|
||||
"Zhipu": "Zhipu",
|
||||
"Zhipu V4": "Zhipu V4",
|
||||
"Zoom": "Zoom"
|
||||
"Zoom": "Zoom",
|
||||
"伟大无需多言": "Величие не требует слов"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+21
-4
@@ -93,6 +93,7 @@
|
||||
"30 Days": "30 ngày",
|
||||
"30 days ago": "30 ngày trước",
|
||||
"30d change": "Thay đổi 30 ngày",
|
||||
"30s refresh": "Làm mới 30 giây",
|
||||
"5 minutes": "5 phút",
|
||||
"5-Hour Window": "Cửa sổ 5 giờ",
|
||||
"50 / page": "50 / trang",
|
||||
@@ -101,6 +102,7 @@
|
||||
"80,443,8080": "80,443,8080",
|
||||
"A billing multiplier. Lower ratios mean lower API call costs.": "Hệ số tính phí. Tỷ lệ càng thấp thì chi phí gọi API càng thấp.",
|
||||
"A focused home for keys, balance, routing, and service health.": "Trang tổng quan tập trung cho khóa, số dư, định tuyến và trạng thái dịch vụ.",
|
||||
"A high-throughput AI API gateway with real-time capacity, resilient routing, and transparent token consumption at the very first glance.": "Cổng AI API thông lượng cao hiển thị ngay từ màn hình đầu tiên năng lực thời gian thực, định tuyến bền bỉ và mức tiêu thụ token minh bạch.",
|
||||
"About": "Giới thiệu",
|
||||
"About {{days}} days left": "Còn khoảng {{days}} ngày",
|
||||
"Accept Unpriced Models": "Chấp nhận các Mô hình chưa định giá",
|
||||
@@ -462,6 +464,7 @@
|
||||
"Availability (last 24h)": "Khả dụng (24 giờ qua)",
|
||||
"Available": "Khả dụng",
|
||||
"Available disk space": "Dung lượng đĩa khả dụng",
|
||||
"Available headroom": "Dư địa khả dụng",
|
||||
"Available Models": "Mô hình khả dụng",
|
||||
"Available Rewards": "Phần thưởng hiện có",
|
||||
"Average latency": "Độ trễ trung bình",
|
||||
@@ -815,6 +818,7 @@
|
||||
"Compliance confirmation required": "Cần xác nhận tuân thủ",
|
||||
"Compliance confirmed": "Đã xác nhận tuân thủ",
|
||||
"Compliance confirmed successfully": "Xác nhận tuân thủ thành công",
|
||||
"Compute usage": "Mức dùng tính toán",
|
||||
"Concatenate channel system prompt with user's prompt": "Nối lời nhắc hệ thống kênh với lời nhắc của người dùng",
|
||||
"Condition Path": "Đường dẫn điều kiện",
|
||||
"Condition Settings": "Cài đặt điều kiện",
|
||||
@@ -1348,6 +1352,7 @@
|
||||
"edit_this": "edit_this",
|
||||
"Editor mode": "Chế độ trình sửa",
|
||||
"Education": "Giáo dục",
|
||||
"Elastic compute headroom": "Dư địa tính toán đàn hồi",
|
||||
"Email": "Email",
|
||||
"Email (required for verification)": "Email (bắt buộc để xác minh)",
|
||||
"Email Address": "Địa chỉ email",
|
||||
@@ -1528,8 +1533,8 @@
|
||||
"Exists": "Tồn tại",
|
||||
"Expand": "Mở rộng",
|
||||
"Expand All": "Mở rộng tất cả",
|
||||
"Expected a JSON array.": "Cần là một mảng JSON.",
|
||||
"Expected a JSON array of group identifiers": "Cần là một mảng JSON gồm các định danh nhóm",
|
||||
"Expected a JSON array.": "Cần là một mảng JSON.",
|
||||
"Experiment with prompts and models in real time.": "Thử nghiệm với prompt và mô hình theo thời gian thực.",
|
||||
"Expiration Time": "Thời gian hết hạn",
|
||||
"expired": "Đã hết hạn",
|
||||
@@ -1816,6 +1821,7 @@
|
||||
"Full width": "Toàn chiều rộng",
|
||||
"Function calling": "Gọi hàm",
|
||||
"Functions": "Hàm",
|
||||
"Gateway Load": "Tải cổng",
|
||||
"GC Count": "Số lần GC",
|
||||
"GC executed": "GC đã thực thi",
|
||||
"GC execution failed": "Thực thi GC thất bại",
|
||||
@@ -1924,7 +1930,8 @@
|
||||
"Header Value (supports string or JSON mapping)": "Giá trị header (hỗ trợ chuỗi hoặc ánh xạ JSON)",
|
||||
"header. Anthropic-formatted endpoints accept the": ". Các endpoint định dạng Anthropic chấp nhận header",
|
||||
"Health": "Sức khỏe",
|
||||
"Healthy": "Bình thường",
|
||||
"Healthy": "Khỏe mạnh",
|
||||
"HHHL AI Gateway": "Liên bang Vũ trụ",
|
||||
"Hidden — verify to reveal": "Ẩn — xác minh để hiển thị",
|
||||
"Hide": "Ẩn",
|
||||
"Hide API key": "Ẩn khóa API",
|
||||
@@ -2198,6 +2205,9 @@
|
||||
"List of models supported by this channel. Use comma to separate multiple models.": "Danh sách các mô hình được hỗ trợ bởi kênh này. Sử dụng dấu phẩy để phân tách nhiều mô hình.",
|
||||
"List of origins (one per line) allowed for Passkey registration and authentication.": "Danh sách các nguồn gốc (mỗi dòng một mục) được phép đăng ký và xác thực Passkey.",
|
||||
"List view": "Xem dạng danh sách",
|
||||
"Live capacity telemetry": "Đo lường năng lực trực tiếp",
|
||||
"Live resource telemetry": "Đo lường tài nguyên trực tiếp",
|
||||
"Live Status": "Trạng thái trực tiếp",
|
||||
"LLM Leaderboard": "Bảng xếp hạng LLM",
|
||||
"LLM prompt helper": "Trợ lý prompt LLM",
|
||||
"Load Balancing": "Tải cân bằng",
|
||||
@@ -2295,6 +2305,7 @@
|
||||
"Media pricing": "Giá phương tiện",
|
||||
"Median time-to-first-token (TTFT) sampled hourly per group": "Độ trễ token đầu tiên trung vị (TTFT) lấy mẫu mỗi giờ theo nhóm",
|
||||
"Medical Q&A, mental health support": "Hỏi đáp y tế, hỗ trợ sức khỏe tinh thần",
|
||||
"Memory Capacity": "Dung lượng bộ nhớ",
|
||||
"Memory Hits": "Lượt truy cập bộ nhớ",
|
||||
"Memory Threshold (%)": "Ngưỡng bộ nhớ (%)",
|
||||
"Merchant ID": "Mã thương gia",
|
||||
@@ -2646,6 +2657,7 @@
|
||||
"No Users Found": "Không tìm thấy người dùng nào",
|
||||
"No vendor data available": "Không có dữ liệu nhà cung cấp",
|
||||
"No X Found": "Không tìm thấy X",
|
||||
"Node": "Nút",
|
||||
"Node Name": "Tên nút",
|
||||
"Non-stream": "Không phát trực tuyến",
|
||||
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Phần thưởng mời khác 0 yêu cầu xác nhận tuân thủ trong cài đặt Cổng thanh toán.",
|
||||
@@ -3193,6 +3205,7 @@
|
||||
"Reasoning Effort": "Cường độ suy luận",
|
||||
"Receive Upstream Model Update Notifications": "Nhận thông báo cập nhật mô hình nguồn",
|
||||
"Received": "Đã nhận",
|
||||
"Received amount": "Số tiền đã nhận",
|
||||
"Recently launched models": "Các mô hình ra mắt gần đây",
|
||||
"Recently launched models gaining traction": "Mô hình mới phát hành đang được ưa chuộng",
|
||||
"Recharge": "Nạp lại",
|
||||
@@ -3209,7 +3222,6 @@
|
||||
"Redeem codes": "Đổi mã",
|
||||
"Redeemed By": "Được chuộc bởi",
|
||||
"Redeemed:": "Đã đổi:",
|
||||
"Received amount": "Số tiền đã nhận",
|
||||
"redemption code": "mã đổi thưởng",
|
||||
"Redemption Code": "Mã đổi thưởng",
|
||||
"Redemption code deleted successfully": "Mã đổi thưởng đã xóa thành công",
|
||||
@@ -3611,6 +3623,8 @@
|
||||
"Server IP": "IP máy chủ",
|
||||
"Server Log Management": "Quản lý nhật ký máy chủ",
|
||||
"Server logging is not enabled (log directory not configured)": "Nhật ký máy chủ chưa được bật (chưa cấu hình thư mục nhật ký)",
|
||||
"Server Power Core": "Lõi sức mạnh máy chủ",
|
||||
"Server Status": "Trạng thái máy chủ",
|
||||
"Server Token": "Mã thông báo máy chủ",
|
||||
"Service account JSON file(s)": "Tệp JSON tài khoản dịch vụ",
|
||||
"Session expired!": "Phiên hết hạn!",
|
||||
@@ -3828,6 +3842,7 @@
|
||||
"Sync Upstream": "Đồng bộ nguồn",
|
||||
"Sync Upstream Models": "Đồng bộ các mô hình nguồn",
|
||||
"Synchronize models and vendors from an upstream source": "Đồng bộ hóa các mô hình và nhà cung cấp từ một nguồn thượng nguồn",
|
||||
"Syncing": "Đang đồng bộ",
|
||||
"Syncing prices, please wait...": "Đang đồng bộ giá, vui lòng đợi...",
|
||||
"Syncing...": "Đang đồng bộ...",
|
||||
"System": "Hệ thống",
|
||||
@@ -4108,6 +4123,7 @@
|
||||
"Total requests made": "Tổng lượt yêu cầu",
|
||||
"Total tokens": "Tổng số token",
|
||||
"Total Tokens": "Tổng số token",
|
||||
"Total Tokens Burned": "Tổng token đã tiêu thụ",
|
||||
"Total Usage": "Tổng Mức Sử dụng",
|
||||
"Total:": "Tổng cộng:",
|
||||
"TPM": "TPM",
|
||||
@@ -4538,6 +4554,7 @@
|
||||
"Zero retention": "Không lưu dữ liệu",
|
||||
"Zhipu": "Zhipu",
|
||||
"Zhipu V4": "Zhipu V4",
|
||||
"Zoom": "Zoom"
|
||||
"Zoom": "Zoom",
|
||||
"伟大无需多言": "Vĩ đại không cần nhiều lời"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+21
-4
@@ -93,6 +93,7 @@
|
||||
"30 Days": "30 天",
|
||||
"30 days ago": "30 天前",
|
||||
"30d change": "30 天变化",
|
||||
"30s refresh": "30 秒刷新",
|
||||
"5 minutes": "5 分钟",
|
||||
"5-Hour Window": "5小时窗口",
|
||||
"50 / page": "50 条/页",
|
||||
@@ -101,6 +102,7 @@
|
||||
"80,443,8080": "80,443,8080",
|
||||
"A billing multiplier. Lower ratios mean lower API call costs.": "计费乘数,倍率越低,API 调用费用越低。",
|
||||
"A focused home for keys, balance, routing, and service health.": "集中展示密钥、余额、路由和服务健康状态。",
|
||||
"A high-throughput AI API gateway with real-time capacity, resilient routing, and transparent token consumption at the very first glance.": "高吞吐 AI API 网关,首屏即可展示实时容量、弹性路由与透明 token 消耗。",
|
||||
"About": "关于",
|
||||
"About {{days}} days left": "约剩 {{days}} 天",
|
||||
"Accept Unpriced Models": "接受未定价模型",
|
||||
@@ -462,6 +464,7 @@
|
||||
"Availability (last 24h)": "可用率(最近 24 小时)",
|
||||
"Available": "可用",
|
||||
"Available disk space": "可用磁盘空间",
|
||||
"Available headroom": "可用余量",
|
||||
"Available Models": "可用模型",
|
||||
"Available Rewards": "可用奖励",
|
||||
"Average latency": "平均延迟",
|
||||
@@ -815,6 +818,7 @@
|
||||
"Compliance confirmation required": "需要确认合规条款",
|
||||
"Compliance confirmed": "合规已确认",
|
||||
"Compliance confirmed successfully": "合规确认成功",
|
||||
"Compute usage": "计算资源占用",
|
||||
"Concatenate channel system prompt with user's prompt": "将渠道系统提示与用户的提示连接起来",
|
||||
"Condition Path": "条件路径",
|
||||
"Condition Settings": "条件项设置",
|
||||
@@ -1348,6 +1352,7 @@
|
||||
"edit_this": "edit_this",
|
||||
"Editor mode": "编辑器模式",
|
||||
"Education": "教育",
|
||||
"Elastic compute headroom": "弹性算力余量",
|
||||
"Email": "邮箱",
|
||||
"Email (required for verification)": "电子邮件(验证必需)",
|
||||
"Email Address": "电子邮件地址",
|
||||
@@ -1528,8 +1533,8 @@
|
||||
"Exists": "存在",
|
||||
"Expand": "展开",
|
||||
"Expand All": "全部展开",
|
||||
"Expected a JSON array.": "应为 JSON 数组。",
|
||||
"Expected a JSON array of group identifiers": "应为分组标识符的 JSON 数组",
|
||||
"Expected a JSON array.": "应为 JSON 数组。",
|
||||
"Experiment with prompts and models in real time.": "实时实验提示词和模型。",
|
||||
"Expiration Time": "过期时间",
|
||||
"expired": "已过期",
|
||||
@@ -1816,6 +1821,7 @@
|
||||
"Full width": "全宽",
|
||||
"Function calling": "函数调用",
|
||||
"Functions": "函数",
|
||||
"Gateway Load": "网关负载",
|
||||
"GC Count": "GC 次数",
|
||||
"GC executed": "GC 已执行",
|
||||
"GC execution failed": "GC 执行失败",
|
||||
@@ -1924,7 +1930,8 @@
|
||||
"Header Value (supports string or JSON mapping)": "请求头值(支持字符串或 JSON 映射)",
|
||||
"header. Anthropic-formatted endpoints accept the": " 请求头。Anthropic 格式的端点也接受",
|
||||
"Health": "健康",
|
||||
"Healthy": "正常",
|
||||
"Healthy": "健康",
|
||||
"HHHL AI Gateway": "宇宙联邦",
|
||||
"Hidden — verify to reveal": "隐藏 — 验证以显示",
|
||||
"Hide": "隐藏",
|
||||
"Hide API key": "隐藏 API 密钥",
|
||||
@@ -2198,6 +2205,9 @@
|
||||
"List of models supported by this channel. Use comma to separate multiple models.": "此渠道支持的模型列表。使用逗号分隔多个模型。",
|
||||
"List of origins (one per line) allowed for Passkey registration and authentication.": "允许用于 Passkey 注册和身份验证的来源列表(每行一个)。",
|
||||
"List view": "列表视图",
|
||||
"Live capacity telemetry": "实时容量遥测",
|
||||
"Live resource telemetry": "实时资源遥测",
|
||||
"Live Status": "实时状态",
|
||||
"LLM Leaderboard": "LLM 排行榜",
|
||||
"LLM prompt helper": "LLM 辅助设计提示词",
|
||||
"Load Balancing": "负载均衡",
|
||||
@@ -2295,6 +2305,7 @@
|
||||
"Media pricing": "媒体定价",
|
||||
"Median time-to-first-token (TTFT) sampled hourly per group": "按小时采样的各分组首 token 延迟(TTFT)中位数",
|
||||
"Medical Q&A, mental health support": "医疗问答与心理健康支持",
|
||||
"Memory Capacity": "内存容量",
|
||||
"Memory Hits": "内存命中",
|
||||
"Memory Threshold (%)": "内存阈值 (%)",
|
||||
"Merchant ID": "商户 ID",
|
||||
@@ -2646,6 +2657,7 @@
|
||||
"No Users Found": "未找到用户",
|
||||
"No vendor data available": "暂无厂商数据",
|
||||
"No X Found": "未找到 X",
|
||||
"Node": "节点",
|
||||
"Node Name": "节点名称",
|
||||
"Non-stream": "非流式",
|
||||
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "非零邀请奖励需要先在支付网关设置中确认合规条款。",
|
||||
@@ -3193,6 +3205,7 @@
|
||||
"Reasoning Effort": "推理强度",
|
||||
"Receive Upstream Model Update Notifications": "接收上游模型更新通知",
|
||||
"Received": "获得",
|
||||
"Received amount": "已收额度",
|
||||
"Recently launched models": "近期发布的模型",
|
||||
"Recently launched models gaining traction": "近期发布并快速增长的模型",
|
||||
"Recharge": "充值",
|
||||
@@ -3209,7 +3222,6 @@
|
||||
"Redeem codes": "兑换码",
|
||||
"Redeemed By": "兑换人",
|
||||
"Redeemed:": "已兑换:",
|
||||
"Received amount": "已收额度",
|
||||
"redemption code": "兑换码",
|
||||
"Redemption Code": "兑换码",
|
||||
"Redemption code deleted successfully": "兑换码删除成功",
|
||||
@@ -3611,6 +3623,8 @@
|
||||
"Server IP": "服务器 IP",
|
||||
"Server Log Management": "服务器日志管理",
|
||||
"Server logging is not enabled (log directory not configured)": "服务器日志功能未启用(未配置日志目录)",
|
||||
"Server Power Core": "服务器动力核心",
|
||||
"Server Status": "服务器状态",
|
||||
"Server Token": "服务器 Token",
|
||||
"Service account JSON file(s)": "服务账号 JSON 文件",
|
||||
"Session expired!": "会话已过期!",
|
||||
@@ -3828,6 +3842,7 @@
|
||||
"Sync Upstream": "同步上游",
|
||||
"Sync Upstream Models": "同步上游模型",
|
||||
"Synchronize models and vendors from an upstream source": "从上游源同步模型和供应商",
|
||||
"Syncing": "同步中",
|
||||
"Syncing prices, please wait...": "正在同步价格,请稍候...",
|
||||
"Syncing...": "同步中...",
|
||||
"System": "系统",
|
||||
@@ -4108,6 +4123,7 @@
|
||||
"Total requests made": "总请求数",
|
||||
"Total tokens": "总 Token",
|
||||
"Total Tokens": "总 Token 数",
|
||||
"Total Tokens Burned": "总消耗 Tokens",
|
||||
"Total Usage": "总用量",
|
||||
"Total:": "总计:",
|
||||
"TPM": "TPM",
|
||||
@@ -4538,6 +4554,7 @@
|
||||
"Zero retention": "零数据保留",
|
||||
"Zhipu": "智谱",
|
||||
"Zhipu V4": "智谱 V4",
|
||||
"Zoom": "缩放"
|
||||
"Zoom": "缩放",
|
||||
"伟大无需多言": "伟大无需多言"
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -167,6 +167,12 @@ function OAuthCallback() {
|
||||
params: { code: search.code, state: search.state },
|
||||
skipBusinessError: true,
|
||||
}
|
||||
// Add PKCE code_verifier if present in sessionStorage
|
||||
const codeVerifier = sessionStorage.getItem(`pkce_verifier_${search.state}`)
|
||||
if (codeVerifier) {
|
||||
config.params.code_verifier = codeVerifier
|
||||
sessionStorage.removeItem(`pkce_verifier_${search.state}`)
|
||||
}
|
||||
const res = await api.get(`/api/oauth/${provider}`, config)
|
||||
if (res?.data?.success) {
|
||||
const { message } = res.data
|
||||
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
*.local
|
||||
.env.local
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
*.local
|
||||
.vite
|
||||
node_modules/.tmp
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%236366f1'/%3E%3Ctext x='50' y='65' font-size='52' text-anchor='middle' fill='white' font-family='system-ui'%3E🖼%3C/text%3E%3C/svg%3E" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>newapi-image-gen</title>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 antialiased">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2627
File diff suppressed because it is too large
Load Diff
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "newapi-image-gen",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Minimal web image-generation frontend for new-api. Login with your new-api account, generate via /v1/images/generations, billing flows through new-api.",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.43",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Vendored
+139
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import SettingsPanel from './components/SettingsPanel.vue'
|
||||
import ImageGenerator from './components/ImageGenerator.vue'
|
||||
import Gallery from './components/Gallery.vue'
|
||||
import { probeSession, listModels, getSelf, type NewApiUser, type Auth } from './lib/api'
|
||||
import { loadSettings } from './lib/settings'
|
||||
|
||||
const user = ref<NewApiUser | null>(null)
|
||||
const auth = ref<Auth>({ kind: 'session' })
|
||||
const baseUrl = ref<string>(loadSettings().baseUrl)
|
||||
const models = ref<string[]>([])
|
||||
const error = ref<string>('')
|
||||
const settingsRef = ref<InstanceType<typeof SettingsPanel> | null>(null)
|
||||
const galleryRef = ref<InstanceType<typeof Gallery> | null>(null)
|
||||
const checking = ref<boolean>(true)
|
||||
|
||||
async function refreshSession() {
|
||||
checking.value = true
|
||||
try {
|
||||
const u = await probeSession(baseUrl.value)
|
||||
user.value = u
|
||||
error.value = ''
|
||||
if (u) {
|
||||
try {
|
||||
models.value = await listModels(baseUrl.value, { kind: 'session' })
|
||||
} catch (e) {
|
||||
error.value = `已登录,但拉取模型失败: ${e}`
|
||||
}
|
||||
} else {
|
||||
models.value = []
|
||||
}
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(refreshSession)
|
||||
// Re-probe when the tab regains focus (e.g. user came back from /sign-in).
|
||||
// `visibilitychange` covers mobile / tab-switch too; `focus` covers desktop.
|
||||
window.addEventListener('focus', refreshSession)
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') refreshSession()
|
||||
})
|
||||
|
||||
async function onSessionChanged(u: NewApiUser | null) {
|
||||
user.value = u
|
||||
if (u) {
|
||||
try {
|
||||
models.value = await listModels(baseUrl.value, { kind: 'session' })
|
||||
} catch (e) {
|
||||
error.value = `已登录,但拉取模型失败: ${e}`
|
||||
}
|
||||
} else {
|
||||
models.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthOverridden(a: Auth) {
|
||||
auth.value = a
|
||||
if (a.kind === 'sk') {
|
||||
// refresh user info using the new auth
|
||||
getSelf(baseUrl.value, a)
|
||||
.then((u) => {
|
||||
user.value = u
|
||||
})
|
||||
.catch((e) => {
|
||||
error.value = `sk-key 无效: ${e}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onGenerated(img: { prompt: string; model: string; urls: string[] }) {
|
||||
galleryRef.value?.add(img)
|
||||
// refresh quota after a generation (best-effort)
|
||||
if (user.value) {
|
||||
probeSession(baseUrl.value)
|
||||
.then((u) => {
|
||||
if (u) user.value = u
|
||||
})
|
||||
.catch(() => {
|
||||
/* ignore */
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onError(msg: string) {
|
||||
error.value = msg
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-full max-w-5xl mx-auto p-6 space-y-5">
|
||||
<header class="flex items-baseline justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">newapi-image-gen</h1>
|
||||
<p class="text-xs text-zinc-500 mt-0.5">
|
||||
走 new-api 的 <code class="text-zinc-400">/v1/images/generations</code>,额度扣 new-api 账户
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
v-if="user"
|
||||
href="/"
|
||||
class="text-xs text-zinc-500 hover:text-zinc-300"
|
||||
>← 返回 new-api 主页</a>
|
||||
</header>
|
||||
|
||||
<p v-if="error" class="rounded-md border border-rose-800 bg-rose-950/40 px-3 py-2 text-sm text-rose-300">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<SettingsPanel
|
||||
ref="settingsRef"
|
||||
:user="user"
|
||||
@session-changed="onSessionChanged"
|
||||
@auth-overridden="onAuthOverridden"
|
||||
/>
|
||||
|
||||
<div v-if="checking" class="text-sm text-zinc-500">检查登录态…</div>
|
||||
|
||||
<ImageGenerator
|
||||
v-else-if="user"
|
||||
:base-url="baseUrl"
|
||||
:auth="auth"
|
||||
:models="models"
|
||||
@generated="onGenerated"
|
||||
@error="onError"
|
||||
/>
|
||||
<div v-else class="rounded-xl border border-dashed border-zinc-800 p-6 text-center text-sm text-zinc-500">
|
||||
先在上方登录,登录后会自动出现生图面板。
|
||||
</div>
|
||||
|
||||
<Gallery ref="galleryRef" :base-url="baseUrl" />
|
||||
|
||||
<footer class="pt-4 pb-2 text-center text-xs text-zinc-600">
|
||||
登录走 new-api 自己的 OAuth,会话 cookie 跟 new-api 主站共享;本前端不会碰你的密码或 token。
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
|
||||
interface Item {
|
||||
id: string
|
||||
prompt: string
|
||||
model: string
|
||||
urls: string[]
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
const props = defineProps<{ baseUrl: string }>()
|
||||
const items = ref<Item[]>([])
|
||||
|
||||
const STORAGE_KEY = 'newapi-image-gen:gallery:v1'
|
||||
|
||||
function load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
items.value = raw ? (JSON.parse(raw) as Item[]) : []
|
||||
} catch {
|
||||
items.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(items.value.slice(0, 200)))
|
||||
}
|
||||
|
||||
function add(item: Omit<Item, 'id' | 'createdAt'>) {
|
||||
items.value.unshift({
|
||||
...item,
|
||||
id: crypto.randomUUID(),
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
save()
|
||||
}
|
||||
|
||||
defineExpose({ add })
|
||||
|
||||
onMounted(load)
|
||||
|
||||
watch(() => props.baseUrl, () => {
|
||||
// gallery is base-agnostic; nothing to reload
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
|
||||
<div class="flex items-baseline justify-between mb-3">
|
||||
<h2 class="font-semibold text-zinc-200">3. 本地作品集</h2>
|
||||
<span class="text-xs text-zinc-500">本浏览器保留,最多 200 条</span>
|
||||
</div>
|
||||
|
||||
<p v-if="!items.length" class="text-sm text-zinc-500">还没生成过图片。</p>
|
||||
|
||||
<div v-else class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="it in items" :key="it.id" class="space-y-1">
|
||||
<a
|
||||
v-for="(u, i) in it.urls"
|
||||
:key="i"
|
||||
:href="u"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="block aspect-square overflow-hidden rounded-md border border-zinc-800 bg-zinc-950"
|
||||
>
|
||||
<img :src="u" class="w-full h-full object-cover" referrerpolicy="no-referrer" loading="lazy" />
|
||||
</a>
|
||||
<p class="text-xs text-zinc-400 line-clamp-2" :title="it.prompt">{{ it.prompt }}</p>
|
||||
<p class="text-[10px] text-zinc-600">{{ it.model }} · {{ new Date(it.createdAt).toLocaleString() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { generateImage, type GeneratedImage, type Auth } from '../lib/api'
|
||||
|
||||
const props = defineProps<{
|
||||
baseUrl: string
|
||||
auth: Auth
|
||||
models: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'generated', img: { prompt: string; model: string; urls: string[] }): void
|
||||
(e: 'error', msg: string): void
|
||||
}>()
|
||||
|
||||
const prompt = ref<string>('')
|
||||
const negativePrompt = ref<string>('')
|
||||
const model = ref<string>('')
|
||||
const n = ref<number>(1)
|
||||
const size = ref<string>('1024x1024')
|
||||
const running = ref<boolean>(false)
|
||||
const preview = ref<GeneratedImage[]>([])
|
||||
|
||||
watch(
|
||||
() => props.models,
|
||||
(m) => {
|
||||
if (m.length && !model.value) model.value = m[0]
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function go() {
|
||||
if (!prompt.value.trim() || !model.value) return
|
||||
running.value = true
|
||||
preview.value = []
|
||||
try {
|
||||
const imgs = await generateImage(props.baseUrl, props.auth, {
|
||||
model: model.value,
|
||||
prompt: prompt.value,
|
||||
negative_prompt: negativePrompt.value || undefined,
|
||||
n: n.value,
|
||||
size: size.value,
|
||||
response_format: 'url',
|
||||
})
|
||||
preview.value = imgs
|
||||
const urls = imgs.map((i) => i.url).filter((u): u is string => !!u)
|
||||
emit('generated', { prompt: prompt.value, model: model.value, urls })
|
||||
} catch (e) {
|
||||
emit('error', String(e))
|
||||
} finally {
|
||||
running.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 space-y-3">
|
||||
<h2 class="font-semibold text-zinc-200">2. 写提示词生图</h2>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-xs text-zinc-400">正向提示词</span>
|
||||
<textarea
|
||||
v-model="prompt"
|
||||
rows="3"
|
||||
placeholder="a cat astronaut floating in space, ultra-detailed, cinematic lighting"
|
||||
class="mt-1 w-full rounded-md bg-zinc-950 border border-zinc-800 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-xs text-zinc-400">负向提示词(可选,部分模型支持)</span>
|
||||
<input
|
||||
v-model="negativePrompt"
|
||||
placeholder="blurry, low quality, watermark"
|
||||
class="mt-1 w-full rounded-md bg-zinc-950 border border-zinc-800 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<label class="block">
|
||||
<span class="text-xs text-zinc-400">模型</span>
|
||||
<select
|
||||
v-model="model"
|
||||
class="mt-1 w-full rounded-md bg-zinc-950 border border-zinc-800 px-3 py-2 text-sm"
|
||||
>
|
||||
<option v-if="!models.length" disabled>暂无可用模型</option>
|
||||
<option v-for="m in models" :key="m" :value="m">{{ m }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-xs text-zinc-400">张数</span>
|
||||
<input
|
||||
v-model.number="n"
|
||||
type="number"
|
||||
min="1"
|
||||
max="4"
|
||||
class="mt-1 w-full rounded-md bg-zinc-950 border border-zinc-800 px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-xs text-zinc-400">尺寸</span>
|
||||
<select v-model="size" class="mt-1 w-full rounded-md bg-zinc-950 border border-zinc-800 px-3 py-2 text-sm">
|
||||
<option value="256x256">256×256</option>
|
||||
<option value="512x512">512×512</option>
|
||||
<option value="1024x1024">1024×1024</option>
|
||||
<option value="1024x1792">1024×1792</option>
|
||||
<option value="1792x1024">1792×1024</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="go"
|
||||
:disabled="running || !prompt.trim() || !model"
|
||||
class="w-full rounded-md bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 disabled:hover:bg-indigo-600 py-2 text-sm font-medium"
|
||||
>
|
||||
{{ running ? '生成中…' : '生成' }}
|
||||
</button>
|
||||
|
||||
<div v-if="preview.length" class="grid grid-cols-2 gap-2 pt-2">
|
||||
<a
|
||||
v-for="(img, i) in preview"
|
||||
:key="i"
|
||||
:href="img.url || '#'"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="block aspect-square overflow-hidden rounded-md border border-zinc-800 bg-zinc-950"
|
||||
>
|
||||
<img
|
||||
v-if="img.url"
|
||||
:src="img.url"
|
||||
class="w-full h-full object-cover"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { loadSettings, saveSettings, type Settings } from '../lib/settings'
|
||||
import { getSelf, probeSession, type NewApiUser, type Auth } from '../lib/api'
|
||||
|
||||
const props = defineProps<{
|
||||
user: NewApiUser | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'session-changed', user: NewApiUser | null): void
|
||||
(e: 'auth-overridden', auth: Auth): void
|
||||
}>()
|
||||
|
||||
const s = ref<Settings>(loadSettings())
|
||||
const checking = ref<boolean>(false)
|
||||
const skKeyInput = ref<string>('')
|
||||
const usingSk = ref<boolean>(false)
|
||||
const skTestMessage = ref<string>('')
|
||||
const skTestStatus = ref<'idle' | 'ok' | 'fail'>('idle')
|
||||
|
||||
onMounted(() => {
|
||||
// If the URL contains ?session=ok (the /sign-in page can be configured
|
||||
// to bounce back here on success), re-probe immediately.
|
||||
if (window.location.search.includes('session=ok')) {
|
||||
recheck()
|
||||
// strip the param so a refresh doesn't re-trigger
|
||||
const u = new URL(window.location.href)
|
||||
u.searchParams.delete('session')
|
||||
window.history.replaceState({}, '', u.toString())
|
||||
}
|
||||
})
|
||||
|
||||
async function recheck() {
|
||||
checking.value = true
|
||||
try {
|
||||
const u = await probeSession(s.value.baseUrl)
|
||||
emit('session-changed', u)
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goSignIn() {
|
||||
// new-api's /sign-in handles OAuth (LinuxDO / GitHub / Discord / OIDC / 自定义)
|
||||
// and username+password. We just bounce through it; on success the user can
|
||||
// click "返回 newapi-image-gen" or simply navigate back to /image-gen.
|
||||
const returnTo = '/image-gen?session=ok'
|
||||
window.location.href = `/sign-in?redirect=${encodeURIComponent(returnTo)}`
|
||||
}
|
||||
|
||||
async function testSk() {
|
||||
skTestStatus.value = 'idle'
|
||||
skTestMessage.value = ''
|
||||
if (!skKeyInput.value.trim().startsWith('sk-')) {
|
||||
skTestStatus.value = 'fail'
|
||||
skTestMessage.value = 'sk-key 应该以 sk- 开头'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const auth: Auth = { kind: 'sk', token: skKeyInput.value.trim() }
|
||||
const u = await getSelf(s.value.baseUrl, auth)
|
||||
skTestStatus.value = 'ok'
|
||||
skTestMessage.value = `已验证: ${u.username} · 剩余 ${u.quota.toLocaleString()}`
|
||||
emit('auth-overridden', auth)
|
||||
} catch (e) {
|
||||
skTestStatus.value = 'fail'
|
||||
skTestMessage.value = String(e)
|
||||
}
|
||||
}
|
||||
|
||||
function clearSk() {
|
||||
skKeyInput.value = ''
|
||||
skTestStatus.value = 'idle'
|
||||
skTestMessage.value = ''
|
||||
emit('auth-overridden', { kind: 'session' })
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// persist any edits the user made
|
||||
saveSettings(s.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="font-semibold text-zinc-200">1. 登录态</h2>
|
||||
<button
|
||||
@click="recheck"
|
||||
:disabled="checking"
|
||||
class="text-xs text-zinc-500 hover:text-zinc-300 disabled:opacity-50"
|
||||
>{{ checking ? '检查中…' : '刷新状态' }}</button>
|
||||
</div>
|
||||
|
||||
<!-- 登录态展示 -->
|
||||
<div v-if="props.user" class="flex items-center gap-3 text-sm">
|
||||
<span class="inline-flex h-2 w-2 rounded-full bg-emerald-400"></span>
|
||||
<span class="text-zinc-300">{{ props.user.display_name || props.user.username }}</span>
|
||||
<span class="text-xs text-zinc-500">@{{ props.user.username }}</span>
|
||||
<span class="text-xs text-zinc-500">· 剩余额度</span>
|
||||
<span class="text-emerald-400 font-mono">{{ props.user.quota.toLocaleString() }}</span>
|
||||
<span class="text-xs text-zinc-600">· 组: {{ props.user.group }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<span class="inline-flex h-2 w-2 rounded-full bg-zinc-600"></span>
|
||||
<span class="text-zinc-400">未登录</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@click="goSignIn"
|
||||
class="rounded-md bg-indigo-600 hover:bg-indigo-500 px-4 py-1.5 text-sm font-medium"
|
||||
>前往 new-api 登录</button>
|
||||
<a
|
||||
href="/sign-in"
|
||||
class="rounded-md border border-zinc-700 hover:border-zinc-500 px-3 py-1.5 text-sm text-zinc-300"
|
||||
>在新窗口打开登录页</a>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-500">
|
||||
登录会跳到 new-api 自家登录页(OAuth / 用户名密码,看你 new-api 怎么配的),登完回到这里。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 高级:sk-key 兜底 -->
|
||||
<details class="pt-1">
|
||||
<summary class="text-xs text-zinc-500 cursor-pointer select-none hover:text-zinc-300">
|
||||
高级:用 sk-key 登录(不推荐,只在 cookie 不灵时用)
|
||||
</summary>
|
||||
<div class="pt-2 space-y-2">
|
||||
<input
|
||||
v-model="skKeyInput"
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
class="w-full rounded-md bg-zinc-950 border border-zinc-800 px-3 py-2 text-sm font-mono focus:border-indigo-500 focus:outline-none"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="testSk"
|
||||
class="rounded-md border border-zinc-700 hover:border-zinc-500 px-3 py-1 text-xs"
|
||||
>验证</button>
|
||||
<button
|
||||
v-if="usingSk || skTestStatus === 'ok'"
|
||||
@click="clearSk"
|
||||
class="rounded-md border border-zinc-700 hover:border-zinc-500 px-3 py-1 text-xs"
|
||||
>清除,回到 session</button>
|
||||
<span v-if="skTestStatus === 'ok'" class="text-xs text-emerald-400">{{ skTestMessage }}</span>
|
||||
<span v-else-if="skTestStatus === 'fail'" class="text-xs text-rose-400">{{ skTestMessage }}</span>
|
||||
</div>
|
||||
<p class="text-[10px] text-zinc-600">
|
||||
注意:sk-key 不会存到 localStorage,刷新页面就丢。
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
Vendored
+160
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Thin client for new-api. All API paths are RELATIVE because image-gen is
|
||||
* served from the same origin as new-api (embedded in the Go binary at
|
||||
* `/image-gen/`). Cookies are sent automatically with `credentials: 'include'`.
|
||||
*
|
||||
* Auth model after the "同源" integration:
|
||||
* - new-api sets a `session` cookie on login (Path=/, SameSite=Strict,
|
||||
* HttpOnly, 30 days). All /api/* and /v1/* endpoints accept the cookie.
|
||||
* - We do NOT handle the login form ourselves. We just call /api/user/self
|
||||
* to see whether the cookie is present, and if not, redirect the user to
|
||||
* new-api's own /sign-in page.
|
||||
* - As a power-user escape hatch, an `sk-` key still works the same way it
|
||||
* did in v1: paste it in the settings panel, and we add it as
|
||||
* `Authorization: Bearer ...`. Sk-keys are NOT persisted (security).
|
||||
*/
|
||||
|
||||
export type Auth =
|
||||
| { kind: 'session' } // uses the new-api session cookie
|
||||
| { kind: 'sk'; token: string } // explicit sk-key fallback
|
||||
| { kind: 'none' }
|
||||
|
||||
export interface NewApiUser {
|
||||
id: number
|
||||
username: string
|
||||
display_name?: string
|
||||
quota: number
|
||||
used_quota: number
|
||||
group: string
|
||||
role: string
|
||||
status: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface GenerateParams {
|
||||
model: string
|
||||
prompt: string
|
||||
n?: number
|
||||
size?: string
|
||||
response_format?: 'url' | 'b64_json'
|
||||
negative_prompt?: string
|
||||
}
|
||||
|
||||
export interface GeneratedImage {
|
||||
url?: string
|
||||
b64_json?: string
|
||||
revised_prompt?: string
|
||||
}
|
||||
|
||||
export class NewApiError extends Error {
|
||||
status: number
|
||||
body: unknown
|
||||
constructor(message: string, status: number, body: unknown) {
|
||||
super(message)
|
||||
this.status = status
|
||||
this.body = body
|
||||
}
|
||||
}
|
||||
|
||||
function joinUrl(base: string, path: string): string {
|
||||
return base.replace(/\/+$/, '') + (path.startsWith('/') ? path : '/' + path)
|
||||
}
|
||||
|
||||
function authHeader(auth: Auth): Record<string, string> {
|
||||
if (auth.kind === 'sk') return { Authorization: `Bearer ${auth.token}` }
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* `base` should usually be `''` (same-origin) since image-gen is served
|
||||
* from inside new-api. A non-empty base is still allowed for local dev where
|
||||
* you run `npm run dev` on :5174 and proxy /api to :3000.
|
||||
*/
|
||||
async function request<T>(
|
||||
base: string,
|
||||
path: string,
|
||||
init: RequestInit & { auth?: Auth } = {},
|
||||
): Promise<T> {
|
||||
const { auth = { kind: 'session' }, headers, ...rest } = init
|
||||
const useCredentials = auth.kind !== 'sk' // session auth = rely on cookie
|
||||
const res = await fetch(joinUrl(base, path), {
|
||||
...rest,
|
||||
credentials: useCredentials ? 'include' : rest.credentials,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeader(auth),
|
||||
...(headers ?? {}),
|
||||
},
|
||||
})
|
||||
const text = await res.text()
|
||||
let body: unknown
|
||||
try {
|
||||
body = text ? JSON.parse(text) : null
|
||||
} catch {
|
||||
body = text
|
||||
}
|
||||
if (!res.ok) {
|
||||
const msg =
|
||||
(body && typeof body === 'object' && 'message' in body && (body as { message?: string }).message) ||
|
||||
(body && typeof body === 'object' && 'error' in body && (body as { error?: { message?: string } }).error?.message) ||
|
||||
`HTTP ${res.status}`
|
||||
throw new NewApiError(String(msg), res.status, body)
|
||||
}
|
||||
return body as T
|
||||
}
|
||||
|
||||
/** Probe the current session. Returns the user if a valid cookie is present, else throws. */
|
||||
export async function getSelf(base: string, auth: Auth): Promise<NewApiUser> {
|
||||
const r = await request<{ success: boolean; data: NewApiUser; message?: string }>(
|
||||
base,
|
||||
'/api/user/self',
|
||||
{ auth },
|
||||
)
|
||||
if (!r.success) throw new NewApiError(r.message || 'not logged in', 401, r)
|
||||
return r.data
|
||||
}
|
||||
|
||||
/** Best-effort session probe. Returns the user or null. Never throws. */
|
||||
export async function probeSession(base: string): Promise<NewApiUser | null> {
|
||||
try {
|
||||
return await getSelf(base, { kind: 'session' })
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** List models the user can access. */
|
||||
export async function listModels(base: string, auth: Auth): Promise<string[]> {
|
||||
const r = await request<{ data: Array<{ id: string }> } | Array<{ id: string }>>(
|
||||
base,
|
||||
'/v1/models',
|
||||
{ auth },
|
||||
)
|
||||
const arr = Array.isArray(r) ? r : r.data
|
||||
return arr.map((m) => m.id)
|
||||
}
|
||||
|
||||
/** Call /v1/images/generations. */
|
||||
export async function generateImage(
|
||||
base: string,
|
||||
auth: Auth,
|
||||
params: GenerateParams,
|
||||
): Promise<GeneratedImage[]> {
|
||||
const r = await request<{ data: GeneratedImage[]; created: number }>(
|
||||
base,
|
||||
'/v1/images/generations',
|
||||
{
|
||||
method: 'POST',
|
||||
auth,
|
||||
body: JSON.stringify({
|
||||
model: params.model,
|
||||
prompt: params.prompt,
|
||||
n: params.n ?? 1,
|
||||
size: params.size ?? '1024x1024',
|
||||
response_format: params.response_format ?? 'url',
|
||||
...(params.negative_prompt ? { negative_prompt: params.negative_prompt } : {}),
|
||||
}),
|
||||
},
|
||||
)
|
||||
return r.data
|
||||
}
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Settings for image-gen.
|
||||
*
|
||||
* After the "同源" integration (image-gen is served from new-api at /image-gen/),
|
||||
* the only persistent setting is the base URL (mostly empty / same-origin).
|
||||
* The session cookie is managed by the browser; we don't see or store it.
|
||||
*
|
||||
* `skKey` is intentionally NOT persisted: if a power user pastes one, it
|
||||
* lives only in memory for the tab session and is wiped on refresh.
|
||||
*/
|
||||
|
||||
const KEY = 'newapi-image-gen:settings:v2'
|
||||
|
||||
export interface Settings {
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
const DEFAULTS: Settings = {
|
||||
// Empty = same origin (image-gen is at /image-gen/, new-api at /).
|
||||
// The dev override (Vite on :5174) sets this via .env / runtime config.
|
||||
baseUrl: '',
|
||||
}
|
||||
|
||||
export function loadSettings(): Settings {
|
||||
try {
|
||||
const raw = localStorage.getItem(KEY)
|
||||
if (!raw) return { ...DEFAULTS }
|
||||
return { ...DEFAULTS, ...(JSON.parse(raw) as Partial<Settings>) }
|
||||
} catch {
|
||||
return { ...DEFAULTS }
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSettings(s: Settings): void {
|
||||
localStorage.setItem(KEY, JSON.stringify(s))
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "src/**/*.d.ts"]
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"include": ["vite.config.*"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
/**
|
||||
* image-gen is served from inside new-api at `/image-gen/`. In production the
|
||||
* built `dist/` is embedded in the Go binary and served by the same origin
|
||||
* as new-api (so /api/* and /v1/* work via session cookies, no CORS).
|
||||
*
|
||||
* In dev, you typically run `npm run dev` here and the Vite server proxies
|
||||
* `/api`, `/mj`, `/pg` to new-api. We default to http://localhost:3000
|
||||
* (new-api dev backend), but you can override with VITE_NEWAPI_BASE.
|
||||
*/
|
||||
const newapiBase = process.env.VITE_NEWAPI_BASE ?? 'http://localhost:3000'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
// The built assets reference absolute paths under /image-gen/ so the
|
||||
// Go server (which embeds the dist and serves from any /image-gen* URL,
|
||||
// including the no-slash form that gin-contrib/static may redirect to)
|
||||
// can always find them at /image-gen/assets/*.
|
||||
base: '/image-gen/',
|
||||
server: {
|
||||
port: 5174,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': { target: newapiBase, changeOrigin: true },
|
||||
'/v1': { target: newapiBase, changeOrigin: true },
|
||||
'/mj': { target: newapiBase, changeOrigin: true },
|
||||
'/pg': { target: newapiBase, changeOrigin: true },
|
||||
'/assets': { target: newapiBase, changeOrigin: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user