iPhone is restrictive. Syncing from Desktop is recommended.
-
Alternative: Use "Alook Browser" app (Paid) which acts like a desktop browser with developer tools.
+
+
iPhone is restrictive. Syncing from Desktop is recommended.
+
+ Alternative: Use "Alook Browser" app (Paid) which acts like a desktop browser with developer tools.
-
- Use Method 1 (Desktop Sync) - it's much faster.
- Or access this site on your Mac/PC and configure it there first.Settings are saved to the browser, so this only works if you sync local storage or use the same device.
+
+ {[
+ { step: 1, text: <>Use Method 1 (Desktop Sync) - it's much faster.> },
+ { step: 2, text: <>Or access this site on your Mac/PC and configure it there first. Settings are saved to the browser.> }
+ ].map((item) => (
+
+ {item.step}
+ {item.text}
+
+ ))}
)}
diff --git a/components/Navbar.tsx b/components/Navbar.tsx
index 590e9ee..48da20e 100644
--- a/components/Navbar.tsx
+++ b/components/Navbar.tsx
@@ -1,13 +1,25 @@
"use client";
-import React from 'react';
+import React, { useEffect } from 'react';
import { useStore } from '@/lib/store';
-import { Sparkles, LayoutGrid, Clock, Settings, User } from 'lucide-react';
+import { Sparkles, LayoutGrid, Clock, Settings, User, Sun, Moon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';
+import { useTheme } from '@/components/theme-provider';
export function Navbar() {
const { currentView, setCurrentView, setSelectionMode } = useStore();
+ const { theme, setTheme } = useTheme();
+
+ const [mounted, setMounted] = React.useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const toggleTheme = () => {
+ setTheme(theme === 'dark' ? 'light' : 'dark');
+ };
const navItems = [
{ id: 'gallery', label: 'Create', icon: Sparkles },
@@ -17,11 +29,11 @@ export function Navbar() {
return (
<>
-
- {/* Yellow Accent Line */}
-
+
+ {/* Visual Highlight Line */}
+
-
+
{/* Logo Area */}
@@ -31,7 +43,7 @@ export function Navbar() {
{/* Center Navigation (Desktop) */}
-
+
{navItems.map((item) => (
@@ -54,6 +66,12 @@ export function Navbar() {
{/* Right Actions */}
+
+ {mounted ? (theme === 'dark' ? : ) :
}
+
setCurrentView('settings')}
className={cn(
@@ -66,62 +84,16 @@ export function Navbar() {
-
-
+
+
KV
- Khoa Vo
+ Khoa Vo
- {/* Mobile Bottom Navigation */}
-
-
- {navItems.map((item) => (
-
{
- setCurrentView(item.id as any);
- if (item.id === 'history') setSelectionMode(null);
- }}
- className={cn(
- "flex flex-col items-center justify-center gap-1 p-2 rounded-xl transition-all w-16",
- currentView === item.id
- ? "text-primary"
- : "text-white/40 hover:text-white/80"
- )}
- >
-
-
-
- {item.label}
-
- ))}
- {/* Settings Item for Mobile */}
-
setCurrentView('settings')}
- className={cn(
- "flex flex-col items-center justify-center gap-1 p-2 rounded-xl transition-all w-16",
- currentView === 'settings'
- ? "text-primary"
- : "text-white/40 hover:text-white/80"
- )}
- >
-
-
-
- Settings
-
-
-
>
);
}
diff --git a/components/PromptHero.tsx b/components/PromptHero.tsx
index c58c336..ad559ce 100644
--- a/components/PromptHero.tsx
+++ b/components/PromptHero.tsx
@@ -434,28 +434,25 @@ export function PromptHero() {
);
return (
-
+
{/* Error/Warning Notification Toast */}
{errorNotification && (
-
+
-
- {errorNotification.type === 'warning' ? '⚠️ Content Moderation' : '❌ Generation Error'}
+
+ {errorNotification.type === 'warning' ? 'Content Moderation' : 'Generation Error'}
-
{errorNotification.message}
+
{errorNotification.message}
setErrorNotification(null)}
- className="p-1 hover:bg-white/10 rounded-full transition-colors"
+ className="p-1 hover:bg-black/5 dark:hover:bg-white/10 rounded-full transition-colors"
>
@@ -463,319 +460,163 @@ export function PromptHero() {
)}
+ {/* Visual Background Accent */}
+
+
- {/* Header / Title + Provider Toggle */}
-
-
-
- {settings.provider === 'meta' ? (
-
- ) : (
-
- )}
+ {/* Header */}
+
+
+
+ {settings.provider === 'meta' ? : }
-
- Create
-
- by
- {settings.provider === 'meta' ? 'Meta AI' :
- 'Whisk'}
-
-
-
+ Create
+
+ Powered by {settings.provider === 'meta' ? 'Meta AI' : 'Google Whisk'}
+
-
- {/* Provider Toggle */}
-
+
setSettings({ provider: 'whisk' })}
- className={cn(
- "flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[10px] font-medium transition-all",
- settings.provider === 'whisk' || !settings.provider
- ? "bg-white/10 text-white shadow-sm"
- : "text-white/40 hover:text-white/70 hover:bg-white/5"
- )}
- title="Google Whisk"
+ onClick={() => setSettings({ provider: settings.provider === 'meta' ? 'whisk' : 'meta' })}
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-card shadow-sm border border-border/50 text-xs font-bold text-foreground hover:bg-muted transition-all active:scale-95"
+ title="Switch Provider"
>
-
- Whisk
-
- setSettings({ provider: 'meta' })}
- className={cn(
- "flex items-center gap-1.5 px-2.5 py-1 rounded-md text-[10px] font-medium transition-all",
- settings.provider === 'meta'
- ? "bg-white/10 text-white shadow-sm"
- : "text-white/40 hover:text-white/70 hover:bg-white/5"
- )}
- title="Meta AI"
- >
-
- Meta
+
+ Switch
{/* Input Area */}
-
-
+
- {/* Controls Area */}
+ {/* Reference Upload Grid */}
+
+ {((settings.provider === 'meta' ? ['subject'] : ['subject', 'scene', 'style']) as ReferenceCategory[]).map((cat) => {
+ const refs = references[cat] || [];
+ const hasRefs = refs.length > 0;
+ const isUploading = uploadingRefs[cat];
- {/* Mobile View: Unified Stack */}
-
- {/* Unified Horizontal Scroll Toolbar */}
-
+ return (
+
toggleReference(cat)}
+ onDragOver={handleDragOver}
+ onDrop={(e) => handleDrop(e, cat)}
+ className={cn(
+ "flex flex-col items-center justify-center gap-2 py-4 rounded-2xl border transition-all relative overflow-hidden group/btn shadow-soft",
+ hasRefs
+ ? "bg-primary/5 border-primary/30"
+ : "bg-muted/50 hover:bg-muted border-border/50"
+ )}
+ >
+ {isUploading ? (
+
+ ) : hasRefs ? (
+
+
+ {refs.slice(0, 3).map((ref, idx) => (
+
+ ))}
+
+
{refs.length}
+
+ ) : (
+
+
+
+ )}
+
+ {cat}
+
+
+ );
+ })}
+
- {/* Reference Pills */}
- {((settings.provider === 'meta' ? ['subject'] : ['subject', 'scene', 'style']) as ReferenceCategory[]).map((cat) => {
- const refs = references[cat] || [];
- const hasRefs = refs.length > 0;
- const isUploading = uploadingRefs[cat];
- return (
-
toggleReference(cat)}
- className={cn(
- "flex items-center gap-1.5 flex-shrink-0 rounded-full px-3 py-1.5 text-xs font-medium transition-all border relative overflow-hidden min-h-[36px] snap-start",
- hasRefs
- ? "bg-purple-500/10 text-purple-200 border-purple-500/30"
- : "bg-white/5 text-white/40 border-white/5"
- )}
- >
- {isUploading ? (
-
- ) : hasRefs ? (
- {refs.length}
- ) : (
-
- )}
- {cat}
- {/* Clear Button (Hidden logic for simplicity on mobile pill, user can tap to open/toggle or long press? For now simplify to toggle) */}
-
- );
- })}
-
- {/* Divider */}
-
-
- {/* Settings Pills */}
+ {/* Settings & Generate Row */}
+
+
-
- {settings.provider === 'meta' ? 4 : settings.imageCount}
+
+ {settings.provider === 'meta' ? 4 : settings.imageCount}
- {settings.aspectRatio}
+
+ {settings.aspectRatio}
setSettings({ preciseMode: !settings.preciseMode })}
- className={cn("flex items-center gap-1.5 flex-shrink-0 rounded-full px-3 py-1.5 text-xs font-medium transition-all border border-white/5 min-h-[36px] snap-start",
- settings.preciseMode ? "bg-amber-500/10 text-amber-200 border-amber-500/30" : "bg-white/5 text-white/60"
+ className={cn(
+ "px-2.5 py-2 rounded-xl transition-all font-black text-[10px] tracking-tight uppercase whitespace-nowrap",
+ settings.preciseMode
+ ? "bg-secondary text-secondary-foreground shadow-sm"
+ : "text-muted-foreground hover:bg-card"
)}
+ title="Precise Mode"
>
- Precise
+ Precise
- {/* Full Width Generate Button */}
-
- {isGenerating ? (
- <>
-
-
Dreaming...
- >
- ) : (
- <>
-
-
Generate
- >
- )}
-
+ {isGenerating ? (
+ <>
+
+ Generating...
+ >
+ ) : (
+ <>
+
+ Dream Big
+ >
+ )}
+
- {/* Desktop Layout: Split Controls (Hidden on Mobile) */}
-
-
- {/* Left: References */}
-
- {((settings.provider === 'meta' ? ['subject'] : ['subject', 'scene', 'style']) as ReferenceCategory[]).map((cat) => {
- const refs = references[cat] || [];
- const hasRefs = refs.length > 0;
- const isUploading = uploadingRefs[cat];
- return (
-
-
toggleReference(cat)}
- onDragOver={handleDragOver}
- onDrop={(e) => handleDrop(e, cat)}
- className={cn(
- "flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-[10px] font-medium transition-all border relative overflow-hidden",
- hasRefs
- ? "bg-purple-500/10 text-purple-200 border-purple-500/30 hover:bg-purple-500/20"
- : "bg-white/5 text-white/40 border-white/5 hover:bg-white/10 hover:text-white/70 hover:border-white/10",
- isUploading && "animate-pulse cursor-wait"
- )}
- >
- {isUploading ? (
-
- ) : hasRefs ? (
-
- {refs.slice(0, 4).map((ref, idx) => (
-
- ))}
-
- ) : (
-
- )}
- {cat}
- {refs.length > 0 && {refs.length} }
-
- {hasRefs && !isUploading && (
-
{ e.stopPropagation(); clearReferences(cat); }}
- >
-
-
- )}
-
- );
- })}
-
-
- {/* Right: Settings & Generate */}
-
-
-
-
- {settings.provider === 'meta' ? 4 : settings.imageCount}
-
-
-
- Ratio:
- {settings.aspectRatio}
-
-
-
setSettings({ preciseMode: !settings.preciseMode })} className={cn("px-2 py-1 rounded-md text-[10px] font-medium transition-all flex items-center gap-1", settings.preciseMode ? "text-amber-300 bg-amber-500/10 ring-1 ring-amber-500/30" : "text-white/40 hover:text-white hover:bg-white/5")}>
- Precise
-
-
-
-
-
- {isGenerating ? (
- <>
-
-
Dreaming...
- >
- ) : (
- <>
-
-
Generate
- >
- )}
-
-
-
-
-
- {/* Hidden File Inputs (Shared) */}
-
handleFileInputChange(e, 'subject')} />
-
handleFileInputChange(e, 'scene')} />
-
handleFileInputChange(e, 'style')} />
-
- {/* Reference Preview Panel - shows when any references exist */}
- {
- (references.subject?.length || references.scene?.length || references.style?.length) ? (
-
-
- {(['subject', 'scene', 'style'] as ReferenceCategory[]).map((cat) => {
- const refs = references[cat] || [];
- if (refs.length === 0) return null;
- return (
-
-
- {cat}
- {refs.length}
-
-
- {refs.map((ref) => (
-
-
-
removeReference(cat, ref.id)}
- className="absolute -top-1 -right-1 p-0.5 rounded-full bg-red-500 text-white opacity-0 group-hover/thumb:opacity-100 transition-opacity hover:bg-red-600"
- title="Remove this reference"
- >
-
-
-
- ))}
- {/* Add more button */}
-
openFilePicker(cat)}
- className="h-10 w-10 rounded border border-dashed border-white/20 flex items-center justify-center text-white/30 hover:text-white/60 hover:border-white/40 transition-colors"
- title={`Add more ${cat} references`}
- >
- +
-
-
-
- );
- })}
-
-
- ) : null
- }
-
-
+ {/* Hidden File Inputs */}
+
handleFileInputChange(e, 'subject')} />
+
handleFileInputChange(e, 'scene')} />
+
handleFileInputChange(e, 'style')} />
+
);
}
diff --git a/components/PromptLibrary.tsx b/components/PromptLibrary.tsx
index b5d146b..df0bf45 100644
--- a/components/PromptLibrary.tsx
+++ b/components/PromptLibrary.tsx
@@ -17,16 +17,23 @@ export function PromptLibrary({ onSelect }: { onSelect?: (prompt: string) => voi
const [searchTerm, setSearchTerm] = useState('');
const [sortMode, setSortMode] = useState<'all' | 'latest' | 'history' | 'foryou'>('all');
+ const [error, setError] = useState
(null);
+ const [retryCount, setRetryCount] = useState(0);
+
const fetchPrompts = async () => {
setLoading(true);
+ setError(null);
try {
const res = await fetch('/api/prompts');
if (res.ok) {
const data: PromptCache = await res.json();
setPrompts(data.prompts);
+ } else {
+ throw new Error(`Server returned ${res.status}`);
}
} catch (error) {
console.error("Failed to fetch prompts", error);
+ setError("Unable to load the prompt library. Please check your connection.");
} finally {
setLoading(false);
}
@@ -34,12 +41,14 @@ export function PromptLibrary({ onSelect }: { onSelect?: (prompt: string) => voi
const syncPrompts = async () => {
setLoading(true);
+ setError(null);
try {
const syncRes = await fetch('/api/prompts/sync', { method: 'POST' });
if (!syncRes.ok) throw new Error('Sync failed');
await fetchPrompts();
} catch (error) {
console.error("Failed to sync prompts", error);
+ setError("Failed to sync new prompts from the community.");
} finally {
setLoading(false);
}
@@ -170,85 +179,111 @@ export function PromptLibrary({ onSelect }: { onSelect?: (prompt: string) => voi
const uniqueSources = ['All', ...Array.from(new Set(prompts.map(p => p.source)))].filter(Boolean);
return (
-
-
-
-
-
+
+
+
+
+
-
Prompt Library
-
Curated inspiration from the community.
+
Prompt Library
+
Curated inspiration from the community.
-
-
-
-
-
-
-
-
setSearchTerm(e.target.value)}
- />
+
+
+
setSearchTerm(e.target.value)}
+ />
+
+
+ {/* Compact Action Buttons inside search bar */}
+
+
+ {error && (
+
+
{error}
+
fetchPrompts()}
+ className="px-6 py-2 bg-destructive text-white rounded-full text-xs font-black uppercase tracking-widest hover:bg-red-600 transition-all active:scale-95"
+ >
+ Retry Now
+
+
+ )}
+
{generating && (
-
+
- Generating preview images for library prompts... This may take a while.
+ Generating library previews...
)}
{/* Smart Tabs */}
-
- {(['all', 'latest', 'history', 'foryou'] as const).map(mode => (
-
setSortMode(mode)}
- className={cn(
- "px-4 py-2 rounded-lg text-sm font-medium transition-all capitalize",
- sortMode === mode
- ? "bg-background text-foreground shadow-sm"
- : "text-muted-foreground hover:text-foreground hover:bg-background/50"
- )}
- >
- {mode === 'foryou' ? 'For You' : mode}
-
- ))}
+
+
+ {(['all', 'latest', 'history', 'foryou'] as const).map(mode => (
+ setSortMode(mode)}
+ className={cn(
+ "px-6 py-2.5 rounded-xl text-xs font-black transition-all capitalize uppercase tracking-tighter active:scale-95",
+ sortMode === mode
+ ? "bg-primary text-primary-foreground shadow-lg shadow-primary/20"
+ : "text-muted-foreground hover:text-foreground hover:bg-muted/80"
+ )}
+ >
+ {mode === 'foryou' ? 'For You' : mode}
+
+ ))}
+
- {/* Sub-Categories (only show if NOT history/foryou to keep clean? Or keep it?) */}
+ {/* Sub-Categories */}
{sortMode === 'all' && (
-
+
{uniqueCategories.map(cat => (
setSelectedCategory(cat)}
className={cn(
- "px-4 py-2 rounded-full text-sm font-medium transition-colors",
+ "px-5 py-2 rounded-2xl text-xs font-bold transition-all border active:scale-95",
selectedCategory === cat
- ? "bg-primary text-primary-foreground"
- : "bg-card hover:bg-secondary text-muted-foreground"
+ ? "bg-secondary text-secondary-foreground border-transparent shadow-md"
+ : "bg-card hover:bg-muted text-muted-foreground border-border/50"
)}
>
{cat}
@@ -258,17 +293,17 @@ export function PromptLibrary({ onSelect }: { onSelect?: (prompt: string) => voi
)}
{/* Source Filter */}
-
-
Sources:
+
+
Sources:
{uniqueSources.map(source => (
setSelectedSource(source)}
className={cn(
- "px-3 py-1 rounded-full text-xs font-medium transition-colors border",
+ "px-4 py-1.5 rounded-xl text-[10px] font-black tracking-widest uppercase transition-all border active:scale-95",
selectedSource === source
- ? "bg-primary text-primary-foreground border-primary"
- : "bg-card hover:bg-secondary text-muted-foreground border-secondary"
+ ? "bg-primary/10 text-primary border-primary/20 shadow-sm"
+ : "bg-muted/30 hover:bg-muted text-muted-foreground/70 border-border/30"
)}
>
{source}
diff --git a/components/Settings.tsx b/components/Settings.tsx
index d130aa7..c4fcd75 100644
--- a/components/Settings.tsx
+++ b/components/Settings.tsx
@@ -2,8 +2,9 @@
import React from 'react';
import { useStore } from '@/lib/store';
-import { Save, Sparkles, Brain, Settings2 } from 'lucide-react';
+import { Save, Sparkles, Brain, Settings2, Moon, Sun, Monitor, Check } from 'lucide-react';
import { cn } from '@/lib/utils';
+import { useTheme } from '@/components/theme-provider';
import { MobileCookieInstructions } from './MobileCookieInstructions';
@@ -16,6 +17,27 @@ const providers: { id: Provider; name: string; icon: any; description: string }[
export function Settings() {
const { settings, setSettings } = useStore();
+ const { theme, setTheme } = useTheme();
+ const [mounted, setMounted] = React.useState(false);
+
+ React.useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const ThemeButton = ({ theme: t, icon: Icon, label }: { theme: 'light' | 'dark' | 'system', icon: any, label: string }) => (
+ setTheme(t)}
+ className={cn(
+ "flex items-center justify-center gap-2 p-3 rounded-xl border transition-all active:scale-95",
+ mounted && theme === t
+ ? "bg-primary text-primary-foreground border-primary"
+ : "bg-card hover:bg-muted border-border text-muted-foreground"
+ )}
+ >
+
+ {label}
+
+ );
// Local state for form fields
const [provider, setProvider] = React.useState(settings.provider || 'whisk');
@@ -25,6 +47,8 @@ export function Settings() {
const [metaCookies, setMetaCookies] = React.useState(settings.metaCookies || '');
const [facebookCookies, setFacebookCookies] = React.useState(settings.facebookCookies || '');
const [saved, setSaved] = React.useState(false);
+ const [whiskVerified, setWhiskVerified] = React.useState(false);
+ const [metaVerified, setMetaVerified] = React.useState(false);
const handleSave = () => {
setSettings({
@@ -40,148 +64,283 @@ export function Settings() {
};
return (
-
-
-
Settings
-
Configure your AI image generation provider.
+
+ {/* Header Section */}
+
+
Settings
+
Configure your AI preferences and API credentials.
- {/* Provider Selection */}
-
-
Image Generation Provider
-
- {providers.map((p) => (
-
setProvider(p.id)}
- className={cn(
- "flex flex-col items-center gap-2 p-4 md:p-4 rounded-xl border-2 transition-all min-h-[100px] active:scale-95",
- provider === p.id
- ? "border-primary bg-primary/10"
- : "border-border hover:border-primary/50 bg-card"
- )}
- >
-
- {p.name}
- {p.description}
-
- ))}
-
-
+ {/* General Preferences Card */}
+
+
- {/* Provider-specific settings */}
-
- {provider === 'whisk' && (
-
-
-
Google Whisk Cookies
-