From 40ca5d7dde1bd5ddf4da4c6df27acffc7a2c4c4e Mon Sep 17 00:00:00 2001 From: hamzaMissewi Date: Wed, 16 Jul 2025 12:58:41 +0100 Subject: [PATCH] Enhance PickleGlass Web: Added LoadingSpinner component for consistent loading states, updated package.json with a new watch script, and improved README with project overview and features. Refactored activity and details pages to utilize LoadingSpinner for better user experience. --- pickleglass_web/README.md | 59 ++ pickleglass_web/app/activity/details/page.tsx | 351 +++---- pickleglass_web/app/activity/page.tsx | 222 +++-- pickleglass_web/app/download/page.tsx | 178 ++-- pickleglass_web/app/help/page.tsx | 177 ++-- pickleglass_web/app/login/page.tsx | 257 ++--- pickleglass_web/app/personalize/page.tsx | 488 +++++----- pickleglass_web/app/settings/page.tsx | 912 +++++++++--------- pickleglass_web/components/LoadingSpinner.tsx | 15 + pickleglass_web/package.json | 61 +- 10 files changed, 1364 insertions(+), 1356 deletions(-) create mode 100644 pickleglass_web/README.md create mode 100644 pickleglass_web/components/LoadingSpinner.tsx diff --git a/pickleglass_web/README.md b/pickleglass_web/README.md new file mode 100644 index 00000000..c418be82 --- /dev/null +++ b/pickleglass_web/README.md @@ -0,0 +1,59 @@ +# PickleGlass Web + +PickleGlass is a personalized AI assistant platform designed to help users in various contexts such as learning, meetings, sales, recruiting, and customer support. This is the Next.js frontend for PickleGlass, providing a modern, responsive, and accessible user interface. + +## Project Overview + +- **Framework:** Next.js (React, TypeScript) +- **Styling:** Tailwind CSS +- **State & Data:** Firebase, REST API, Local Storage +- **UI Components:** Custom and Lucide React icons + +## Main Features + +- **Personalized AI Contexts:** + - Choose or create custom AI assistant presets for different scenarios (school, meetings, sales, recruiting, customer support, etc.). +- **User Authentication:** + - Supports both Firebase (Google sign-in) and local mode for privacy. +- **Activity Tracking:** + - View, manage, and delete your past AI sessions and conversations. +- **Settings & Personalization:** + - Manage your profile, privacy settings, billing (coming soon), and API keys. +- **Search:** + - Quickly search through your conversations and activities. +- **Responsive Sidebar Navigation:** + - Collapsible sidebar with quick access to main sections and external resources. +- **Download Center:** + - Download PickleGlass for desktop, mobile, or tablet platforms. +- **Help Center:** + - Access guides and support resources. + +## Recent Improvements + +- **Reusable Loading Spinner:** + - All loading states now use a consistent, accessible `` component for better UX and maintainability. +- **UI Consistency:** + - Loading spinners and empty states are now visually unified across all pages. +- **Code Maintainability:** + - Reduced code duplication and improved readability by refactoring repeated UI patterns. + +## Getting Started + +1. **Install dependencies:** + ```bash + npm install + ``` +2. **Run the development server:** + ```bash + npm run dev + ``` +3. **Open your browser:** + Visit [http://localhost:3000](http://localhost:3000) + +## Contributing + +Contributions are welcome! Please fork the repository, create a feature branch, and submit a pull request. For major changes, open an issue first to discuss your ideas. + +## License + +This project is licensed under the MIT License. diff --git a/pickleglass_web/app/activity/details/page.tsx b/pickleglass_web/app/activity/details/page.tsx index 78b022cf..e00c6989 100644 --- a/pickleglass_web/app/activity/details/page.tsx +++ b/pickleglass_web/app/activity/details/page.tsx @@ -1,201 +1,208 @@ -'use client' - -import { useState, useEffect, Suspense } from 'react' -import { useRedirectIfNotAuth } from '@/utils/auth' -import { useSearchParams, useRouter } from 'next/navigation' -import Link from 'next/link' -import { - UserProfile, - SessionDetails, - Transcript, - AiMessage, - getSessionDetails, - deleteSession, -} from '@/utils/api' +'use client'; + +import { useState, useEffect, Suspense } from 'react'; +import { useRedirectIfNotAuth } from '@/utils/auth'; +import { useSearchParams, useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { UserProfile, SessionDetails, Transcript, AiMessage, getSessionDetails, deleteSession } from '@/utils/api'; +import LoadingSpinner from '@/components/LoadingSpinner'; type ConversationItem = (Transcript & { type: 'transcript' }) | (AiMessage & { type: 'ai_message' }); -const Section = ({ title, children }: { title: string, children: React.ReactNode }) => ( +const Section = ({ title, children }: { title: string; children: React.ReactNode }) => (

{title}

-
- {children} -
+
{children}
); function SessionDetailsContent() { - const userInfo = useRedirectIfNotAuth() as UserProfile | null; - const [sessionDetails, setSessionDetails] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const searchParams = useSearchParams(); - const sessionId = searchParams.get('sessionId'); - const router = useRouter(); - const [deleting, setDeleting] = useState(false); - - useEffect(() => { - if (userInfo && sessionId) { - const fetchDetails = async () => { - setIsLoading(true); + const userInfo = useRedirectIfNotAuth() as UserProfile | null; + const [sessionDetails, setSessionDetails] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const searchParams = useSearchParams(); + const sessionId = searchParams.get('sessionId'); + const router = useRouter(); + const [deleting, setDeleting] = useState(false); + + useEffect(() => { + if (userInfo && sessionId) { + const fetchDetails = async () => { + setIsLoading(true); + try { + const details = await getSessionDetails(sessionId as string); + setSessionDetails(details); + } catch (error) { + console.error('Failed to load session details:', error); + } finally { + setIsLoading(false); + } + }; + fetchDetails(); + } + }, [userInfo, sessionId]); + + const handleDelete = async () => { + if (!sessionId) return; + if (!window.confirm('Are you sure you want to delete this activity? This cannot be undone.')) return; + setDeleting(true); try { - const details = await getSessionDetails(sessionId as string); - setSessionDetails(details); + await deleteSession(sessionId); + router.push('/activity'); } catch (error) { - console.error('Failed to load session details:', error); - } finally { - setIsLoading(false); + alert('Failed to delete activity.'); + setDeleting(false); + console.error(error); } - }; - fetchDetails(); + }; + + if (!userInfo || isLoading) { + return ( +
+ +
+ ); } - }, [userInfo, sessionId]); - - const handleDelete = async () => { - if (!sessionId) return; - if (!window.confirm('Are you sure you want to delete this activity? This cannot be undone.')) return; - setDeleting(true); - try { - await deleteSession(sessionId); - router.push('/activity'); - } catch (error) { - alert('Failed to delete activity.'); - setDeleting(false); - console.error(error); + + if (!sessionDetails) { + return ( +
+
+

Session Not Found

+

The requested session could not be found.

+ + ← Back to Activity + +
+
+ ); } - }; - if (!userInfo || isLoading) { - return ( -
-
-
-

Loading session details...

-
-
- ); - } + const askMessages = sessionDetails.ai_messages || []; - if (!sessionDetails) { return ( -
-
-

Session Not Found

-

The requested session could not be found.

- - ← Back to Activity +
+
+
+ + + + + Back -
-
- ) - } - - const askMessages = sessionDetails.ai_messages || []; - - return ( -
-
-
- - - - - Back - -
+
-
-
-
-

- {sessionDetails.session.title || `Conversation on ${new Date(sessionDetails.session.started_at * 1000).toLocaleDateString()}`} -

-
- {new Date(sessionDetails.session.started_at * 1000).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })} - {new Date(sessionDetails.session.started_at * 1000).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })} - - {sessionDetails.session.session_type} - +
+
+
+

+ {sessionDetails.session.title || + `Conversation on ${new Date(sessionDetails.session.started_at * 1000).toLocaleDateString()}`} +

+
+ + {new Date(sessionDetails.session.started_at * 1000).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + })} + + + {new Date(sessionDetails.session.started_at * 1000).toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true, + })} + + + {sessionDetails.session.session_type} + +
+
- -
- {sessionDetails.summary && ( -
-

"{sessionDetails.summary.tldr}"

- - {sessionDetails.summary.bullet_json && JSON.parse(sessionDetails.summary.bullet_json).length > 0 && -
-

Key Points:

-
    - {JSON.parse(sessionDetails.summary.bullet_json).map((point: string, index: number) => ( -
  • {point}
  • - ))} -
+ {sessionDetails.summary && ( +
+

"{sessionDetails.summary.tldr}"

+ + {sessionDetails.summary.bullet_json && JSON.parse(sessionDetails.summary.bullet_json).length > 0 && ( +
+

Key Points:

+
    + {JSON.parse(sessionDetails.summary.bullet_json).map((point: string, index: number) => ( +
  • {point}
  • + ))} +
+
+ )} + + {sessionDetails.summary.action_json && JSON.parse(sessionDetails.summary.action_json).length > 0 && ( +
+

Action Items:

+
    + {JSON.parse(sessionDetails.summary.action_json).map((action: string, index: number) => ( +
  • {action}
  • + ))} +
+
+ )} +
+ )} + + {sessionDetails.transcripts && sessionDetails.transcripts.length > 0 && ( +
+
+ {sessionDetails.transcripts.map(item => ( +

+ {item.speaker}: + {item.text} +

+ ))}
- } - - {sessionDetails.summary.action_json && JSON.parse(sessionDetails.summary.action_json).length > 0 && -
-

Action Items:

-
    - {JSON.parse(sessionDetails.summary.action_json).map((action: string, index: number) => ( -
  • {action}
  • - ))} -
+
+ )} + + {askMessages.length > 0 && ( +
+
+ {askMessages.map(item => ( +
+

{item.role === 'user' ? 'You' : 'AI'}

+

{item.content}

+
+ ))}
- } -
- )} - - {sessionDetails.transcripts && sessionDetails.transcripts.length > 0 && ( -
-
- {sessionDetails.transcripts.map((item) => ( -

- {item.speaker}: - {item.text} -

- ))} -
-
- )} - - {askMessages.length > 0 && ( -
-
- {askMessages.map((item) => ( -
-

{item.role === 'user' ? 'You' : 'AI'}

-

{item.content}

-
- ))} -
-
- )} +
+ )} +
-
- ); + ); } export default function SessionDetailsPage() { - return ( - -
-
-

Loading...

-
-
- }> - - - ); -} \ No newline at end of file + return ( + + +
+ } + > + + + ); +} diff --git a/pickleglass_web/app/activity/page.tsx b/pickleglass_web/app/activity/page.tsx index 9d605782..406b43a3 100644 --- a/pickleglass_web/app/activity/page.tsx +++ b/pickleglass_web/app/activity/page.tsx @@ -1,124 +1,122 @@ -'use client' +'use client'; -import { useState, useEffect } from 'react' -import Link from 'next/link' -import { useRedirectIfNotAuth } from '@/utils/auth' -import { - UserProfile, - Session, - getSessions, - deleteSession, -} from '@/utils/api' +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { useRedirectIfNotAuth } from '@/utils/auth'; +import { UserProfile, Session, getSessions, deleteSession } from '@/utils/api'; +import LoadingSpinner from '@/components/LoadingSpinner'; export default function ActivityPage() { - const userInfo = useRedirectIfNotAuth() as UserProfile | null; - const [sessions, setSessions] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [deletingId, setDeletingId] = useState(null) + const userInfo = useRedirectIfNotAuth() as UserProfile | null; + const [sessions, setSessions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [deletingId, setDeletingId] = useState(null); - const fetchSessions = async () => { - try { - const fetchedSessions = await getSessions(); - setSessions(fetchedSessions); - } catch (error) { - console.error('Failed to fetch conversations:', error) - } finally { - setIsLoading(false) - } - } + const fetchSessions = async () => { + try { + const fetchedSessions = await getSessions(); + setSessions(fetchedSessions); + } catch (error) { + console.error('Failed to fetch conversations:', error); + } finally { + setIsLoading(false); + } + }; - useEffect(() => { - fetchSessions() - }, []) + useEffect(() => { + fetchSessions(); + }, []); - if (!userInfo) { - return ( -
-
-
-

Loading...

-
-
- ) - } + if (!userInfo) { + return ( +
+ +
+ ); + } - const getGreeting = () => { - const hour = new Date().getHours() - if (hour < 12) return 'Good morning' - if (hour < 18) return 'Good afternoon' - return 'Good evening' - } + const getGreeting = () => { + const hour = new Date().getHours(); + if (hour < 12) return 'Good morning'; + if (hour < 18) return 'Good afternoon'; + return 'Good evening'; + }; - const handleDelete = async (sessionId: string) => { - if (!window.confirm('Are you sure you want to delete this activity? This cannot be undone.')) return; - setDeletingId(sessionId); - try { - await deleteSession(sessionId); - setSessions(sessions => sessions.filter(s => s.id !== sessionId)); - } catch (error) { - alert('Failed to delete activity.'); - console.error(error); - } finally { - setDeletingId(null); - } - } + const handleDelete = async (sessionId: string) => { + if (!window.confirm('Are you sure you want to delete this activity? This cannot be undone.')) return; + setDeletingId(sessionId); + try { + await deleteSession(sessionId); + setSessions(sessions => sessions.filter(s => s.id !== sessionId)); + } catch (error) { + alert('Failed to delete activity.'); + console.error(error); + } finally { + setDeletingId(null); + } + }; - return ( -
-
-
-

- {getGreeting()}, {userInfo.display_name} -

-
-
-

- Your Past Activity -

- {isLoading ? ( -
-
-

Loading conversations...

-
- ) : sessions.length > 0 ? ( -
- {sessions.map((session) => ( -
-
-
- - {session.title || `Conversation - ${new Date(session.started_at * 1000).toLocaleDateString()}`} - -
- {new Date(session.started_at * 1000).toLocaleString()} -
-
- -
- - {session.session_type || 'ask'} - + return ( +
+
+
+

+ {getGreeting()}, {userInfo.display_name} +

+
+
+

Your Past Activity

+ {isLoading ? ( + + ) : sessions.length > 0 ? ( +
+ {sessions.map(session => ( +
+
+
+ + {session.title || `Conversation - ${new Date(session.started_at * 1000).toLocaleDateString()}`} + +
{new Date(session.started_at * 1000).toLocaleString()}
+
+ +
+ + {session.session_type || 'ask'} + +
+ ))} +
+ ) : ( +
+

+ No conversations yet. Start a conversation in the desktop app to see your activity here. +

+
+ 💡 Tip: Use the desktop app to have AI-powered conversations that will appear here automatically. +
+
+ )}
- ))} -
- ) : ( -
-

- No conversations yet. Start a conversation in the desktop app to see your activity here. -

-
- 💡 Tip: Use the desktop app to have AI-powered conversations that will appear here automatically. -
- )}
-
-
- ) -} \ No newline at end of file + ); +} diff --git a/pickleglass_web/app/download/page.tsx b/pickleglass_web/app/download/page.tsx index 79e9c31a..2752cf99 100644 --- a/pickleglass_web/app/download/page.tsx +++ b/pickleglass_web/app/download/page.tsx @@ -1,102 +1,98 @@ -'use client' +'use client'; -import { Download, Smartphone, Monitor, Tablet } from 'lucide-react' -import { useRedirectIfNotAuth } from '@/utils/auth' +import { Monitor, Download, Smartphone, Tablet } from 'lucide-react'; +import { useRedirectIfNotAuth } from '@/utils/auth'; +import LoadingSpinner from '@/components/LoadingSpinner'; export default function DownloadPage() { - const userInfo = useRedirectIfNotAuth() + const userInfo = useRedirectIfNotAuth(); + + if (!userInfo) { + return ; + } - if (!userInfo) { return ( -
-
-
-

Loading...

-
-
- ) - } +
+
+

Download pickleglass

+

Use pickleglass on various platforms

- return ( -
-
-

Download pickleglass

-

- Use pickleglass on various platforms -

- -
-
- -

Desktop

-

Windows, macOS, Linux

- -
+
+
+ +

Desktop

+

Windows, macOS, Linux

+ +
-
- -

Mobile

-

iOS, Android

-
- - -
-
+
+ +

Mobile

+

iOS, Android

+
+ + +
+
-
- -

Tablet

-

iPad, Android Tablet

- -
-
+
+ +

Tablet

+

iPad, Android Tablet

+ +
+
-
-

System Requirements

-
-
-

Windows

-
    -
  • • Windows 10 or later
  • -
  • • 4GB RAM
  • -
  • • 100MB Storage
  • -
-
-
-

macOS

-
    -
  • • macOS 11.0 or later
  • -
  • • 4GB RAM
  • -
  • • 100MB Storage
  • -
-
-
-

Mobile

-
    -
  • • iOS 14.0 or later
  • -
  • • Android 8.0 or later
  • -
  • • 50MB Storage
  • -
-
-
-
+
+

System Requirements

+
+
+

Windows

+
    +
  • • Windows 10 or later
  • +
  • • 4GB RAM
  • +
  • • 100MB Storage
  • +
+
+
+

macOS

+
    +
  • • macOS 11.0 or later
  • +
  • • 4GB RAM
  • +
  • • 100MB Storage
  • +
+
+
+

Mobile

+
    +
  • • iOS 14.0 or later
  • +
  • • Android 8.0 or later
  • +
  • • 50MB Storage
  • +
+
+
+
-
-

- Having issues? Check out our Help Center. -

+
+

+ Having issues? Check out our{' '} + + Help Center + + . +

+
+
-
-
- ) -} \ No newline at end of file + ); +} diff --git a/pickleglass_web/app/help/page.tsx b/pickleglass_web/app/help/page.tsx index 5bc91d17..ce532b4d 100644 --- a/pickleglass_web/app/help/page.tsx +++ b/pickleglass_web/app/help/page.tsx @@ -1,110 +1,89 @@ -'use client' +'use client'; -import { HelpCircle, Book, MessageCircle, Mail } from 'lucide-react' -import { useRedirectIfNotAuth } from '@/utils/auth' +import { HelpCircle, Book, MessageCircle, Mail } from 'lucide-react'; +import { useRedirectIfNotAuth } from '@/utils/auth'; +import LoadingSpinner from '@/components/LoadingSpinner'; export default function HelpPage() { - const userInfo = useRedirectIfNotAuth() + const userInfo = useRedirectIfNotAuth(); + + if (!userInfo) { + return ( +
+ +
+ ); + } - if (!userInfo) { return ( -
-
-
-

Loading...

-
-
- ) - } +
+
+

Help Center

- return ( -
-
-

Help Center

- -
-
-
- -

Getting Started

-
-

- New to pickleglass? Learn about basic features and setup methods. -

-
    -
  • • Setting up personalized contexts
  • -
  • • Selecting presets and creating custom contexts
  • -
  • • Checking activity records
  • -
  • • Changing settings
  • -
-
+
+
+
+ +

Getting Started

+
+

New to pickleglass? Learn about basic features and setup methods.

+
    +
  • • Setting up personalized contexts
  • +
  • • Selecting presets and creating custom contexts
  • +
  • • Checking activity records
  • +
  • • Changing settings
  • +
+
-
-
- -

Frequently Asked Questions

-
-

- Check out frequently asked questions and answers from other users. -

-
-
- - How do I change the context? - -

- On the Personalize page, select a preset or enter a custom context, then click the Save button. -

-
-
- - Where can I check my activity history? - -

- You can check your past activity records on the My Activity page. -

-
-
-
-
+
+
+ +

Frequently Asked Questions

+
+

Check out frequently asked questions and answers from other users.

+
+
+ How do I change the context? +

+ On the Personalize page, select a preset or enter a custom context, then click the Save button. +

+
+
+ Where can I check my activity history? +

You can check your past activity records on the My Activity page.

+
+
+
+
-
-
-
- -

Community

-
-

- Connect with other users and share tips. -

- -
+
+
+
+ +

Community

+
+

Connect with other users and share tips.

+ +
-
-
- -

Contact Us

-
-

- Couldn't find a solution? Contact us directly. -

- -
-
+
+
+ +

Contact Us

+
+

Couldn't find a solution? Contact us directly.

+ +
+
-
-

💡 Tip

-

- Each context is optimized for different situations. - Choose the appropriate preset for your work environment, - or create your own custom context! -

+
+

💡 Tip

+

+ Each context is optimized for different situations. Choose the appropriate preset for your work environment, or create your + own custom context! +

+
+
-
-
- ) -} \ No newline at end of file + ); +} diff --git a/pickleglass_web/app/login/page.tsx b/pickleglass_web/app/login/page.tsx index 60ab49a0..298f59e0 100644 --- a/pickleglass_web/app/login/page.tsx +++ b/pickleglass_web/app/login/page.tsx @@ -1,132 +1,139 @@ -'use client' +'use client'; -import { useRouter } from 'next/navigation' -import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth' -import { auth } from '@/utils/firebase' -import { Chrome } from 'lucide-react' -import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation'; +import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; +import { auth } from '@/utils/firebase'; +import { Chrome } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import LoadingSpinner from '@/components/LoadingSpinner'; export default function LoginPage() { - const router = useRouter() - const [isLoading, setIsLoading] = useState(false) - const [isElectronMode, setIsElectronMode] = useState(false) - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search) - const mode = urlParams.get('mode') - setIsElectronMode(mode === 'electron') - }, []) - - const handleGoogleSignIn = async () => { - const provider = new GoogleAuthProvider() - setIsLoading(true) - - try { - const result = await signInWithPopup(auth, provider) - const user = result.user - - if (user) { - console.log('✅ Google login successful:', user.uid) - - if (isElectronMode) { - try { - const idToken = await user.getIdToken() - - const deepLinkUrl = `pickleglass://auth-success?` + new URLSearchParams({ - uid: user.uid, - email: user.email || '', - displayName: user.displayName || '', - token: idToken - }).toString() - - console.log('🔗 Return to electron app via deep link:', deepLinkUrl) - - window.location.href = deepLinkUrl - - setTimeout(() => { - alert('Login completed. Please return to Pickle Glass app.') - }, 1000) - - } catch (error) { - console.error('❌ Deep link processing failed:', error) - alert('Login was successful but failed to return to app. Please check the app.') - } - } - else if (typeof window !== 'undefined' && window.require) { - try { - const { ipcRenderer } = window.require('electron') - const idToken = await user.getIdToken() - - ipcRenderer.send('firebase-auth-success', { - uid: user.uid, - displayName: user.displayName, - email: user.email, - idToken - }) - - console.log('📡 Auth info sent to electron successfully') - } catch (error) { - console.error('❌ Electron communication failed:', error) - } - } - else { - router.push('/settings') - } - } - } catch (error: any) { - console.error('❌ Google login failed:', error) - - if (error.code !== 'auth/popup-closed-by-user') { - alert('An error occurred during login. Please try again.') - } - } finally { - setIsLoading(false) - } - } - - return ( -
-
-

Welcome to Pickle Glass

-

Sign in with your Google account to sync your data across all devices.

- {isElectronMode ? ( -

🔗 Login requested from Electron app

- ) : ( -

Local mode will run if you don't sign in.

- )} -
- -
-
- - -
- -
+ } + } catch (error: any) { + console.error('❌ Google login failed:', error); + + if (error.code !== 'auth/popup-closed-by-user') { + alert('An error occurred during login. Please try again.'); + } + } finally { + setIsLoading(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

Welcome to Pickle Glass

+

Sign in with your Google account to sync your data across all devices.

+ {isElectronMode ? ( +

🔗 Login requested from Electron app

+ ) : ( +

Local mode will run if you don't sign in.

+ )} +
+ +
+
+ + +
+ +
+
+ +

By signing in, you agree to our Terms of Service and Privacy Policy.

+
- -

- By signing in, you agree to our Terms of Service and Privacy Policy. -

-
-
- ) -} \ No newline at end of file + ); +} diff --git a/pickleglass_web/app/personalize/page.tsx b/pickleglass_web/app/personalize/page.tsx index c24398b6..ac44cc4c 100644 --- a/pickleglass_web/app/personalize/page.tsx +++ b/pickleglass_web/app/personalize/page.tsx @@ -1,275 +1,263 @@ -'use client' +'use client'; -import { useState, useEffect } from 'react' -import { ChevronDown, Plus, Copy } from 'lucide-react' -import { getPresets, updatePreset, createPreset, PromptPreset } from '@/utils/api' +import { useState, useEffect } from 'react'; +import { ChevronDown, Plus, Copy } from 'lucide-react'; +import { getPresets, updatePreset, createPreset, PromptPreset } from '@/utils/api'; +import LoadingSpinner from '@/components/LoadingSpinner'; export default function PersonalizePage() { - const [allPresets, setAllPresets] = useState([]); - const [selectedPreset, setSelectedPreset] = useState(null); - const [showPresets, setShowPresets] = useState(true); - const [editorContent, setEditorContent] = useState(''); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [isDirty, setIsDirty] = useState(false); + const [allPresets, setAllPresets] = useState([]); + const [selectedPreset, setSelectedPreset] = useState(null); + const [showPresets, setShowPresets] = useState(true); + const [editorContent, setEditorContent] = useState(''); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [isDirty, setIsDirty] = useState(false); - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - const presetsData = await getPresets(); - setAllPresets(presetsData); - - if (presetsData.length > 0) { - const firstUserPreset = presetsData.find(p => p.is_default === 0) || presetsData[0]; - setSelectedPreset(firstUserPreset); - setEditorContent(firstUserPreset.prompt); + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const presetsData = await getPresets(); + setAllPresets(presetsData); + + if (presetsData.length > 0) { + const firstUserPreset = presetsData.find(p => p.is_default === 0) || presetsData[0]; + setSelectedPreset(firstUserPreset); + setEditorContent(firstUserPreset.prompt); + } + } catch (error) { + console.error('Failed to fetch presets:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const handlePresetClick = (preset: PromptPreset) => { + if (isDirty && !window.confirm('You have unsaved changes. Are you sure you want to switch?')) { + return; } - } catch (error) { - console.error("Failed to fetch presets:", error); - } finally { - setLoading(false); - } + setSelectedPreset(preset); + setEditorContent(preset.prompt); + setIsDirty(false); }; - - fetchData(); - }, []); - const handlePresetClick = (preset: PromptPreset) => { - if (isDirty && !window.confirm("You have unsaved changes. Are you sure you want to switch?")) { - return; - } - setSelectedPreset(preset); - setEditorContent(preset.prompt); - setIsDirty(false); - }; + const handleEditorChange = (e: React.ChangeEvent) => { + setEditorContent(e.target.value); + setIsDirty(true); + }; - const handleEditorChange = (e: React.ChangeEvent) => { - setEditorContent(e.target.value); - setIsDirty(true); - }; + const handleSave = async () => { + if (!selectedPreset || saving || !isDirty) return; - const handleSave = async () => { - if (!selectedPreset || saving || !isDirty) return; - - if (selectedPreset.is_default === 1) { - alert("Default presets cannot be modified."); - return; - } - - try { - setSaving(true); - await updatePreset(selectedPreset.id, { - title: selectedPreset.title, - prompt: editorContent - }); + if (selectedPreset.is_default === 1) { + alert('Default presets cannot be modified.'); + return; + } - setAllPresets(prev => - prev.map(p => - p.id === selectedPreset.id - ? { ...p, prompt: editorContent } - : p - ) - ); - setIsDirty(false); - } catch (error) { - console.error("Save failed:", error); - alert("Failed to save preset. See console for details."); - } finally { - setSaving(false); - } - }; + try { + setSaving(true); + await updatePreset(selectedPreset.id, { + title: selectedPreset.title, + prompt: editorContent, + }); - const handleCreateNewPreset = async () => { - const title = prompt("Enter a title for the new preset:"); - if (!title) return; - - try { - setSaving(true); - const { id } = await createPreset({ - title, - prompt: "Enter your custom prompt here..." - }); - - const newPreset: PromptPreset = { - id, - uid: 'current_user', - title, - prompt: "Enter your custom prompt here...", - is_default: 0, - created_at: Date.now(), - sync_state: 'clean' - }; - - setAllPresets(prev => [...prev, newPreset]); - setSelectedPreset(newPreset); - setEditorContent(newPreset.prompt); - setIsDirty(false); - } catch (error) { - console.error("Failed to create preset:", error); - alert("Failed to create preset. See console for details."); - } finally { - setSaving(false); - } - }; + setAllPresets(prev => prev.map(p => (p.id === selectedPreset.id ? { ...p, prompt: editorContent } : p))); + setIsDirty(false); + } catch (error) { + console.error('Save failed:', error); + alert('Failed to save preset. See console for details.'); + } finally { + setSaving(false); + } + }; - const handleDuplicatePreset = async () => { - if (!selectedPreset) return; - - const title = prompt("Enter a title for the duplicated preset:", `${selectedPreset.title} (Copy)`); - if (!title) return; - - try { - setSaving(true); - const { id } = await createPreset({ - title, - prompt: editorContent - }); - - const newPreset: PromptPreset = { - id, - uid: 'current_user', - title, - prompt: editorContent, - is_default: 0, - created_at: Date.now(), - sync_state: 'clean' - }; - - setAllPresets(prev => [...prev, newPreset]); - setSelectedPreset(newPreset); - setIsDirty(false); - } catch (error) { - console.error("Failed to duplicate preset:", error); - alert("Failed to duplicate preset. See console for details."); - } finally { - setSaving(false); - } - }; + const handleCreateNewPreset = async () => { + const title = prompt('Enter a title for the new preset:'); + if (!title) return; - if (loading) { - return ( -
-
Loading...
-
- ); - } + try { + setSaving(true); + const { id } = await createPreset({ + title, + prompt: 'Enter your custom prompt here...', + }); + + const newPreset: PromptPreset = { + id, + uid: 'current_user', + title, + prompt: 'Enter your custom prompt here...', + is_default: 0, + created_at: Date.now(), + sync_state: 'clean', + }; + + setAllPresets(prev => [...prev, newPreset]); + setSelectedPreset(newPreset); + setEditorContent(newPreset.prompt); + setIsDirty(false); + } catch (error) { + console.error('Failed to create preset:', error); + alert('Failed to create preset. See console for details.'); + } finally { + setSaving(false); + } + }; + + const handleDuplicatePreset = async () => { + if (!selectedPreset) return; - return ( -
-
-
-
-
-

Presets

-

Personalize

+ const title = prompt('Enter a title for the duplicated preset:', `${selectedPreset.title} (Copy)`); + if (!title) return; + + try { + setSaving(true); + const { id } = await createPreset({ + title, + prompt: editorContent, + }); + + const newPreset: PromptPreset = { + id, + uid: 'current_user', + title, + prompt: editorContent, + is_default: 0, + created_at: Date.now(), + sync_state: 'clean', + }; + + setAllPresets(prev => [...prev, newPreset]); + setSelectedPreset(newPreset); + setIsDirty(false); + } catch (error) { + console.error('Failed to duplicate preset:', error); + alert('Failed to duplicate preset. See console for details.'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
-
- - {selectedPreset && ( - - )} - + ); + } + + return ( +
+
+
+
+
+

Presets

+

Personalize

+
+
+ + {selectedPreset && ( + + )} + +
+
+
-
-
-
-
-
-
- -
- - {showPresets && ( -
- {allPresets.map((preset) => ( -
handlePresetClick(preset)} - className={` +
+
+
+ +
+ + {showPresets && ( +
+ {allPresets.map(preset => ( +
handlePresetClick(preset)} + className={` p-4 rounded-lg cursor-pointer transition-all duration-200 bg-white h-48 flex flex-col shadow-sm hover:shadow-md relative - ${selectedPreset?.id === preset.id - ? 'border-2 border-blue-500 shadow-md' - : 'border border-gray-200 hover:border-gray-300' - } + ${selectedPreset?.id === preset.id ? 'border-2 border-blue-500 shadow-md' : 'border border-gray-200 hover:border-gray-300'} `} - > - {preset.is_default === 1 && ( -
- Default -
- )} -

- {preset.title} -

-

- {preset.prompt.substring(0, 100) + (preset.prompt.length > 100 ? '...' : '')} -

+ > + {preset.is_default === 1 && ( +
+ Default +
+ )} +

{preset.title}

+

+ {preset.prompt.substring(0, 100) + (preset.prompt.length > 100 ? '...' : '')} +

+
+ ))} +
+ )}
- ))}
- )} -
-
-
-
- {selectedPreset?.is_default === 1 && ( -
-
-
-

- This is a default preset and cannot be edited. - Use the "Duplicate" button above to create an editable copy, or create a new preset. -

-
+
+
+ {selectedPreset?.is_default === 1 && ( +
+
+
+

+ This is a default preset and cannot be edited. + Use the "Duplicate" button above to create an editable copy, or create a new preset. +

+
+
+ )} +