Create standalone browser port of LTX Athena with authenticated native API transport

This commit is contained in:
Mikei386
2026-09-29 17:59:23 +02:00
commit 1333b0c36b
202 changed files with 63054 additions and 0 deletions
@@ -0,0 +1,50 @@
import { Heart, Image, Video } from 'lucide-react'
import type { GenSpaceTypeFilter } from '../../lib/genspace-gallery'
export function GenSpaceFilterEmptyState({
typeFilter,
showFavorites,
hasTypeMatches,
}: {
typeFilter: GenSpaceTypeFilter
showFavorites: boolean
hasTypeMatches: boolean
}) {
if (typeFilter !== 'all' && !hasTypeMatches) {
if (typeFilter === 'video') {
return (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center pointer-events-none">
<Video className="h-12 w-12 text-zinc-700 mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">No videos yet</h3>
<p className="text-zinc-500 text-sm">
Generate a video or switch the filter to see other media.
</p>
</div>
)
}
return (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center pointer-events-none">
<Image className="h-12 w-12 text-zinc-700 mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">No images yet</h3>
<p className="text-zinc-500 text-sm">
Generate an image or switch the filter to see other media.
</p>
</div>
)
}
if (showFavorites) {
return (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center pointer-events-none">
<Heart className="h-12 w-12 text-zinc-700 mb-4" />
<h3 className="text-lg font-semibold text-white mb-2">No favorites yet</h3>
<p className="text-zinc-500 text-sm">
Click the heart icon on any asset to add it to your favorites.
</p>
</div>
)
}
return null
}
@@ -0,0 +1,130 @@
import { useEffect, useRef, useState } from 'react'
export type GallerySize = 'small' | 'medium' | 'large'
export const gallerySizeClasses: Record<GallerySize, string> = {
small: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7',
medium: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5',
large: 'grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3',
}
function GridSmallIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="2" width="4" height="4" rx="0.5" />
<rect x="8" y="2" width="4" height="4" rx="0.5" />
<rect x="14" y="2" width="4" height="4" rx="0.5" />
<rect x="20" y="2" width="2" height="4" rx="0.5" />
<rect x="2" y="8" width="4" height="4" rx="0.5" />
<rect x="8" y="8" width="4" height="4" rx="0.5" />
<rect x="14" y="8" width="4" height="4" rx="0.5" />
<rect x="20" y="8" width="2" height="4" rx="0.5" />
<rect x="2" y="14" width="4" height="4" rx="0.5" />
<rect x="8" y="14" width="4" height="4" rx="0.5" />
<rect x="14" y="14" width="4" height="4" rx="0.5" />
<rect x="20" y="14" width="2" height="4" rx="0.5" />
</svg>
)
}
function GridMediumIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="2" width="6" height="6" rx="1" />
<rect x="10" y="2" width="6" height="6" rx="1" />
<rect x="18" y="2" width="4" height="6" rx="1" />
<rect x="2" y="10" width="6" height="6" rx="1" />
<rect x="10" y="10" width="6" height="6" rx="1" />
<rect x="18" y="10" width="4" height="6" rx="1" />
<rect x="2" y="18" width="6" height="4" rx="1" />
<rect x="10" y="18" width="6" height="4" rx="1" />
<rect x="18" y="18" width="4" height="4" rx="1" />
</svg>
)
}
function GridLargeIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<rect x="2" y="2" width="9" height="9" rx="1.5" />
<rect x="13" y="2" width="9" height="9" rx="1.5" />
<rect x="2" y="13" width="9" height="9" rx="1.5" />
<rect x="13" y="13" width="9" height="9" rx="1.5" />
</svg>
)
}
export function GenSpaceGallerySizeMenu({
gallerySize,
onGallerySizeChange,
}: {
gallerySize: GallerySize
onGallerySizeChange: (size: GallerySize) => void
}) {
const [open, setOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setOpen(false)
}
}
if (open) {
document.addEventListener('mousedown', handleClickOutside)
}
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [open])
const CurrentIcon = gallerySize === 'small'
? GridSmallIcon
: gallerySize === 'medium'
? GridMediumIcon
: GridLargeIcon
return (
<div ref={menuRef} className="relative">
<button
type="button"
onClick={() => setOpen(value => !value)}
aria-label={`Gallery size: ${gallerySize}`}
aria-haspopup="menu"
aria-expanded={open}
className={`p-2 rounded-md transition-colors ${
open ? 'bg-zinc-800 text-white' : 'text-zinc-400 hover:text-white hover:bg-zinc-800'
}`}
>
<CurrentIcon className="h-4 w-4" />
</button>
{open && (
<div className="absolute top-full mt-2 right-0 bg-zinc-800 border border-zinc-700 rounded-md p-2 min-w-[160px] shadow-xl z-50">
{([
{ value: 'small' as const, label: 'Small', icon: GridSmallIcon },
{ value: 'medium' as const, label: 'Medium', icon: GridMediumIcon },
{ value: 'large' as const, label: 'Large', icon: GridLargeIcon },
]).map(option => (
<button
key={option.value}
type="button"
onClick={() => { onGallerySizeChange(option.value); setOpen(false) }}
className={`w-full flex items-center justify-between px-2 py-2.5 rounded-md transition-colors text-left ${gallerySize === option.value ? 'bg-white/20 hover:bg-white/25' : 'hover:bg-zinc-700'}`}
>
<div className="flex items-center gap-3">
<option.icon className={`h-4 w-4 ${gallerySize === option.value ? 'text-white' : 'text-zinc-500'}`} />
<span className={`text-sm ${gallerySize === option.value ? 'text-white font-medium' : 'text-zinc-400'}`}>
{option.label}
</span>
</div>
{gallerySize === option.value && (
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</button>
))}
</div>
)}
</div>
)
}
@@ -0,0 +1,83 @@
import { Heart, Sparkles } from 'lucide-react'
import type { GenSpaceSortDir, GenSpaceSortKey, GenSpaceTypeFilter } from '../../lib/genspace-gallery'
import { GenSpaceGallerySizeMenu, type GallerySize } from './GenSpaceGallerySizeMenu'
import { GenSpaceSortMenu } from './GenSpaceSortMenu'
import { GenSpaceTypeFilter as TypeFilter } from './GenSpaceTypeFilter'
export function GenSpaceGalleryToolbar({
showBrowseLoras,
onBrowseLoras,
typeFilter,
onTypeFilterChange,
sortKey,
sortDir,
onSortKeyChange,
onToggleSortDir,
showFavorites,
favoriteCount,
onToggleFavorites,
gallerySize,
onGallerySizeChange,
}: {
showBrowseLoras: boolean
onBrowseLoras: () => void
typeFilter: GenSpaceTypeFilter
onTypeFilterChange: (value: GenSpaceTypeFilter) => void
sortKey: GenSpaceSortKey
sortDir: GenSpaceSortDir
onSortKeyChange: (key: GenSpaceSortKey) => void
onToggleSortDir: () => void
showFavorites: boolean
favoriteCount: number
onToggleFavorites: () => void
gallerySize: GallerySize
onGallerySizeChange: (size: GallerySize) => void
}) {
return (
<div className="flex items-center justify-between pb-2 gap-2">
<div className="flex items-center gap-2">
{showBrowseLoras && (
<button
type="button"
onClick={onBrowseLoras}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors"
>
<Sparkles className="h-4 w-4" /> Browse LoRAs
</button>
)}
</div>
<div className="flex items-center gap-2">
<TypeFilter value={typeFilter} onChange={onTypeFilterChange} />
<GenSpaceSortMenu
sortKey={sortKey}
sortDir={sortDir}
onSortKeyChange={onSortKeyChange}
onToggleSortDir={onToggleSortDir}
/>
<button
type="button"
onClick={onToggleFavorites}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
showFavorites
? 'bg-red-500/20 text-red-400 border border-red-500/30'
: 'text-zinc-400 hover:text-white hover:bg-zinc-800'
}`}
>
<Heart className={`h-4 w-4 ${showFavorites ? 'fill-current' : ''}`} />
Favorites
{favoriteCount > 0 && (
<span className={`text-xs px-1.5 py-0.5 rounded-full ${
showFavorites ? 'bg-red-500/30 text-red-300' : 'bg-zinc-800 text-zinc-500'
}`}>
{favoriteCount}
</span>
)}
</button>
<GenSpaceGallerySizeMenu
gallerySize={gallerySize}
onGallerySizeChange={onGallerySizeChange}
/>
</div>
</div>
)
}
@@ -0,0 +1,53 @@
import { ChevronDown, ChevronUp } from 'lucide-react'
import { SettingsDropdown } from '../../components/SettingsDropdown'
import {
GENSPACE_SORT_OPTIONS,
type GenSpaceSortDir,
type GenSpaceSortKey,
} from '../../lib/genspace-gallery'
const toolbarTriggerClass = 'px-3 py-1.5 text-sm font-medium text-zinc-400 hover:text-white rounded-r-none'
export function GenSpaceSortMenu({
sortKey,
sortDir,
onSortKeyChange,
onToggleSortDir,
}: {
sortKey: GenSpaceSortKey
sortDir: GenSpaceSortDir
onSortKeyChange: (key: GenSpaceSortKey) => void
onToggleSortDir: () => void
}) {
const currentLabel = GENSPACE_SORT_OPTIONS.find(option => option.value === sortKey)?.label ?? 'Date'
const DirectionIcon = sortDir === 'desc' ? ChevronDown : ChevronUp
const directionLabel = sortDir === 'desc' ? 'Descending' : 'Ascending'
return (
<div className="flex items-center">
<SettingsDropdown
placement="below"
title="Sort"
value={sortKey}
onChange={next => onSortKeyChange(next as GenSpaceSortKey)}
triggerClassName={toolbarTriggerClass}
trigger={
<>
<span>{currentLabel}</span>
<ChevronDown className="h-3.5 w-3.5 text-zinc-500" />
</>
}
options={GENSPACE_SORT_OPTIONS}
/>
<button
type="button"
onClick={onToggleSortDir}
title={directionLabel}
aria-label={`Sort ${directionLabel.toLowerCase()}`}
className="p-1.5 rounded-md rounded-l-none border-l border-zinc-700 text-zinc-400 hover:text-white hover:bg-zinc-800 transition-colors"
>
<DirectionIcon className="h-3.5 w-3.5" />
</button>
</div>
)
}
@@ -0,0 +1,35 @@
import { ChevronDown } from 'lucide-react'
import { SettingsDropdown } from '../../components/SettingsDropdown'
import {
GENSPACE_TYPE_FILTER_OPTIONS,
type GenSpaceTypeFilter,
} from '../../lib/genspace-gallery'
const toolbarTriggerClass = 'px-3 py-1.5 text-sm font-medium text-zinc-400 hover:text-white'
export function GenSpaceTypeFilter({
value,
onChange,
}: {
value: GenSpaceTypeFilter
onChange: (value: GenSpaceTypeFilter) => void
}) {
const currentLabel = GENSPACE_TYPE_FILTER_OPTIONS.find(option => option.value === value)?.label ?? 'All'
return (
<SettingsDropdown
placement="below"
title="Type"
value={value}
onChange={next => onChange(next as GenSpaceTypeFilter)}
triggerClassName={toolbarTriggerClass}
trigger={
<>
<span>{currentLabel}</span>
<ChevronDown className="h-3.5 w-3.5 text-zinc-500" />
</>
}
options={GENSPACE_TYPE_FILTER_OPTIONS}
/>
)
}
@@ -0,0 +1,67 @@
import { useCallback, useMemo, useState } from 'react'
import type { Asset } from '../../types/project-model'
import {
defaultSortDir,
filterGenSpaceAssets,
sortGenSpaceAssets,
type GenSpaceSortDir,
type GenSpaceSortKey,
type GenSpaceTypeFilter,
} from '../../lib/genspace-gallery'
export function useGenSpaceGallery(assets: Asset[]) {
const [typeFilter, setTypeFilter] = useState<GenSpaceTypeFilter>('all')
const [sortKey, setSortKey] = useState<GenSpaceSortKey>('createdAt')
const [sortDir, setSortDir] = useState<GenSpaceSortDir>(() => defaultSortDir('createdAt'))
const [showFavorites, setShowFavorites] = useState(false)
const filteredAssets = useMemo(
() => sortGenSpaceAssets(
filterGenSpaceAssets(assets, typeFilter, showFavorites),
sortKey,
sortDir,
),
[assets, typeFilter, showFavorites, sortKey, sortDir],
)
const favoriteCount = useMemo(
() => filterGenSpaceAssets(assets, 'all', true).length,
[assets],
)
const hasTypeMatches = useMemo(
() => filterGenSpaceAssets(assets, typeFilter, false).length > 0,
[assets, typeFilter],
)
const onSortKeyChange = useCallback((key: GenSpaceSortKey) => {
if (key === sortKey) {
setSortDir(direction => (direction === 'asc' ? 'desc' : 'asc'))
return
}
setSortKey(key)
setSortDir(defaultSortDir(key))
}, [sortKey])
const onToggleSortDir = useCallback(() => {
setSortDir(direction => (direction === 'asc' ? 'desc' : 'asc'))
}, [])
const onToggleFavorites = useCallback(() => {
setShowFavorites(value => !value)
}, [])
return {
typeFilter,
setTypeFilter,
sortKey,
sortDir,
onSortKeyChange,
onToggleSortDir,
showFavorites,
onToggleFavorites,
filteredAssets,
favoriteCount,
hasTypeMatches,
}
}
@@ -0,0 +1,40 @@
import { useCallback, useRef, useState } from 'react'
import type { PanelImperativeHandle } from 'react-resizable-panels'
import {
DEFAULT_PROMPT_BAR_HEIGHT,
PROMPT_BAR_HEIGHT_LIMITS,
clampPromptBarHeight,
loadPromptBarHeight,
savePromptBarHeight,
} from '../../lib/genspace-layout'
export function useGenSpacePromptBarHeight() {
const initialHeightRef = useRef(loadPromptBarHeight())
const heightRef = useRef(initialHeightRef.current)
const panelRef = useRef<PanelImperativeHandle | null>(null)
const [promptBarHeight, setPromptBarHeight] = useState(initialHeightRef.current)
const persistHeight = useCallback((inPixels: number) => {
const next = clampPromptBarHeight(inPixels)
if (next === heightRef.current) return
heightRef.current = next
setPromptBarHeight(next)
savePromptBarHeight(next)
}, [])
const resetHeight = useCallback(() => {
heightRef.current = DEFAULT_PROMPT_BAR_HEIGHT
setPromptBarHeight(DEFAULT_PROMPT_BAR_HEIGHT)
savePromptBarHeight(DEFAULT_PROMPT_BAR_HEIGHT)
panelRef.current?.resize(DEFAULT_PROMPT_BAR_HEIGHT)
}, [])
return {
initialHeight: initialHeightRef.current,
promptBarHeight,
panelRef,
persistHeight,
resetHeight,
limits: PROMPT_BAR_HEIGHT_LIMITS,
}
}