diff --git a/README.md b/README.md index 610bf7b..6e07f05 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,4 @@ We love contributions! Feel free to open issues for bugs or feature requests. **Our mission is to build a living digital clone for everyone.** Glass is part of Step 1—a trusted pipeline that transforms your daily data into a scalable clone. Visit [pickle.com](https://pickle.com) to learn more. ## Star History - - - +[![Star History Chart](https://api.star-history.com/svg?repos=pickle-com/glass&type=Date)](https://www.star-history.com/#pickle-com/glass&Date) \ No newline at end of file diff --git a/electron-builder.yml b/electron-builder.yml index 532f4ef..d1c1b5d 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -8,29 +8,41 @@ productName: Glass # Publish configuration for GitHub releases publish: - provider: github - owner: pickle-com - repo: glass - releaseType: draft + provider: github + owner: pickle-com + repo: glass + releaseType: draft # List of files to be included in the app package files: - - src/**/* - - package.json - - pickleglass_web/backend_node/**/* - - '!**/node_modules/electron/**' - - public/build/**/* + - src/**/* + - package.json + - pickleglass_web/backend_node/**/* + - '!**/node_modules/electron/**' + - public/build/**/* # Additional resources to be copied into the app's resources directory extraResources: - - from: src/assets/SystemAudioDump - to: SystemAudioDump - - from: pickleglass_web/out - to: out + - from: src/assets/SystemAudioDump + to: SystemAudioDump + - from: pickleglass_web/out + to: out # macOS specific configuration mac: - # The application category type - category: public.app-category.utilities - # Path to the .icns icon file - icon: src/assets/logo.icns \ No newline at end of file + # The application category type + category: public.app-category.utilities + # Path to the .icns icon file + icon: src/assets/logo.icns + # Target both Intel and Apple Silicon architectures + target: + - target: dmg + arch: + - x64 + - arm64 + - target: zip + arch: + - x64 + - arm64 + # Minimum macOS version (supports both Intel and Apple Silicon) + minimumSystemVersion: '11.0' diff --git a/forge.config.js b/forge.config.js index 4f0bd19..f60a427 100644 --- a/forge.config.js +++ b/forge.config.js @@ -5,25 +5,26 @@ const { notarizeApp } = require('./notarize'); module.exports = { packagerConfig: { asar: { - unpack: - '**/*.node,**/*.dylib,' + - '**/node_modules/{sharp,@img}/**/*' + unpack: '**/*.node,**/*.dylib,' + '**/node_modules/{sharp,@img}/**/*', }, extraResource: ['./src/assets/SystemAudioDump', './pickleglass_web/out'], name: 'Glass', icon: 'src/assets/logo', appBundleId: 'com.pickle.glass', + arch: 'universal', protocols: [ { name: 'PickleGlass Protocol', - schemes: ['pickleglass'] - } + schemes: ['pickleglass'], + }, ], asarUnpack: [ - "**/*.node", - "**/*.dylib", - "node_modules/@img/sharp-darwin-arm64/**", - "node_modules/@img/sharp-libvips-darwin-arm64/**" + '**/*.node', + '**/*.dylib', + 'node_modules/@img/sharp-darwin-x64/**', + 'node_modules/@img/sharp-libvips-darwin-x64/**', + 'node_modules/@img/sharp-darwin-arm64/**', + 'node_modules/@img/sharp-libvips-darwin-arm64/**', ], osxSign: { identity: process.env.APPLE_SIGNING_IDENTITY, @@ -35,8 +36,8 @@ module.exports = { tool: 'notarytool', appleId: process.env.APPLE_ID, appleIdPassword: process.env.APPLE_ID_PASSWORD, - teamId: process.env.APPLE_TEAM_ID - } + teamId: process.env.APPLE_TEAM_ID, + }, }, rebuildConfig: {}, makers: [ diff --git a/package.json b/package.json index b443c9e..2f9b5ec 100644 --- a/package.json +++ b/package.json @@ -66,5 +66,9 @@ "electron-builder": "^26.0.12", "electron-reloader": "^1.2.3", "esbuild": "^0.25.5" + }, + "optionalDependencies": { + "@img/sharp-darwin-x64": "^0.34.2", + "@img/sharp-libvips-darwin-x64": "^1.1.0" } } diff --git a/pickleglass_web/app/activity/details/page.tsx b/pickleglass_web/app/activity/details/page.tsx index f00f71a..f2069b5 100644 --- a/pickleglass_web/app/activity/details/page.tsx +++ b/pickleglass_web/app/activity/details/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, Suspense } from 'react' import { useRedirectIfNotAuth } from '@/utils/auth' -import { useSearchParams } from 'next/navigation' +import { useSearchParams, useRouter } from 'next/navigation' import Link from 'next/link' import { UserProfile, @@ -10,6 +10,7 @@ import { Transcript, AiMessage, getSessionDetails, + deleteSession, } from '@/utils/api' type ConversationItem = (Transcript & { type: 'transcript' }) | (AiMessage & { type: 'ai_message' }); @@ -29,6 +30,8 @@ function SessionDetailsContent() { 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) { @@ -47,6 +50,20 @@ function SessionDetailsContent() { } }, [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 (!userInfo || isLoading) { return (
@@ -92,14 +109,23 @@ function SessionDetailsContent() {
-
-

- {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.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.summary && ( diff --git a/pickleglass_web/app/activity/page.tsx b/pickleglass_web/app/activity/page.tsx index 2909c45..663c17a 100644 --- a/pickleglass_web/app/activity/page.tsx +++ b/pickleglass_web/app/activity/page.tsx @@ -7,12 +7,14 @@ import { UserProfile, Session, getSessions, + deleteSession, } from '@/utils/api' 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 fetchSessions = async () => { try { @@ -47,6 +49,20 @@ export default function ActivityPage() { 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); + } + } + return (
@@ -67,17 +83,28 @@ export default function ActivityPage() { ) : sessions.length > 0 ? (
{sessions.map((session) => ( - +
-

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

- - {new Date(session.started_at * 1000).toLocaleString()} - +
+ + {session.title || `Conversation - ${new Date(session.started_at * 1000).toLocaleDateString()}`} + +
+ {new Date(session.started_at * 1000).toLocaleString()} +
+
+
- - Conversation - - + + Conversation + +
))}
) : ( diff --git a/pickleglass_web/app/personalize/page.tsx b/pickleglass_web/app/personalize/page.tsx index 1a0d562..c24398b 100644 --- a/pickleglass_web/app/personalize/page.tsx +++ b/pickleglass_web/app/personalize/page.tsx @@ -1,8 +1,8 @@ 'use client' import { useState, useEffect } from 'react' -import { ChevronDown } from 'lucide-react' -import { getPresets, updatePreset, PromptPreset } from '@/utils/api' +import { ChevronDown, Plus, Copy } from 'lucide-react' +import { getPresets, updatePreset, createPreset, PromptPreset } from '@/utils/api' export default function PersonalizePage() { const [allPresets, setAllPresets] = useState([]); @@ -72,7 +72,6 @@ export default function PersonalizePage() { ) ); setIsDirty(false); - console.log('Save completed!'); } catch (error) { console.error("Save failed:", error); alert("Failed to save preset. See console for details."); @@ -81,6 +80,73 @@ export default function PersonalizePage() { } }; + 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); + } + }; + + 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); + } + }; + if (loading) { return (
@@ -98,19 +164,39 @@ export default function PersonalizePage() {

Presets

Personalize

- +
+ + {selectedPreset && ( + + )} + +
@@ -137,13 +223,18 @@ export default function PersonalizePage() { onClick={() => 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 + 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' } `} > + {preset.is_default === 1 && ( +
+ Default +
+ )}

{preset.title}

@@ -158,12 +249,24 @@ export default function PersonalizePage() {
-
+
+ {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. +

+
+
+ )}