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
-
-
-
+[](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.
+
+
+
+ )}
diff --git a/pickleglass_web/utils/api.ts b/pickleglass_web/utils/api.ts
index 6e6561f..78d0c41 100644
--- a/pickleglass_web/utils/api.ts
+++ b/pickleglass_web/utils/api.ts
@@ -542,7 +542,10 @@ export const updatePreset = async (id: string, data: { title: string, prompt: st
method: 'PUT',
body: JSON.stringify(data),
});
- if (!response.ok) throw new Error('Failed to update preset');
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Failed to update preset: ${response.status} ${errorText}`);
+ }
}
};
diff --git a/src/features/listen/liveSummaryService.js b/src/features/listen/liveSummaryService.js
index 3c03c62..1f367e3 100644
--- a/src/features/listen/liveSummaryService.js
+++ b/src/features/listen/liveSummaryService.js
@@ -524,6 +524,8 @@ async function saveConversationTurn(speaker, transcription) {
}
async function initializeLiveSummarySession(language = 'en') {
+ // Use system environment variable if set, otherwise use the provided language
+ const effectiveLanguage = process.env.OPENAI_TRANSCRIBE_LANG || language || 'en';
if (isInitializingSession) {
console.log('Session initialization already in progress.');
return false;
@@ -620,7 +622,7 @@ async function initializeLiveSummarySession(language = 'en') {
};
const mySttConfig = {
- language: language,
+ language: effectiveLanguage,
callbacks: {
onmessage: handleMyMessage,
onerror: error => console.error('My STT session error:', error.message),
@@ -628,7 +630,7 @@ async function initializeLiveSummarySession(language = 'en') {
},
};
const theirSttConfig = {
- language: language,
+ language: effectiveLanguage,
callbacks: {
onmessage: handleTheirMessage,
onerror: error => console.error('Their STT session error:', error.message),