fix: auth buttons, dropdown ring, redesign homepage
Docker Build / Build and Push Docker Image (push) Successful in 4m33s
Docker Build / Build and Push Docker Image (push) Successful in 4m33s
This commit is contained in:
+1
-1
@@ -62,7 +62,7 @@ function DropdownMenuContent({
|
||||
<MenuPrimitive.Popup
|
||||
data-slot='dropdown-menu-content'
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden',
|
||||
'bg-popover text-popover-foreground ring-border data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ function PopoverContent({
|
||||
<PopoverPrimitive.Popup
|
||||
data-slot='popover-content'
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100',
|
||||
'bg-popover text-popover-foreground ring-border data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ function SelectContent({
|
||||
data-slot='select-content'
|
||||
data-align-trigger={alignItemWithTrigger}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none',
|
||||
'bg-popover text-popover-foreground ring-border data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ export function ForgotPasswordForm({
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2'
|
||||
className='mt-2 bg-[#00D2FF] text-[#0A0E14] hover:bg-[#00B8E6]'
|
||||
disabled={isLoading || isActive || !turnstileReady}
|
||||
>
|
||||
{isActive
|
||||
|
||||
@@ -203,7 +203,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) {
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2 w-full'
|
||||
className='mt-2 w-full bg-[#00D2FF] text-[#0A0E14] hover:bg-[#00B8E6]'
|
||||
disabled={!isFormValid || isLoading}
|
||||
>
|
||||
{isLoading ? <Loader2 className='h-4 w-4 animate-spin' /> : null}
|
||||
|
||||
@@ -374,7 +374,7 @@ export function UserAuthForm({
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2 w-full justify-center gap-2'
|
||||
className='mt-2 w-full justify-center gap-2 bg-[#00D2FF] text-[#0A0E14] hover:bg-[#00B8E6]'
|
||||
disabled={isLoading || (requiresLegalConsent && !agreedToLegal)}
|
||||
>
|
||||
{isLoading ? <Loader2 className='animate-spin' /> : <LogIn />}
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export function SignIn() {
|
||||
<AuthLayout>
|
||||
<div className='w-full space-y-8'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-center text-2xl font-semibold tracking-tight sm:text-left'>
|
||||
<h2 className='text-center text-2xl font-semibold tracking-tight text-white sm:text-left'>
|
||||
{t('Sign in')}
|
||||
</h2>
|
||||
{!status?.self_use_mode_enabled &&
|
||||
|
||||
@@ -354,7 +354,7 @@ export function SignUpForm({
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type='submit'
|
||||
className='mt-2 w-full justify-center gap-2'
|
||||
className='mt-2 w-full justify-center gap-2 bg-[#00D2FF] text-[#0A0E14] hover:bg-[#00B8E6]'
|
||||
disabled={
|
||||
isLoading ||
|
||||
(requiresLegalConsent && !agreedToLegal) ||
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ export function SignUp() {
|
||||
<AuthLayout>
|
||||
<div className='w-full space-y-8'>
|
||||
<div className='space-y-2'>
|
||||
<h2 className='text-center text-2xl font-semibold tracking-tight sm:text-left'>
|
||||
<h2 className='text-center text-2xl font-semibold tracking-tight text-white sm:text-left'>
|
||||
{t('Create an account')}
|
||||
</h2>
|
||||
<p className='text-muted-foreground text-left text-sm sm:text-base'>
|
||||
|
||||
+87
-159
@@ -16,7 +16,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact admin@modelstoken.com
|
||||
*/
|
||||
import type React from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { ArrowRight, Zap, Shield, Layers, BarChart3, Key, GitBranch } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -28,7 +27,7 @@ interface HeroProps {
|
||||
isAuthenticated?: boolean
|
||||
}
|
||||
|
||||
/* Termius-dark palette: black bg, cyan accent */
|
||||
/* Palette: black bg, cyan accent, gray layers */
|
||||
const T = {
|
||||
bg: '#0A0E14',
|
||||
surface: '#0F1419',
|
||||
@@ -44,12 +43,12 @@ const T = {
|
||||
}
|
||||
|
||||
const features = [
|
||||
{ icon: Zap, title: 'Multi-model Routing', desc: 'Intelligent routing across providers with automatic failover and load balancing', code: `// Auto failover when provider is down\nrouter.route("gpt-4o", {\n fallback: ["claude-3.5", "gemini-pro"],\n strategy: "latency-first"\n});` },
|
||||
{ icon: Key, title: 'Key Management', desc: 'Centralized API key lifecycle management with usage tracking and rotation', code: `// Rotate keys without downtime\nkeyManager.rotate("sk-prod-xxx", {\n gracePeriod: "24h",\n notify: ["admin@team.io"]\n});` },
|
||||
{ icon: Shield, title: 'Access Control', desc: 'Fine-grained permissions, rate limiting, and token-level access policies', code: `// Per-user rate limits\npolicy.set("user-123", {\n rpm: 60,\n tokensPerDay: 100000,\n allowedModels: ["gpt-4o", "claude-3.5"]\n});` },
|
||||
{ icon: BarChart3, title: 'Real-time Monitoring', desc: 'Live dashboards with request latency, token usage, and cost analytics', code: `// Query usage metrics\nconst metrics = await monitor.query({\n period: "24h",\n groupBy: "model",\n fields: ["latency_p50", "tokens", "cost"]\n});` },
|
||||
{ icon: Layers, title: 'OpenAI Compatible', desc: 'Drop-in replacement for OpenAI API — no code changes required', code: `// Just change the base URL, that's it\nconst openai = new OpenAI({\n baseURL: "https://your-gateway/v1",\n apiKey: "sk-your-key"\n});` },
|
||||
{ icon: GitBranch, title: 'Model Mapping', desc: 'Map model names, override parameters, and customize per-channel behavior', code: `// Map internal names to providers\nmapper.add("fast-chat", {\n provider: "anthropic",\n model: "claude-3.5-haiku",\n params: { max_tokens: 4096 }\n});` },
|
||||
{ icon: Zap, title: 'Multi-model Routing', desc: 'Intelligent routing with automatic failover and load balancing across providers' },
|
||||
{ icon: Key, title: 'Key Management', desc: 'Centralized API key lifecycle management with usage tracking and rotation' },
|
||||
{ icon: Shield, title: 'Access Control', desc: 'Fine-grained permissions, rate limiting, and token-level access policies' },
|
||||
{ icon: BarChart3, title: 'Real-time Monitoring', desc: 'Live dashboards with request latency, token usage, and cost analytics' },
|
||||
{ icon: Layers, title: 'OpenAI Compatible', desc: 'Drop-in replacement for OpenAI API — no code changes required' },
|
||||
{ icon: GitBranch, title: 'Model Mapping', desc: 'Map model names, override parameters, and customize per-channel behavior' },
|
||||
]
|
||||
|
||||
export function Hero(props: HeroProps) {
|
||||
@@ -67,28 +66,38 @@ export function Hero(props: HeroProps) {
|
||||
className='pointer-events-none absolute inset-0'
|
||||
style={{
|
||||
background:
|
||||
'radial-gradient(ellipse 60% 35% at 50% -5%, rgba(0,210,255,0.06) 0%, transparent 70%)',
|
||||
'radial-gradient(ellipse 50% 30% at 50% 0%, rgba(0,210,255,0.07) 0%, transparent 70%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className='relative mx-auto max-w-6xl px-6'>
|
||||
<div className='relative mx-auto max-w-5xl px-6'>
|
||||
{/* ── Hero ── */}
|
||||
<div className='flex min-h-[calc(100svh-3rem)] flex-col items-center justify-center py-20 text-center'>
|
||||
{/* Eyebrow */}
|
||||
<div className='mb-8 inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs' style={{ borderColor: T.border, backgroundColor: T.surface, color: T.grayLight }}>
|
||||
<span className='inline-block size-1.5 rounded-full' style={{ backgroundColor: T.accent }} />
|
||||
{t('Compatible with OpenAI API')}
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<h1
|
||||
className='max-w-3xl text-[clamp(2rem,5.5vw,3.5rem)] leading-[1.1] font-bold tracking-[-0.025em]'
|
||||
style={{ color: T.accent }}
|
||||
className='max-w-2xl text-[clamp(2.2rem,6vw,4rem)] leading-[1.05] font-bold tracking-[-0.03em]'
|
||||
style={{ color: T.white }}
|
||||
>
|
||||
{t('Unified LLM Gateway')}
|
||||
{t('Unified LLM')}{' '}
|
||||
<span style={{ color: T.accent }}>{t('Gateway')}</span>
|
||||
</h1>
|
||||
|
||||
<p className='mt-5 max-w-lg text-base leading-relaxed sm:text-lg' style={{ color: T.grayLight }}>
|
||||
{/* Subtitle */}
|
||||
<p className='mt-6 max-w-md text-base leading-relaxed' style={{ color: T.grayLight }}>
|
||||
{t('One endpoint for all models. OpenAI-compatible, switch and go.')}
|
||||
</p>
|
||||
|
||||
<div className='mt-8 flex flex-wrap items-center justify-center gap-3'>
|
||||
{/* CTA */}
|
||||
<div className='mt-10 flex flex-wrap items-center justify-center gap-3'>
|
||||
{props.isAuthenticated ? (
|
||||
<Button
|
||||
className='group h-11 rounded-lg px-6 text-sm font-semibold hover:opacity-90'
|
||||
className='group h-11 rounded-lg px-7 text-sm font-semibold hover:opacity-90'
|
||||
style={{ backgroundColor: T.accent, color: T.bg }}
|
||||
render={<Link to='/dashboard' />}
|
||||
>
|
||||
@@ -98,7 +107,7 @@ export function Hero(props: HeroProps) {
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
className='group h-11 rounded-lg px-6 text-sm font-semibold hover:opacity-90'
|
||||
className='group h-11 rounded-lg px-7 text-sm font-semibold hover:opacity-90'
|
||||
style={{ backgroundColor: T.accent, color: T.bg }}
|
||||
render={<Link to='/sign-up' />}
|
||||
>
|
||||
@@ -107,7 +116,7 @@ export function Hero(props: HeroProps) {
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-11 rounded-lg px-6 text-sm font-medium hover:opacity-90'
|
||||
className='h-11 rounded-lg px-7 text-sm font-medium hover:opacity-90'
|
||||
style={{ borderColor: T.border, backgroundColor: 'transparent', color: T.white }}
|
||||
render={<Link to='/pricing' />}
|
||||
>
|
||||
@@ -116,45 +125,48 @@ export function Hero(props: HeroProps) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Terminal demo */}
|
||||
<div className='mt-16 w-full max-w-2xl'>
|
||||
<div
|
||||
className='overflow-hidden rounded-lg border shadow-[0_8px_40px_-12px_rgba(0,0,0,0.6)]'
|
||||
style={{ borderColor: T.border, backgroundColor: T.surface }}
|
||||
>
|
||||
<div className='flex items-center gap-2 border-b px-4 py-3' style={{ borderColor: T.border }}>
|
||||
<div className='flex gap-1.5'>
|
||||
<div className='size-[9px] rounded-full' style={{ backgroundColor: T.red }} />
|
||||
<div className='size-[9px] rounded-full' style={{ backgroundColor: T.yellow }} />
|
||||
<div className='size-[9px] rounded-full' style={{ backgroundColor: T.green }} />
|
||||
</div>
|
||||
<span className='ml-1 text-[11px] font-medium' style={{ color: T.gray }}>
|
||||
bash
|
||||
</span>
|
||||
{/* ── Terminal demo ── */}
|
||||
<div className='-mt-8 pb-20'>
|
||||
<div
|
||||
className='mx-auto w-full max-w-2xl overflow-hidden rounded-xl border shadow-[0_16px_64px_-16px_rgba(0,0,0,0.5)]'
|
||||
style={{ borderColor: T.border, backgroundColor: T.surface }}
|
||||
>
|
||||
{/* Terminal header */}
|
||||
<div className='flex items-center gap-2 border-b px-4 py-2.5' style={{ borderColor: T.border }}>
|
||||
<div className='flex gap-1.5'>
|
||||
<div className='size-2.5 rounded-full' style={{ backgroundColor: T.red }} />
|
||||
<div className='size-2.5 rounded-full' style={{ backgroundColor: T.yellow }} />
|
||||
<div className='size-2.5 rounded-full' style={{ backgroundColor: T.green }} />
|
||||
</div>
|
||||
<div className='p-5 font-mono text-[13px] leading-[1.8]'>
|
||||
<div>
|
||||
<span style={{ color: T.green }}>$</span>{' '}
|
||||
<span style={{ color: T.white }}>curl</span>{' '}
|
||||
<span style={{ color: T.accent }}>{serverAddress}/v1/chat/completions</span>{' '}
|
||||
<span style={{ color: T.gray }}>\</span>
|
||||
</div>
|
||||
<div className='pl-4'>
|
||||
<span style={{ color: T.gray }}>-H</span>{' '}
|
||||
<span style={{ color: T.yellow }}>"Authorization: Bearer sk-..."</span>{' '}
|
||||
<span style={{ color: T.gray }}>\</span>
|
||||
</div>
|
||||
<div className='pl-4'>
|
||||
<span style={{ color: T.gray }}>-d</span>{' '}
|
||||
<span style={{ color: T.yellow }}>{"'{ \"model\": \"gpt-4o\", \"messages\": [...] }'"}</span>
|
||||
</div>
|
||||
<div className='mt-4 flex items-center gap-2 border-t pt-4' style={{ borderColor: T.border }}>
|
||||
<span className='ml-2 text-[11px]' style={{ color: T.gray }}>bash</span>
|
||||
</div>
|
||||
{/* Terminal content */}
|
||||
<div className='p-5 font-mono text-[13px] leading-[1.9]'>
|
||||
<div>
|
||||
<span style={{ color: T.accent }}>$</span>{' '}
|
||||
<span style={{ color: T.white }}>curl</span>{' '}
|
||||
<span style={{ color: T.accent }}>{serverAddress}/v1/chat/completions</span>{' '}
|
||||
<span style={{ color: T.gray }}>\</span>
|
||||
</div>
|
||||
<div className='pl-5'>
|
||||
<span style={{ color: T.gray }}>-H</span>{' '}
|
||||
<span style={{ color: T.yellow }}>"Authorization: Bearer sk-..."</span>{' '}
|
||||
<span style={{ color: T.gray }}>\</span>
|
||||
</div>
|
||||
<div className='pl-5'>
|
||||
<span style={{ color: T.gray }}>-d</span>{' '}
|
||||
<span style={{ color: T.yellow }}>{"'{ \"model\": \"gpt-4o\", \"messages\": [...] }'"}</span>
|
||||
</div>
|
||||
|
||||
<div className='mt-5 border-t pt-4' style={{ borderColor: T.border }}>
|
||||
<div className='flex items-center gap-2 mb-3'>
|
||||
<span className='inline-block size-1.5 rounded-full' style={{ backgroundColor: T.green }} />
|
||||
<span className='text-[11px] font-medium' style={{ color: T.gray }}>200 OK</span>
|
||||
<span className='text-[11px]' style={{ color: T.gray }}>-- 312ms</span>
|
||||
<span className='text-[11px]' style={{ color: T.gray }}>312ms</span>
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<div>
|
||||
<span style={{ color: T.gray }}>{"{"}</span>{' '}
|
||||
<span style={{ color: T.accent }}>"model"</span>
|
||||
<span style={{ color: T.gray }}>:</span>{' '}
|
||||
@@ -162,68 +174,59 @@ export function Hero(props: HeroProps) {
|
||||
<span style={{ color: T.gray }}>,</span>{' '}
|
||||
<span style={{ color: T.accent }}>"usage"</span>
|
||||
<span style={{ color: T.gray }}>:</span>{' '}
|
||||
<span style={{ color: T.gray }}>{"{...}"}</span>
|
||||
<span style={{ color: T.gray }}>{"{ \"prompt_tokens\": 12, \"completion_tokens\": 47 }"}</span>
|
||||
<span style={{ color: T.gray }}>{"}"}</span>
|
||||
</div>
|
||||
<div className='mt-3'>
|
||||
<span style={{ color: T.green }}>$</span>
|
||||
<span className='ml-1 inline-block h-3.5 w-[7px] animate-pulse align-middle' style={{ backgroundColor: T.green }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3'>
|
||||
<span style={{ color: T.accent }}>$</span>
|
||||
<span className='ml-1 inline-block h-3.5 w-[7px] animate-pulse align-middle' style={{ backgroundColor: T.accent }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Features ── */}
|
||||
<div className='pb-24'>
|
||||
<div className='mb-16 text-center'>
|
||||
<h2 className='text-xl font-semibold tracking-tight sm:text-2xl' style={{ color: T.white }}>
|
||||
<div className='pb-28'>
|
||||
<div className='mb-14 text-center'>
|
||||
<h2 className='text-2xl font-semibold tracking-tight' style={{ color: T.white }}>
|
||||
{t('Full-featured gateway')}
|
||||
</h2>
|
||||
<p className='mt-2 text-sm' style={{ color: T.grayLight }}>
|
||||
<p className='mt-3 text-sm' style={{ color: T.grayLight }}>
|
||||
{t('Everything you need to manage, route, and monitor LLM API traffic')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='space-y-6'>
|
||||
{features.map((feature, i) => (
|
||||
<div className='grid gap-5 sm:grid-cols-2 lg:grid-cols-3'>
|
||||
{features.map((feature) => (
|
||||
<div
|
||||
key={feature.title}
|
||||
className='grid items-center gap-6 rounded-lg border p-6 transition-colors duration-200 lg:grid-cols-2 lg:gap-10'
|
||||
className='group rounded-xl border p-6 transition-all duration-200'
|
||||
style={{
|
||||
borderColor: T.border,
|
||||
backgroundColor: T.surface,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = T.accentDim
|
||||
e.currentTarget.style.transform = 'translateY(-2px)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = T.border
|
||||
e.currentTarget.style.transform = 'translateY(0)'
|
||||
}}
|
||||
>
|
||||
{/* Text side */}
|
||||
<div className={i % 2 === 1 ? 'lg:order-2' : ''}>
|
||||
<div
|
||||
className='mb-3 flex size-9 items-center justify-center rounded-md'
|
||||
style={{ backgroundColor: `${T.accent}15` }}
|
||||
>
|
||||
<feature.icon className='size-[18px]' style={{ color: T.accent }} />
|
||||
</div>
|
||||
<h3 className='text-base font-semibold' style={{ color: T.white }}>
|
||||
{t(feature.title)}
|
||||
</h3>
|
||||
<p className='mt-1.5 text-sm leading-relaxed' style={{ color: T.grayLight }}>
|
||||
{t(feature.desc)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Code side */}
|
||||
<div
|
||||
className={`overflow-hidden rounded-md border p-4 font-mono text-[12px] leading-[1.7] ${i % 2 === 1 ? 'lg:order-1' : ''}`}
|
||||
style={{ borderColor: T.border, backgroundColor: T.bg }}
|
||||
className='mb-4 flex size-10 items-center justify-center rounded-lg'
|
||||
style={{ backgroundColor: `${T.accent}12` }}
|
||||
>
|
||||
<CodeBlock code={feature.code} />
|
||||
<feature.icon className='size-5' style={{ color: T.accent }} />
|
||||
</div>
|
||||
<h3 className='text-sm font-semibold' style={{ color: T.white }}>
|
||||
{t(feature.title)}
|
||||
</h3>
|
||||
<p className='mt-2 text-[13px] leading-relaxed' style={{ color: T.gray }}>
|
||||
{t(feature.desc)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -232,78 +235,3 @@ export function Hero(props: HeroProps) {
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/** Simple syntax-highlighted code block */
|
||||
function CodeBlock({ code }: { code: string }) {
|
||||
const keywords = new Set(['const', 'let', 'var', 'function', 'return', 'new', 'await', 'async', 'import', 'from', 'export', 'default'])
|
||||
const strings = /"([^"]*)"|'([^']*)'/
|
||||
|
||||
return (
|
||||
<>
|
||||
{code.split('\n').map((line, i) => (
|
||||
<div key={i}>
|
||||
{tokenize(line, keywords, strings)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function tokenize(line: string, keywords: Set<string>, _stringRe: RegExp) {
|
||||
// Comment line
|
||||
if (line.trimStart().startsWith('//')) {
|
||||
return <span style={{ color: T.gray }}>{line}</span>
|
||||
}
|
||||
|
||||
// Split into: strings | keywords | punctuation | rest
|
||||
const tokens: React.ReactNode[] = []
|
||||
let remaining = line
|
||||
let key = 0
|
||||
|
||||
while (remaining.length > 0) {
|
||||
// Try string match
|
||||
const strMatch = remaining.match(/^"([^"]*)"|^'([^']*)'/)
|
||||
if (strMatch) {
|
||||
const full = strMatch[0]
|
||||
tokens.push(<span key={key++} style={{ color: T.yellow }}>{full}</span>)
|
||||
remaining = remaining.slice(full.length)
|
||||
continue
|
||||
}
|
||||
|
||||
// Try word
|
||||
const wordMatch = remaining.match(/^([a-zA-Z_$][\w$]*)/)
|
||||
if (wordMatch) {
|
||||
const word = wordMatch[1]
|
||||
if (keywords.has(word)) {
|
||||
tokens.push(<span key={key++} style={{ color: T.accent }}>{word}</span>)
|
||||
} else if (/^[A-Z]/.test(word)) {
|
||||
tokens.push(<span key={key++} style={{ color: T.accent }}>{word}</span>)
|
||||
} else {
|
||||
tokens.push(<span key={key++} style={{ color: T.white }}>{word}</span>)
|
||||
}
|
||||
remaining = remaining.slice(word.length)
|
||||
continue
|
||||
}
|
||||
|
||||
// Try number
|
||||
const numMatch = remaining.match(/^\d+/)
|
||||
if (numMatch) {
|
||||
tokens.push(<span key={key++} style={{ color: T.yellow }}>{numMatch[0]}</span>)
|
||||
remaining = remaining.slice(numMatch[0].length)
|
||||
continue
|
||||
}
|
||||
|
||||
// Punctuation / operators
|
||||
const ch = remaining[0]
|
||||
if ('(){}[].:;,'.includes(ch)) {
|
||||
tokens.push(<span key={key++} style={{ color: T.grayLight }}>{ch}</span>)
|
||||
} else if ('=<>!+-*/%&|?'.includes(ch)) {
|
||||
tokens.push(<span key={key++} style={{ color: T.accent }}>{ch}</span>)
|
||||
} else {
|
||||
tokens.push(<span key={key++} style={{ color: T.white }}>{ch}</span>)
|
||||
}
|
||||
remaining = remaining.slice(1)
|
||||
}
|
||||
|
||||
return <>{tokens}</>
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -959,6 +959,8 @@
|
||||
"Route requests across OpenAI, Claude, Gemini, and 30+ providers through a single endpoint.": "Route requests across OpenAI, Claude, Gemini, and 30+ providers through a single endpoint.",
|
||||
"Track usage, costs, and performance with live analytics dashboards.": "Track usage, costs, and performance with live analytics dashboards.",
|
||||
"Unified LLM Gateway": "Unified LLM Gateway",
|
||||
"Unified LLM": "Unified LLM",
|
||||
"Gateway": "Gateway",
|
||||
"Copied {{count}} key(s)": "Copied {{count}} key(s)",
|
||||
"Copied to clipboard": "Copied to clipboard",
|
||||
"Copied: {{model}}": "Copied: {{model}}",
|
||||
|
||||
Vendored
+2
@@ -959,6 +959,8 @@
|
||||
"Route requests across OpenAI, Claude, Gemini, and 30+ providers through a single endpoint.": "通过单一端点将请求路由至 OpenAI、Claude、Gemini 等 30+ 供应商。",
|
||||
"Track usage, costs, and performance with live analytics dashboards.": "通过实时分析仪表板追踪用量、成本和性能。",
|
||||
"Unified LLM Gateway": "统一大模型网关",
|
||||
"Unified LLM": "统一大模型",
|
||||
"Gateway": "网关",
|
||||
"Copied {{count}} key(s)": "已复制 {{count}} 个密钥",
|
||||
"Copied to clipboard": "已复制到剪贴板",
|
||||
"Copied: {{model}}": "已复制: {{model}}",
|
||||
|
||||
Reference in New Issue
Block a user