Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions pickleglass_web/README.md
Original file line number Diff line number Diff line change
@@ -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 `<LoadingSpinner />` 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.
351 changes: 179 additions & 172 deletions pickleglass_web/app/activity/details/page.tsx

Large diffs are not rendered by default.

222 changes: 110 additions & 112 deletions pickleglass_web/app/activity/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Session[]>([])
const [isLoading, setIsLoading] = useState(true)
const [deletingId, setDeletingId] = useState<string | null>(null)
const userInfo = useRedirectIfNotAuth() as UserProfile | null;
const [sessions, setSessions] = useState<Session[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [deletingId, setDeletingId] = useState<string | null>(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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
)
}
if (!userInfo) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<LoadingSpinner />
</div>
);
}

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 (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto px-8 py-12">
<div className="text-center mb-12">
<h1 className="text-2xl text-gray-600">
{getGreeting()}, {userInfo.display_name}
</h1>
</div>
<div>
<h2 className="text-2xl font-semibold text-gray-900 mb-8 text-center">
Your Past Activity
</h2>
{isLoading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading conversations...</p>
</div>
) : sessions.length > 0 ? (
<div className="space-y-4">
{sessions.map((session) => (
<div key={session.id} className="block bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-shadow cursor-pointer">
<div className="flex justify-between items-start mb-3">
<div>
<Link href={`/activity/details?sessionId=${session.id}`} className="text-lg font-medium text-gray-900 hover:underline">
{session.title || `Conversation - ${new Date(session.started_at * 1000).toLocaleDateString()}`}
</Link>
<div className="text-sm text-gray-500">
{new Date(session.started_at * 1000).toLocaleString()}
</div>
</div>
<button
onClick={() => handleDelete(session.id)}
disabled={deletingId === session.id}
className={`ml-4 px-3 py-1 rounded text-xs font-medium border border-red-200 text-red-700 bg-red-50 hover:bg-red-100 transition-colors ${deletingId === session.id ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{deletingId === session.id ? 'Deleting...' : 'Delete'}
</button>
</div>
<span className={`capitalize inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${session.session_type === 'listen' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'}`}>
{session.session_type || 'ask'}
</span>
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto px-8 py-12">
<div className="text-center mb-12">
<h1 className="text-2xl text-gray-600">
{getGreeting()}, {userInfo.display_name}
</h1>
</div>
<div>
<h2 className="text-2xl font-semibold text-gray-900 mb-8 text-center">Your Past Activity</h2>
{isLoading ? (
<LoadingSpinner />
) : sessions.length > 0 ? (
<div className="space-y-4">
{sessions.map(session => (
<div
key={session.id}
className="block bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-shadow cursor-pointer"
>
<div className="flex justify-between items-start mb-3">
<div>
<Link
href={`/activity/details?sessionId=${session.id}`}
className="text-lg font-medium text-gray-900 hover:underline"
>
{session.title || `Conversation - ${new Date(session.started_at * 1000).toLocaleDateString()}`}
</Link>
<div className="text-sm text-gray-500">{new Date(session.started_at * 1000).toLocaleString()}</div>
</div>
<button
onClick={() => handleDelete(session.id)}
disabled={deletingId === session.id}
className={`ml-4 px-3 py-1 rounded text-xs font-medium border border-red-200 text-red-700 bg-red-50 hover:bg-red-100 transition-colors ${
deletingId === session.id ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{deletingId === session.id ? 'Deleting...' : 'Delete'}
</button>
</div>
<span
className={`capitalize inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
session.session_type === 'listen' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'
}`}
>
{session.session_type || 'ask'}
</span>
</div>
))}
</div>
) : (
<div className="text-center bg-white rounded-lg p-12">
<p className="text-gray-500 mb-4">
No conversations yet. Start a conversation in the desktop app to see your activity here.
</p>
<div className="text-sm text-gray-400">
💡 Tip: Use the desktop app to have AI-powered conversations that will appear here automatically.
</div>
</div>
)}
</div>
))}
</div>
) : (
<div className="text-center bg-white rounded-lg p-12">
<p className="text-gray-500 mb-4">
No conversations yet. Start a conversation in the desktop app to see your activity here.
</p>
<div className="text-sm text-gray-400">
💡 Tip: Use the desktop app to have AI-powered conversations that will appear here automatically.
</div>
</div>
)}
</div>
</div>
</div>
)
}
);
}
Loading