Full Code of pickle-com/glass for AI

main 71bc3dce7c92 cached
153 files
1.3 MB
364.0k tokens
1197 symbols
1 requests
Download .txt
Showing preview only (1,406K chars total). Download the full file or copy to clipboard to get everything.
Repository: pickle-com/glass
Branch: main
Commit: 71bc3dce7c92
Files: 153
Total size: 1.3 MB

Directory structure:
gitextract_d8n6n_v9/

├── .firebaserc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── assign-on-comment.yml
│       └── build.yml
├── .gitignore
├── .gitmodules
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode/
│   └── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.js
├── docs/
│   ├── DESIGN_PATTERNS.md
│   └── refactor-plan.md
├── electron-builder.yml
├── entitlements.plist
├── firebase.json
├── firestore.indexes.json
├── functions/
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── index.js
│   └── package.json
├── notarize.js
├── package.json
├── pickleglass_web/
│   ├── app/
│   │   ├── activity/
│   │   │   ├── details/
│   │   │   │   └── page.tsx
│   │   │   └── page.tsx
│   │   ├── download/
│   │   │   └── page.tsx
│   │   ├── globals.css
│   │   ├── help/
│   │   │   └── page.tsx
│   │   ├── layout.tsx
│   │   ├── login/
│   │   │   └── page.tsx
│   │   ├── page.tsx
│   │   ├── personalize/
│   │   │   └── page.tsx
│   │   └── settings/
│   │       ├── billing/
│   │       │   └── page.tsx
│   │       ├── page.tsx
│   │       └── privacy/
│   │           └── page.tsx
│   ├── backend_node/
│   │   ├── index.js
│   │   ├── ipcBridge.js
│   │   ├── middleware/
│   │   │   └── auth.js
│   │   └── routes/
│   │       ├── auth.js
│   │       ├── conversations.js
│   │       ├── presets.js
│   │       └── user.js
│   ├── components/
│   │   ├── ClientLayout.tsx
│   │   ├── SearchPopup.tsx
│   │   └── Sidebar.tsx
│   ├── next-env.d.ts
│   ├── next.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── public/
│   │   └── README.md
│   ├── requirements.txt
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── utils/
│       ├── api.ts
│       ├── auth.ts
│       ├── firebase.ts
│       └── firestore.ts
├── preload.js
└── src/
    ├── bridge/
    │   ├── featureBridge.js
    │   ├── internalBridge.js
    │   └── windowBridge.js
    ├── features/
    │   ├── ask/
    │   │   ├── askService.js
    │   │   └── repositories/
    │   │       ├── firebase.repository.js
    │   │       ├── index.js
    │   │       └── sqlite.repository.js
    │   ├── common/
    │   │   ├── ai/
    │   │   │   ├── factory.js
    │   │   │   └── providers/
    │   │   │       ├── anthropic.js
    │   │   │       ├── deepgram.js
    │   │   │       ├── gemini.js
    │   │   │       ├── ollama.js
    │   │   │       ├── openai.js
    │   │   │       └── whisper.js
    │   │   ├── config/
    │   │   │   ├── checksums.js
    │   │   │   ├── config.js
    │   │   │   └── schema.js
    │   │   ├── prompts/
    │   │   │   ├── promptBuilder.js
    │   │   │   └── promptTemplates.js
    │   │   ├── repositories/
    │   │   │   ├── firestoreConverter.js
    │   │   │   ├── ollamaModel/
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── permission/
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── preset/
    │   │   │   │   ├── firebase.repository.js
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── providerSettings/
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── session/
    │   │   │   │   ├── firebase.repository.js
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── user/
    │   │   │   │   ├── firebase.repository.js
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   └── whisperModel/
    │   │   │       └── index.js
    │   │   ├── services/
    │   │   │   ├── authService.js
    │   │   │   ├── databaseInitializer.js
    │   │   │   ├── encryptionService.js
    │   │   │   ├── firebaseClient.js
    │   │   │   ├── localAIManager.js
    │   │   │   ├── migrationService.js
    │   │   │   ├── modelStateService.js
    │   │   │   ├── ollamaService.js
    │   │   │   ├── permissionService.js
    │   │   │   ├── sqliteClient.js
    │   │   │   └── whisperService.js
    │   │   └── utils/
    │   │       └── spawnHelper.js
    │   ├── listen/
    │   │   ├── listenService.js
    │   │   ├── stt/
    │   │   │   ├── repositories/
    │   │   │   │   ├── firebase.repository.js
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   └── sttService.js
    │   │   └── summary/
    │   │       ├── repositories/
    │   │       │   ├── firebase.repository.js
    │   │       │   ├── index.js
    │   │       │   └── sqlite.repository.js
    │   │       └── summaryService.js
    │   ├── settings/
    │   │   ├── repositories/
    │   │   │   ├── firebase.repository.js
    │   │   │   ├── index.js
    │   │   │   └── sqlite.repository.js
    │   │   └── settingsService.js
    │   └── shortcuts/
    │       ├── repositories/
    │       │   ├── index.js
    │       │   └── sqlite.repository.js
    │       └── shortcutsService.js
    ├── index.js
    ├── preload.js
    ├── ui/
    │   ├── app/
    │   │   ├── ApiKeyHeader.js
    │   │   ├── HeaderController.js
    │   │   ├── MainHeader.js
    │   │   ├── PermissionHeader.js
    │   │   ├── PickleGlassApp.js
    │   │   ├── WelcomeHeader.js
    │   │   ├── content.html
    │   │   └── header.html
    │   ├── ask/
    │   │   └── AskView.js
    │   ├── assets/
    │   │   ├── SystemAudioDump
    │   │   ├── logo.icns
    │   │   └── smd.js
    │   ├── listen/
    │   │   ├── ListenView.js
    │   │   ├── audioCore/
    │   │   │   ├── aec.js
    │   │   │   ├── listenCapture.js
    │   │   │   └── renderer.js
    │   │   ├── stt/
    │   │   │   └── SttView.js
    │   │   └── summary/
    │   │       └── SummaryView.js
    │   ├── settings/
    │   │   ├── SettingsView.js
    │   │   └── ShortCutSettingsView.js
    │   └── styles/
    │       └── glass-bypass.css
    └── window/
        ├── smoothMovementManager.js
        ├── windowLayoutManager.js
        └── windowManager.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .firebaserc
================================================
{
  "projects": {
    "default": "pickle-3651a"
  }
}


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug Report
about: Create a report to help us improve
title: "[BUG] "
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment (please complete the following information):**
 - OS: [e.g. macOS, Windows]
 - App Version [e.g. 1.0.0]

**Additional context**
Add any other context about the problem here. 

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature Request
about: Suggest an idea for this project
title: "[FEAT] "
labels: feature
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here. 

================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
---
name: Pull Request
about: Propose a change to the codebase
---

## Summary of Changes

Please provide a brief, high-level summary of the changes in this pull request.

## Related Issue

- Closes #XXX

*Please replace `XXX` with the issue number that this pull request resolves. If it does not resolve a specific issue, please explain why this change is needed.*

## Contributor's Self-Review Checklist

Please check the boxes that apply. This is a reminder of what we look for in a good pull request.

- [ ] I have read the [CONTRIBUTING.md](https://github.com/your-org/your-repo/blob/main/CONTRIBUTING.md) document.
- [ ] My code follows the project's coding style and architectural patterns as described in [DESIGN_PATTERNS.md](https://github.com/your-org/your-repo/blob/main/docs/DESIGN_PATTERNS.md).
- [ ] I have added or updated relevant tests for my changes.
- [ ] I have updated the documentation to reflect my changes (if applicable).
- [ ] My changes have been tested locally and are working as expected.

## Additional Context (Optional)

Add any other context or screenshots about the pull request here. 

================================================
FILE: .github/workflows/assign-on-comment.yml
================================================
name: Assign on Comment

on:
  issue_comment:
    types: [created]

jobs:
  # Job 1: Any contributor can self-assign
  self-assign:
    # Only run if the comment is exactly '/assign'
    if: startsWith(github.event.comment.body, '/assign') && !contains(github.event.comment.body, '@')
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - name: Assign commenter to the issue
        uses: actions/github-script@v7
        with:
          script: |
            // Assign the commenter as the assignee
            await github.rest.issues.addAssignees({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              assignees: [context.actor]
            });
            // Add a rocket (🚀) reaction to indicate success
            await github.rest.reactions.createForIssueComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              comment_id: context.payload.comment.id,
              content: 'rocket'
            });

  # Job 2: Admin can assign others
  assign-others:
    # Only run if the comment starts with '/assign @' and the commenter is in the admin group
    if: startsWith(github.event.comment.body, '/assign @') && contains(fromJson('["OWNER", "COLLABORATOR", "MEMBER"]'), github.event.comment.author_association)
    runs-on: ubuntu-latest
    permissions:
      issues: write
    steps:
      - name: Assign mentioned user
        uses: actions/github-script@v7
        with:
          script: |
            const mention = context.payload.comment.body.split(' ')[1];
            const assignee = mention.substring(1); // Remove '@'
            // Assign the mentioned user as the assignee
            await github.rest.issues.addAssignees({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              assignees: [assignee]
            });
            // Add a thumbs up (+1) reaction to indicate success
            await github.rest.reactions.createForIssueComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              comment_id: context.payload.comment.id,
              content: '+1'
            });

================================================
FILE: .github/workflows/build.yml
================================================
name: Build & Verify

on:
  push:
    branches: [ "main" ] # Runs on every push to main branch

jobs:
  build:
    # Currently runs on macOS only, can add windows-latest later
    runs-on: macos-latest

    steps:
      - name: 🚚 Checkout code
        uses: actions/checkout@v4

      - name: ⚙️ Setup Node.js environment
        uses: actions/setup-node@v4
        with:
          node-version: '20.x' # Node.js version compatible with project
          cache: 'npm' # npm dependency caching for speed improvement

      - name: 📦 Install root dependencies
        run: npm install

      - name: 🌐 Install and build web (Renderer) part
        # Move to pickleglass_web directory and run commands
        working-directory: ./pickleglass_web
        run: |
          npm install
          npm run build

      - name: 🖥️ Build Electron app
        # Run Electron build script from root directory
        run: npm run build

      - name: 🚨 Send failure notification to Slack
        if: failure()
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_CHANNEL: general
          SLACK_TITLE: "🚨 Build Failed"
          SLACK_MESSAGE: "😭 Build failed for `${{ github.repository }}` repo on main branch."
          SLACK_COLOR: 'danger'
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}



================================================
FILE: .gitignore
================================================
# Logs
src/data
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# Webpack
.webpack/

# Vite
.vite/

# Electron-Forge
out/
.specstory
.specstory/

data/pickleglass.db
pickleglass_web/backend/__pycache__/
pickleglass_web/venv/

# Node / JS
node_modules/
npm-debug.log
yarn-error.log

# Database
data/*.db
data/*.db-journal
data/*.db-shm
data/*.db-wal

# Build output
out/
dist/
build/ 

================================================
FILE: .gitmodules
================================================
[submodule "aec"]
	path = aec
	url = https://github.com/samtiz/aec.git


================================================
FILE: .npmrc
================================================
better-sqlite3:ignore-scripts=true
sharp:ignore-scripts=true

================================================
FILE: .prettierignore
================================================
src/ui/assets
node_modules


================================================
FILE: .prettierrc
================================================
{
    "semi": true,
    "tabWidth": 4,
    "printWidth": 150,
    "singleQuote": true,
    "trailingComma": "es5",
    "bracketSpacing": true,
    "arrowParens": "avoid",
    "endOfLine": "lf"
}


================================================
FILE: .vscode/settings.json
================================================
{
    "search.useIgnoreFiles": true
}

================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Glass

Thank you for considering contributing to **Glass by Pickle**! Contributions make the open-source community vibrant, innovative, and collaborative. We appreciate every contribution you make—big or small.

This document guides you through the entire contribution process, from finding an issue to getting your pull request merged.

---

## 🚀 Contribution Workflow

To ensure a smooth and effective workflow, all contributions must go through the following process. Please follow these steps carefully.

### 1. Find or Create an Issue

All work begins with an issue. This is the central place to discuss new ideas and track progress.

-   Browse our existing [**Issues**](https://github.com/pickle-com/glass/issues) to find something you'd like to work on. We recommend looking for issues labeled `good first issue` if you're new!
-   If you have a new idea or find a bug that hasn't been reported, please **create a new issue** using our templates.

### 2. Claim the Issue

To avoid duplicate work, you must claim an issue before you start coding.

-   On the issue you want to work on, leave a comment with the command:
    ```
    /assign
    ```
-   Our GitHub bot will automatically assign the issue to you. Once your profile appears in the **`Assignees`** section on the right, you are ready to start development.

### 3. Fork & Create a Branch

Now it's time to set up your local environment.

1.  **Fork** the repository to your own GitHub account.
2.  **Clone** your forked repository to your local machine.
3.  **Create a new branch** from `main`. A clear branch name is recommended.
    -   For new features: `feat/short-description` (e.g., `feat/user-login-ui`)
    -   For bug fixes: `fix/short-description` (e.g., `fix/header-rendering-bug`)

### 4. Develop

Write your code! As you work, please adhere to our quality standards.

-   **Code Style & Quality**: Our project uses `Prettier` and `ESLint` to maintain a consistent code style.
-   **Architecture & Design Patterns**: All new code must be consistent with the project's architecture. Please read our **[Design Patterns Guide](https://github.com/pickle-com/glass/blob/main/docs/DESIGN_PATTERNS.md)** before making significant changes.

### 5. Create a Pull Request (PR)

Once your work is ready, create a Pull Request to the `main` branch of the original repository.

-   **Fill out the PR Template**: Our template will appear automatically. Please provide a clear summary of your changes.
-   **Link the Issue**: In the PR description, include the line `Closes #XXX` (e.g., `Closes #123`) to link it to the issue you resolved. This is mandatory.
-   **Code Review**: A maintainer will review your code, provide feedback, and merge it.

---

# Developing

### Prerequisites

Ensure the following are installed:
- [Node.js v20.x.x](https://nodejs.org/en/download)
- [Python](https://www.python.org/downloads/)
- (Windows users) [Build Tools for Visual Studio](https://visualstudio.microsoft.com/downloads/)

Ensure you're using Node.js version 20.x.x to avoid build errors with native dependencies.

```bash
# Check your Node.js version
node --version

# If you need to install Node.js 20.x.x, we recommend using nvm:
# curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# nvm install 20
# nvm use 20
```

## Setup and Build

```bash
npm run setup
```
Please ensure that you can make a full production build before pushing code.



## Linting

```bash
npm run lint
```

If you get errors, be sure to fix them before committing.

================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

================================================
FILE: README.md
================================================
<p align="center">
  <a href="https://pickle.com/glass">
   <img src="./public/assets/banner.gif" alt="Logo">
  </a>

  <h1 align="center">Glass by Pickle: Digital Mind Extension 🧠</h1>

</p>


<p align="center">
  <a href="https://discord.gg/UCZH5B5Hpd"><img src="./public/assets/button_dc.png" width="80" alt="Pickle Discord"></a>&ensp;<a href="https://pickle.com"><img src="./public/assets/button_we.png" width="105" alt="Pickle Website"></a>&ensp;<a href="https://x.com/intent/user?screen_name=leinadpark"><img src="./public/assets/button_xe.png" width="109" alt="Follow Daniel"></a>
</p>

> This project is a fork of [CheatingDaddy](https://github.com/sohzm/cheating-daddy) with modifications and enhancements. Thanks to [Soham](https://x.com/soham_btw) and all the open-source contributors who made this possible!

🤖 **Fast, light & open-source**—Glass lives on your desktop, sees what you see, listens in real time, understands your context, and turns every moment into structured knowledge.

💬 **Proactive in meetings**—it surfaces action items, summaries, and answers the instant you need them.

🫥️ **Truly invisible**—never shows up in screen recordings, screenshots, or your dock; no always-on capture or hidden sharing.

To have fun building with us, join our [Discord](https://discord.gg/UCZH5B5Hpd)!

## Instant Launch

⚡️  Skip the setup—launch instantly with our ready-to-run macOS app.  [[Download Here]](https://www.dropbox.com/scl/fi/znid09apxiwtwvxer6oc9/Glass_latest.dmg?rlkey=gwvvyb3bizkl25frhs4k1zwds&st=37q31b4w&dl=1)

## Quick Start (Local Build)

### Prerequisites

First download & install [Python](https://www.python.org/downloads/) and [Node](https://nodejs.org/en/download).
If you are using Windows, you need to also install [Build Tools for Visual Studio](https://visualstudio.microsoft.com/downloads/)

Ensure you're using Node.js version 20.x.x to avoid build errors with native dependencies.

```bash
# Check your Node.js version
node --version

# If you need to install Node.js 20.x.x, we recommend using nvm:
# curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# nvm install 20
# nvm use 20
```

### Installation

```bash
npm run setup
```

## Highlights


### Ask: get answers based on all your previous screen actions & audio

<img width="100%" alt="booking-screen" src="./public/assets/00.gif">

### Meetings: real-time meeting notes, live summaries, session records

<img width="100%" alt="booking-screen" src="./public/assets/01.gif">

### Use your own API key, or sign up to use ours (free)

<img width="100%" alt="booking-screen" src="./public/assets/02.gif">

**Currently Supporting:**
- OpenAI API: Get OpenAI API Key [here](https://platform.openai.com/api-keys)
- Gemini API: Get Gemini API Key [here](https://aistudio.google.com/apikey)
- Local LLM Ollama & Whisper

### Liquid Glass Design (coming soon)

<img width="100%" alt="booking-screen" src="./public/assets/03.gif">

<p>
  for a more detailed guide, please refer to this <a href="https://www.youtube.com/watch?v=qHg3_4bU1Dw">video.</a>
  <i style="color:gray; font-weight:300;">
    we don't waste money on fancy vids; we just code.
  </i>
</p>


## Keyboard Shortcuts

`Ctrl/Cmd + \` : show and hide main window

`Ctrl/Cmd + Enter` : ask AI using all your previous screen and audio

`Ctrl/Cmd + Arrows` : move main window position

## Repo Activity

![Alt](https://repobeats.axiom.co/api/embed/a23e342faafa84fa8797fa57762885d82fac1180.svg "Repobeats analytics image")

## Contributing

We love contributions! Feel free to open issues for bugs or feature requests. For detailed guide, please see our [contributing guide](/CONTRIBUTING.md).
> Currently, we're working on a full code refactor and modularization. Once that's completed, we'll jump into addressing the major issues.

### Contributors

<a href="https://github.com/pickle-com/glass/graphs/contributors">
  <img src="https://contrib.rocks/image?repo=pickle-com/glass" />
</a>

### Help Wanted Issues

We have a list of [help wanted](https://github.com/pickle-com/glass/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22%F0%9F%99%8B%E2%80%8D%E2%99%82%EF%B8%8Fhelp%20wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.


### 🛠 Current Issues & Improvements

| Status | Issue                          | Description                                       |
|--------|--------------------------------|---------------------------------------------------|
| 🚧 WIP      | Liquid Glass                    | Liquid Glass UI for MacOS 26 |

### Changelog

- Jul 5: Now support Gemini, Intel Mac supported
- Jul 6: Full code refactoring has done.
- Jul 7: Now support Claude, LLM/STT model selection
- Jul 8: Now support Windows(beta), Improved AEC by Rust(to seperate mic/system audio), shortcut editing(beta)
- Jul 8: Now support Local LLM & STT, Firebase Data Storage 


## About Pickle

**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)


================================================
FILE: build.js
================================================
const esbuild = require('esbuild');
const path = require('path');

const baseConfig = {
    bundle: true,
    platform: 'browser',
    format: 'esm',
    loader: { '.js': 'jsx' },
    sourcemap: true,
    external: ['electron'],
    define: {
        'process.env.NODE_ENV': `"${process.env.NODE_ENV || 'development'}"`,
    },
};

const entryPoints = [
    { in: 'src/ui/app/HeaderController.js', out: 'public/build/header' },
    { in: 'src/ui/app/PickleGlassApp.js', out: 'public/build/content' },
];

async function build() {
    try {
        console.log('Building renderer process code...');
        await Promise.all(entryPoints.map(point => esbuild.build({
            ...baseConfig,
            entryPoints: [point.in],
            outfile: `${point.out}.js`,
        })));
        console.log('✅ Renderer builds successful!');
    } catch (e) {
        console.error('Renderer build failed:', e);
        process.exit(1);
    }
}

async function watch() {
    try {
        const contexts = await Promise.all(entryPoints.map(point => esbuild.context({
            ...baseConfig,
            entryPoints: [point.in],
            outfile: `${point.out}.js`,
        })));
        
        console.log('Watching for changes...');
        await Promise.all(contexts.map(context => context.watch()));

    } catch (e) {
        console.error('Watch mode failed:', e);
        process.exit(1);
    }
}

if (process.argv.includes('--watch')) {
    watch();
} else {
    build();
} 

================================================
FILE: docs/DESIGN_PATTERNS.md
================================================
# Glass: Design Patterns and Architectural Overview

Welcome to the Glass project! This document is the definitive guide to the architectural patterns, conventions, and design philosophy that guide our development. Adhering to these principles is essential for building new features, maintaining the quality of our codebase, and ensuring a stable, consistent developer experience.

The architecture is designed to be modular, robust, and clear, with a strict separation of concerns.

---

## Core Architectural Principles

These are the fundamental rules that govern the entire application.

1.  **Centralized Data Logic**: All data persistence logic (reading from or writing to a database) is centralized within the **Electron Main Process**. The UI layers (both Electron's renderer and the web dashboard) are forbidden from accessing data sources directly.
2.  **Feature-Based Modularity**: Code is organized by feature (`src/features`) to promote encapsulation and separation of concerns. A new feature should be self-contained within its own directory.
3.  **Dual-Database Repositories**: The data access layer uses a **Repository Pattern** that abstracts away the underlying database. Every repository that handles user data **must** have two implementations: one for the local `SQLite` database and one for the cloud `Firebase` database. Both must expose an identical interface.
4.  **AI Provider Abstraction**: AI model interactions are abstracted using a **Factory Pattern**. To add a new provider (e.g., a new LLM), you only need to create a new provider module that conforms to the base interface in `src/common/ai/providers/` and register it in the `factory.js`.
5.  **Single Source of Truth for Schema**: The schema for the local SQLite database is defined in a single location: `src/common/config/schema.js`. Any change to the database structure **must** be updated here.
6.  **Encryption by Default**: All sensitive user data **must** be encrypted before being persisted to Firebase. This includes, but is not limited to, API keys, conversation titles, transcription text, and AI-generated summaries. This is handled automatically by the `createEncryptedConverter` Firestore helper.

---

## I. Electron Application Architecture (`src/`)

This section details the architecture of the core desktop application.

### 1. Overall Pattern: Service-Repository

The Electron app's logic is primarily built on a **Service-Repository** pattern, with the Views being the HTML/JS files in the `src/app` and `src/features` directories.

-   **Views** (`*.html`, `*View.js`): The UI layer. Views are responsible for rendering the interface and capturing user interactions. They are intentionally kept "dumb" and delegate all significant logic to a corresponding Service.
-   **Services** (`*Service.js`): Services contain the application's business logic. They act as the intermediary between Views and Repositories. For example, `sttService` contains the logic for STT, while `summaryService` handles the logic for generating summaries.
-   **Repositories** (`*.repository.js`): Repositories are responsible for all data access. They are the *only* part of the application that directly interacts with `sqliteClient` or `firebaseClient`.

**Location of Modules:**
-   **Feature-Specific**: If a service or repository is used by only one feature, it should reside within that feature's directory (e.g., `src/features/listen/summary/summaryService.js`).
-   **Common**: If a service or repository is shared across multiple features (like `authService` or `userRepository`), it must be placed in `src/common/services/` or `src/common/repositories/` respectively.

### 2. Data Persistence: The Dual Repository Factory

The application dynamically switches between using the local SQLite database and the cloud-based Firebase Firestore.

-   **SQLite**: The default data store for all users, especially those not logged in. This ensures full offline functionality. The low-level client is `src/common/services/sqliteClient.js`.
-   **Firebase**: Used exclusively for users who are authenticated. This enables data synchronization across devices and with the web dashboard.

The selection mechanism is a sophisticated **Factory and Adapter Pattern** located in the `index.js` file of each repository directory (e.g., `src/common/repositories/session/index.js`).

**How it works:**
1.  **Service Call**: A service makes a call to a high-level repository function, like `sessionRepository.create('ask')`. The service is unaware of the user's state or the underlying database.
2.  **Repository Selection (Factory)**: The `index.js` adapter logic first determines which underlying repository to use. It imports and calls `authService.getCurrentUser()` to check the login state. If the user is logged in, it selects `firebase.repository.js`; otherwise, it defaults to `sqlite.repository.js`.
3.  **UID Injection (Adapter)**: The adapter then retrieves the current user's ID (`uid`) from `authService.getCurrentUserId()`. It injects this `uid` into the actual, low-level repository call (e.g., `firebaseRepository.create(uid, 'ask')`).
4.  **Execution**: The selected repository (`sqlite` or `firebase`) executes the data operation.

This powerful pattern accomplishes two critical goals:
-   It makes the services completely agnostic about the underlying data source.
-   It frees the services from the responsibility of managing and passing user IDs for every database query.

**Visualizing the Data Flow**

```mermaid
graph TD
    subgraph "Electron Main Process"
        A -- User Action --> B[Service Layer];
        B -- Data Request --> C[Repository Factory];
        C -- Check Login Status --> D{Decision};
        D -- No --> E[SQLite Repository];
        D -- Yes --> F[Firebase Repository];
        E -- Access Local DB --> G[(SQLite)];
        F -- Access Cloud DB --> H[(Firebase)];
        G -- Return Data --> B;
        H -- Return Data --> B;
        B -- Update UI --> A;
    end

    style A fill:#D6EAF8,stroke:#3498DB
    style G fill:#E8DAEF,stroke:#8E44AD
    style H fill:#FADBD8,stroke:#E74C3C
```

---

## II. Web Dashboard Architecture (`pickleglass_web/`)

This section details the architecture of the Next.js web application, which serves as the user-facing dashboard for account management and cloud data viewing.

### 1. Frontend, Backend, and Main Process Communication

The web dashboard has a more complex, three-part architecture:

1.  **Next.js Frontend (`app/`):** The React-based user interface.
2.  **Node.js Backend (`backend_node/`):** An Express.js server that acts as an intermediary.
3.  **Electron Main Process (`src/`):** The ultimate authority for all local data access.

Crucially, **the web dashboard's backend cannot access the local SQLite database directly**. It must communicate with the Electron main process to request data.

### 2. The IPC Data Flow

When the web frontend needs data that resides in the local SQLite database (e.g., viewing a non-synced session), it follows this precise flow:

1.  **HTTP Request**: The Next.js frontend makes a standard API call to its own Node.js backend (e.g., `GET /api/conversations`).
2.  **IPC Request**: The Node.js backend receives the HTTP request. It **does not** contain any database logic. Instead, it uses the `ipcRequest` helper from `backend_node/ipcBridge.js`.
3.  **IPC Emission**: `ipcRequest` sends an event to the Electron main process over an IPC channel (`web-data-request`). It passes three things: the desired action (e.g., `'get-sessions'`), a unique channel name for the response, and a payload.
4.  **Main Process Listener**: The Electron main process has a listener (`ipcMain.on('web-data-request', ...)`) that receives this request. It identifies the action and calls the appropriate **Service** or **Repository** to fetch the data from the SQLite database.
5.  **IPC Response**: Once the data is retrieved, the main process sends it back to the web backend using the unique response channel provided in the request.
6.  **HTTP Response**: The web backend's `ipcRequest` promise resolves with the data, and the backend sends it back to the Next.js frontend as a standard JSON HTTP response.

This round-trip ensures our core principle of centralizing data logic in the main process is never violated.

**Visualizing the IPC Data Flow**

```mermaid
sequenceDiagram
    participant FE as Next.js Frontend
    participant BE as Node.js Backend
    participant Main as Electron Main Process

    FE->>+BE: 1. HTTP GET /api/local-data
    Note over BE: Receives local data request
    
    BE->>+Main: 2. ipcRequest('get-data', responseChannel)
    Note over Main: Receives request, fetches data from SQLite<br/>via Service/Repository
    
    Main-->>-BE: 3. ipcResponse on responseChannel (data)
    Note over BE: Receives data, prepares HTTP response
    
    BE-->>-FE: 4. HTTP 200 OK (JSON data)
```

================================================
FILE: docs/refactor-plan.md
================================================
# Refactor Plan: Non-Window Logic Migration from windowManager.js

## Goal
`windowManager.js`를 순수 창 관리 모듈로 만들기 위해 비즈니스 로직을 해당 서비스와 `featureBridge.js`로 이전.

## Steps (based on initial plan)
1. **Shortcuts**: Completed. Logic moved to `shortcutsService.js` and IPC to `featureBridge.js`. Used `internalBridge` for coordination.

2. **Screenshot**: Next. Move `captureScreenshot` function and related IPC handlers from `windowManager.js` to `askService.js` (since it's primarily used there). Update `askService.js` to use its own screenshot method. Add IPC handlers to `featureBridge.js` if needed.

3. **System Permissions**: Create new `permissionService.js` in `src/features/common/services/`. Move all permission-related logic (check, request, open preferences, mark completed, etc.) and IPC handlers from `windowManager.js` to the new service and `featureBridge.js`.

4. **API Key / Model State**: Completely remove from `windowManager.js` (e.g., `setupApiKeyIPC` and helpers). Ensure all usages (e.g., in `askService.js`) directly require and use `modelStateService.js` instead.

## Notes
- Maintain original logic without changes.
- Break circular dependencies if found.
- Use `internalBridge` for inter-module communication where appropriate.
- After each step, verify no errors and test functionality. 

================================================
FILE: electron-builder.yml
================================================
# electron-builder.yml

# The unique application ID
appId: com.pickle.glass

# The user-facing application name
productName: Glass

# Publish configuration for GitHub releases
publish:
    provider: github
    owner: pickle-com
    repo: glass
    releaseType: draft

# Protocols configuration for deep linking
protocols:
    name: PickleGlass Protocol
    schemes: 
        - pickleglass

# List of files to be included in the app package
files:
    - 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: pickleglass_web/out
      to: out

asarUnpack:
    - "src/ui/assets/SystemAudioDump"
    - "**/node_modules/sharp/**/*"
    - "**/node_modules/@img/**/*"

# Windows configuration
win:
    icon: src/ui/assets/logo.ico
    target:
        - target: nsis
          arch: x64
        - target: portable
          arch: x64
    requestedExecutionLevel: asInvoker
    signAndEditExecutable: true
    cscLink: build\certs\glass-dev.pfx
    cscKeyPassword: "${env.CSC_KEY_PASSWORD}"
    signtoolOptions:
      certificateSubjectName: "Glass Dev Code Signing"

# NSIS installer configuration for Windows
nsis:
    oneClick: false
    perMachine: false
    allowToChangeInstallationDirectory: true
    deleteAppDataOnUninstall: true
    createDesktopShortcut: always
    createStartMenuShortcut: true
    shortcutName: Glass

# macOS specific configuration
mac:
    # The application category type
    category: public.app-category.utilities
    # Path to the .icns icon file
    icon: src/ui/assets/logo.icns
    # Minimum macOS version (supports both Intel and Apple Silicon)
    minimumSystemVersion: '11.0'
    hardenedRuntime: true
    entitlements: entitlements.plist
    entitlementsInherit: entitlements.plist
    target:
      - target: dmg
        arch: universal
      - target: zip
        arch: universal


================================================
FILE: entitlements.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.debugger</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
    <key>com.apple.security.device.audio-input</key>
    <true/>
    <key>com.apple.security.device.microphone</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
    <key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
    <array>
      <string>com.deeplink.pickleglass.MachPortRendezvousServer.*</string>
    </array>
    <key>com.apple.security.app-sandbox</key>
    <false/>
  </dict>
</plist>

================================================
FILE: firebase.json
================================================
{
  "functions": [
    {
      "source": "functions",
      "codebase": "pickle-glass",
      "ignore": [
        "node_modules",
        ".git",
        "firebase-debug.log",
        "firebase-debug.*.log",
        "*.local"
      ],
      "predeploy": [
        "npm --prefix \"$RESOURCE_DIR\" run lint"
      ]
    }
  ],
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "hosting": {
    "public": "pickleglass_web/out",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}


================================================
FILE: firestore.indexes.json
================================================
{
  "indexes": [],
  "fieldOverrides": []
} 

================================================
FILE: functions/.eslintrc.js
================================================
module.exports = {
  env: {
    es6: true,
    node: true,
  },
  parserOptions: {
    "ecmaVersion": 2018,
  },
  extends: [
    "eslint:recommended",
    "google",
  ],
  rules: {
    "no-restricted-globals": ["error", "name", "length"],
    "prefer-arrow-callback": "error",
    "quotes": ["error", "double", {"allowTemplateLiterals": true}],
  },
  overrides: [
    {
      files: ["**/*.spec.*"],
      env: {
        mocha: true,
      },
      rules: {},
    },
  ],
  globals: {},
};


================================================
FILE: functions/.gitignore
================================================
node_modules/
*.local

================================================
FILE: functions/index.js
================================================
/**
 * Import function triggers from their respective submodules:
 *
 * const {onCall} = require("firebase-functions/v2/https");
 * const {onDocumentWritten} = require("firebase-functions/v2/firestore");
 *
 * See a full list of supported triggers at https://firebase.google.com/docs/functions
 */

const {onRequest} = require("firebase-functions/v2/https");
const logger = require("firebase-functions/logger");
const admin = require("firebase-admin");
const cors = require("cors")({origin: true});

admin.initializeApp();

// Create and deploy your first functions
// https://firebase.google.com/docs/functions/get-started

// exports.helloWorld = onRequest((request, response) => {
//   logger.info("Hello logs!", {structuredData: true});
//   response.send("Hello from Firebase!");
// });

/**
 * @name pickleGlassAuthCallback
 * @description
 * Validate Firebase ID token and return custom token.
 * On success, return success response with user information.
 * On failure, return error message.
 *
 * @param {object} request - HTTPS request object. { token: "..." } in body.
 * @param {object} response - HTTPS response object.
 */
const authCallbackHandler = (request, response) => {
  cors(request, response, async () => {
    try {
      logger.info("pickleGlassAuthCallback function triggered", {
        body: request.body,
      });

      if (request.method !== "POST") {
        response.status(405).send("Method Not Allowed");
        return;
      }
      if (!request.body || !request.body.token) {
        logger.error("Token is missing from the request body");
        response.status(400).send({
          success: false,
          error: "ID token is required.",
        });
        return;
      }

      const idToken = request.body.token;

      const decodedToken = await admin.auth().verifyIdToken(idToken);
      const uid = decodedToken.uid;

      logger.info("Successfully verified token for UID:", uid);

      const customToken = await admin.auth().createCustomToken(uid);

      response.status(200).send({
        success: true,
        message: "Authentication successful.",
        user: {
          uid: decodedToken.uid,
          email: decodedToken.email,
          name: decodedToken.name,
          picture: decodedToken.picture,
        },
        customToken,
      });
    } catch (error) {
      logger.error("Authentication failed:", error);
      response.status(401).send({
        success: false,
        error: "Invalid token or authentication failed.",
        details: error.message,
      });
    }
  });
};

exports.pickleGlassAuthCallback = onRequest(
    {region: "us-west1"},
    authCallbackHandler,
);


================================================
FILE: functions/package.json
================================================
{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "lint": "eslint . --fix",
    "serve": "firebase emulators:start --only functions",
    "shell": "firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "20"
  },
  "main": "index.js",
  "dependencies": {
    "cors": "^2.8.5",
    "firebase-admin": "^12.7.0",
    "firebase-functions": "^6.0.1"
  },
  "devDependencies": {
    "eslint": "^8.15.0",
    "eslint-config-google": "^0.14.0",
    "firebase-functions-test": "^3.1.0"
  },
  "private": true
}


================================================
FILE: notarize.js
================================================
const { notarize } = require('@electron/notarize');

exports.notarizeApp = async function (context) {
  if (context.electronPlatformName !== 'darwin') {
    return;
  }

  console.log(' notarizing a macOS build!');

  const { appOutDir } = context;
  const appName = context.packager.appInfo.productFilename;
  const appPath = `${appOutDir}/${appName}.app`;

  if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD || !process.env.APPLE_TEAM_ID) {
    throw new Error('APPLE_ID, APPLE_ID_PASSWORD, and APPLE_TEAM_ID environment variables are required for notarization.');
  }

  await notarize({
    appBundleId: 'com.pickle.glass',
    appPath: appPath,
    appleId: process.env.APPLE_ID,
    appleIdPassword: process.env.APPLE_ID_PASSWORD,
    teamId: process.env.APPLE_TEAM_ID,
  });

  console.log(`Successfully notarized ${appName}`);
}; 

================================================
FILE: package.json
================================================
{
    "name": "pickle-glass",
    "productName": "Glass",
    "version": "0.2.4",
    "description": "Cl*ely for Free",
    "main": "src/index.js",
    "scripts": {
        "setup": "npm install && cd pickleglass_web && npm install && npm run build && cd .. && npm start",
        "start": "npm run build:renderer && electron .",
        "package": "npm run build:all && electron-builder --dir",
        "make": "npm run build:renderer && electron-forge make",
        "build": "npm run build:all && electron-builder --config electron-builder.yml --publish never",
        "build:win": "npm run build:all && electron-builder --win --x64 --publish never",
        "publish": "npm run build:all && electron-builder --config electron-builder.yml --publish always",
        "lint": "eslint --ext .ts,.tsx,.js .",
        "postinstall": "electron-builder install-app-deps",
        "build:renderer": "node build.js",
        "build:web": "cd pickleglass_web && npm run build && cd ..",
        "build:all": "npm run build:renderer && npm run build:web",
        "watch:renderer": "node build.js --watch"
    },
    "keywords": [
        "glass",
        "pickle glass",
        "ai assistant",
        "real-time",
        "live summary",
        "contextual ai"
    ],
    "author": {
        "name": "Pickle Team"
    },
    "license": "GPL-3.0",
    "dependencies": {
        "@anthropic-ai/sdk": "^0.56.0",
        "@deepgram/sdk": "^4.9.1",
        "@google/genai": "^1.8.0",
        "@google/generative-ai": "^0.24.1",
        "axios": "^1.10.0",
        "better-sqlite3": "^9.6.0",
        "cors": "^2.8.5",
        "dotenv": "^17.0.0",
        "electron-squirrel-startup": "^1.0.1",
        "electron-store": "^8.2.0",
        "electron-updater": "^6.6.2",
        "express": "^4.18.2",
        "firebase": "^11.10.0",
        "firebase-admin": "^13.4.0",
        "jsonwebtoken": "^9.0.2",
        "keytar": "^7.9.0",
        "node-fetch": "^2.7.0",
        "openai": "^4.70.0",
        "portkey-ai": "^1.10.1",
        "react-hot-toast": "^2.5.2",
        "sharp": "^0.34.2",
        "validator": "^13.11.0",
        "wait-on": "^8.0.3",
        "ws": "^8.18.0"
    },
    "devDependencies": {
        "@electron/fuses": "^1.8.0",
        "@electron/notarize": "^2.5.0",
        "electron": "^30.5.1",
        "electron-builder": "^26.0.12",
        "electron-reloader": "^1.2.3",
        "esbuild": "^0.25.5",
        "prettier": "^3.6.2"
    },
    "optionalDependencies": {
        "electron-liquid-glass": "^1.0.1"
    }
}


================================================
FILE: pickleglass_web/app/activity/details/page.tsx
================================================
'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'

type ConversationItem = (Transcript & { type: 'transcript' }) | (AiMessage & { type: 'ai_message' });

const Section = ({ title, children }: { title: string, children: React.ReactNode }) => (
    <div className="mb-8">
        <h2 className="text-lg font-semibold text-gray-800 mb-3">{title}</h2>
        <div className="text-gray-700 space-y-2">
            {children}
        </div>
    </div>
);

function SessionDetailsContent() {
  const userInfo = useRedirectIfNotAuth() as UserProfile | null;
  const [sessionDetails, setSessionDetails] = useState<SessionDetails | null>(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 {
      await deleteSession(sessionId);
      router.push('/activity');
    } catch (error) {
      alert('Failed to delete activity.');
      setDeleting(false);
      console.error(error);
    }
  };

  if (!userInfo || isLoading) {
    return (
      <div className="min-h-screen bg-[#FDFCF9] 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 session details...</p>
        </div>
      </div>
    );
  }

  if (!sessionDetails) {
    return (
        <div className="min-h-screen bg-[#FDFCF9] flex items-center justify-center">
            <div className="max-w-4xl mx-auto px-8 py-12 text-center">
                <h2 className="text-2xl font-semibold text-gray-900 mb-8">Session Not Found</h2>
                <p className="text-gray-600">The requested session could not be found.</p>
                                    <Link href="/activity" className="mt-4 inline-block text-blue-600 hover:text-blue-800">
                        &larr; Back to Activity
                    </Link>
            </div>
        </div>
    )
  }
  
  const askMessages = sessionDetails.ai_messages || [];

  return (
    <div className="min-h-screen bg-[#FDFCF9] text-gray-800">
        <div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
            <div className="mb-8">
                <Link href="/activity" className="text-sm text-gray-500 hover:text-gray-700 flex items-center">
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
                    </svg>
                    Back
                </Link>
            </div>

            <div className="bg-white p-8 rounded-xl shadow-md border border-gray-100">
                <div className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
                    <div>
                        <h1 className="text-2xl font-bold text-gray-900 mb-2">
                            {sessionDetails.session.title || `Conversation on ${new Date(sessionDetails.session.started_at * 1000).toLocaleDateString()}`}
                        </h1>
                        <div className="flex items-center text-sm text-gray-500 space-x-4">
                            <span>{new Date(sessionDetails.session.started_at * 1000).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}</span>
                            <span>{new Date(sessionDetails.session.started_at * 1000).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })}</span>
                            <span className={`capitalize px-2 py-0.5 rounded-full text-xs font-medium ${sessionDetails.session.session_type === 'listen' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'}`}>
                                {sessionDetails.session.session_type}
                            </span>
                        </div>
                    </div>
                    <button
                        onClick={handleDelete}
                        disabled={deleting}
                        className={`px-4 py-2 rounded text-sm font-medium border border-red-200 text-red-700 bg-red-50 hover:bg-red-100 transition-colors ${deleting ? 'opacity-50 cursor-not-allowed' : ''}`}
                    >
                        {deleting ? 'Deleting...' : 'Delete Activity'}
                    </button>
                </div>

                {sessionDetails.summary && (
                    <Section title="Summary">
                        <p className="text-lg italic text-gray-600 mb-4">"{sessionDetails.summary.tldr}"</p>
                        
                        {sessionDetails.summary.bullet_json && JSON.parse(sessionDetails.summary.bullet_json).length > 0 &&
                            <div className="mt-4">
                                <h3 className="font-semibold text-gray-700 mb-2">Key Points:</h3>
                                <ul className="list-disc list-inside space-y-1 text-gray-600">
                                    {JSON.parse(sessionDetails.summary.bullet_json).map((point: string, index: number) => (
                                        <li key={index}>{point}</li>
                                    ))}
                                </ul>
                            </div>
                        }

                        {sessionDetails.summary.action_json && JSON.parse(sessionDetails.summary.action_json).length > 0 &&
                            <div className="mt-4">
                                <h3 className="font-semibold text-gray-700 mb-2">Action Items:</h3>
                                <ul className="list-disc list-inside space-y-1 text-gray-600">
                                    {JSON.parse(sessionDetails.summary.action_json).map((action: string, index: number) => (
                                        <li key={index}>{action}</li>
                                    ))}
                                </ul>
                            </div>
                        }
                    </Section>
                )}
                
                {sessionDetails.transcripts && sessionDetails.transcripts.length > 0 && (
                    <Section title="Listen: Transcript">
                        <div className="space-y-3">
                            {sessionDetails.transcripts.map((item) => (
                                <p key={item.id} className="text-gray-700">
                                    <span className="font-semibold capitalize">{item.speaker}: </span>
                                    {item.text}
                                </p>
                            ))}
                        </div>
                    </Section>
                )}
                
                {askMessages.length > 0 && (
                    <Section title="Ask: Q&A">
                        <div className="space-y-4">
                            {askMessages.map((item) => (
                                <div key={item.id} className={`p-3 rounded-lg ${item.role === 'user' ? 'bg-gray-100' : 'bg-blue-50'}`}>
                                    <p className="font-semibold capitalize text-sm text-gray-600 mb-1">{item.role === 'user' ? 'You' : 'AI'}</p>
                                    <p className="text-gray-800 whitespace-pre-wrap">{item.content}</p>
                                </div>
                            ))}
                        </div>
                    </Section>
                )}
            </div>
        </div>
    </div>
  );
}

export default function SessionDetailsPage() {
  return (
    <Suspense fallback={
      <div className="min-h-screen bg-[#FDFCF9] 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>
    }>
      <SessionDetailsContent />
    </Suspense>
  );
} 

================================================
FILE: pickleglass_web/app/activity/page.tsx
================================================
'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'

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 fetchSessions = async () => {
    try {
      const fetchedSessions = await getSessions();
      setSessions(fetchedSessions);
    } catch (error) {
      console.error('Failed to fetch conversations:', error)
    } finally {
      setIsLoading(false)
    }
  }

  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>
    )
  }

  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);
    }
  }

  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>
                </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>
  )
} 

================================================
FILE: pickleglass_web/app/download/page.tsx
================================================
'use client'

import { Download, Smartphone, Monitor, Tablet } from 'lucide-react'
import { useRedirectIfNotAuth } from '@/utils/auth'

export default function DownloadPage() {
  const userInfo = useRedirectIfNotAuth()

  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>
    )
  }

  return (
    <div className="p-8">
      <div className="max-w-4xl mx-auto text-center">
        <h1 className="text-3xl font-bold text-gray-900 mb-4">Download pickleglass</h1>
        <p className="text-lg text-gray-600 mb-12">
          Use pickleglass on various platforms
        </p>
        
        <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
          <div className="bg-white rounded-lg border border-gray-200 p-8 hover:shadow-lg transition-shadow">
            <Monitor className="h-16 w-16 text-blue-600 mx-auto mb-4" />
            <h3 className="text-xl font-semibold text-gray-900 mb-2">Desktop</h3>
            <p className="text-gray-600 mb-6">Windows, macOS, Linux</p>
            <button className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg hover:bg-blue-700 transition-colors">
              <Download className="h-5 w-5 inline mr-2" />
              Download Desktop
            </button>
          </div>

          <div className="bg-white rounded-lg border border-gray-200 p-8 hover:shadow-lg transition-shadow">
            <Smartphone className="h-16 w-16 text-green-600 mx-auto mb-4" />
            <h3 className="text-xl font-semibold text-gray-900 mb-2">Mobile</h3>
            <p className="text-gray-600 mb-6">iOS, Android</p>
            <div className="space-y-3">
              <button className="w-full bg-gray-900 text-white py-3 px-6 rounded-lg hover:bg-gray-800 transition-colors">
                App Store
              </button>
              <button className="w-full bg-green-600 text-white py-3 px-6 rounded-lg hover:bg-green-700 transition-colors">
                Google Play
              </button>
            </div>
          </div>

          <div className="bg-white rounded-lg border border-gray-200 p-8 hover:shadow-lg transition-shadow">
            <Tablet className="h-16 w-16 text-purple-600 mx-auto mb-4" />
            <h3 className="text-xl font-semibold text-gray-900 mb-2">Tablet</h3>
            <p className="text-gray-600 mb-6">iPad, Android Tablet</p>
            <button className="w-full bg-purple-600 text-white py-3 px-6 rounded-lg hover:bg-purple-700 transition-colors">
              <Download className="h-5 w-5 inline mr-2" />
              Download Tablet
            </button>
          </div>
        </div>

        <div className="mt-12 p-6 bg-gray-50 rounded-lg">
          <h3 className="text-lg font-semibold text-gray-900 mb-4">System Requirements</h3>
          <div className="grid grid-cols-1 md:grid-cols-3 gap-6 text-left">
            <div>
              <h4 className="font-medium text-gray-900 mb-2">Windows</h4>
              <ul className="text-sm text-gray-600 space-y-1">
                <li>• Windows 10 or later</li>
                <li>• 4GB RAM</li>
                <li>• 100MB Storage</li>
              </ul>
            </div>
            <div>
              <h4 className="font-medium text-gray-900 mb-2">macOS</h4>
              <ul className="text-sm text-gray-600 space-y-1">
                <li>• macOS 11.0 or later</li>
                <li>• 4GB RAM</li>
                <li>• 100MB Storage</li>
              </ul>
            </div>
            <div>
              <h4 className="font-medium text-gray-900 mb-2">Mobile</h4>
              <ul className="text-sm text-gray-600 space-y-1">
                <li>• iOS 14.0 or later</li>
                <li>• Android 8.0 or later</li>
                <li>• 50MB Storage</li>
              </ul>
            </div>
          </div>
        </div>

        <div className="mt-8 text-center">
          <p className="text-gray-600">
            Having issues? Check out our <a href="/help" className="text-blue-600 hover:text-blue-700">Help Center</a>.
          </p>
        </div>
      </div>
    </div>
  )
} 

================================================
FILE: pickleglass_web/app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

@media (prefers-color-scheme: dark) {
  :root {
    --foreground-rgb: 255, 255, 255;
    --background-start-rgb: 0, 0, 0;
    --background-end-rgb: 0, 0, 0;
  }
}

body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(
      to bottom,
      transparent,
      rgb(var(--background-end-rgb))
    )
    rgb(var(--background-start-rgb));
  letter-spacing: -0.03em;
}

@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
} 

================================================
FILE: pickleglass_web/app/help/page.tsx
================================================
'use client'

import { HelpCircle, Book, MessageCircle, Mail } from 'lucide-react'
import { useRedirectIfNotAuth } from '@/utils/auth'

export default function HelpPage() {
  const userInfo = useRedirectIfNotAuth()

  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>
    )
  }

  return (
    <div className="p-8">
      <div className="max-w-4xl mx-auto">
        <h1 className="text-3xl font-bold text-gray-900 mb-8">Help Center</h1>
        
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-white rounded-lg border border-gray-200 p-6">
            <div className="flex items-center mb-4">
              <Book className="h-6 w-6 text-blue-600 mr-3" />
              <h2 className="text-xl font-semibold text-gray-900">Getting Started</h2>
            </div>
            <p className="text-gray-600 mb-4">
              New to pickleglass? Learn about basic features and setup methods.
            </p>
            <ul className="space-y-2 text-sm text-gray-600">
              <li>• Setting up personalized contexts</li>
              <li>• Selecting presets and creating custom contexts</li>
              <li>• Checking activity records</li>
              <li>• Changing settings</li>
            </ul>
          </div>

          <div className="bg-white rounded-lg border border-gray-200 p-6">
            <div className="flex items-center mb-4">
              <HelpCircle className="h-6 w-6 text-green-600 mr-3" />
              <h2 className="text-xl font-semibold text-gray-900">Frequently Asked Questions</h2>
            </div>
            <p className="text-gray-600 mb-4">
              Check out frequently asked questions and answers from other users.
            </p>
            <div className="space-y-3">
              <details className="text-sm">
                <summary className="font-medium text-gray-700 cursor-pointer">
                  How do I change the context?
                </summary>
                <p className="text-gray-600 mt-2 pl-4">
                  On the Personalize page, select a preset or enter a custom context, then click the Save button.
                </p>
              </details>
              <details className="text-sm">
                <summary className="font-medium text-gray-700 cursor-pointer">
                  Where can I check my activity history?
                </summary>
                <p className="text-gray-600 mt-2 pl-4">
                  You can check your past activity records on the My Activity page.
                </p>
              </details>
            </div>
          </div>
        </div>

        <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
          <div className="bg-white rounded-lg border border-gray-200 p-6">
            <div className="flex items-center mb-4">
              <MessageCircle className="h-6 w-6 text-purple-600 mr-3" />
              <h2 className="text-xl font-semibold text-gray-900">Community</h2>
            </div>
            <p className="text-gray-600 mb-4">
              Connect with other users and share tips.
            </p>
            <button className="text-blue-600 hover:text-blue-700 text-sm font-medium">
              Join Community →
            </button>
          </div>

          <div className="bg-white rounded-lg border border-gray-200 p-6">
            <div className="flex items-center mb-4">
              <Mail className="h-6 w-6 text-red-600 mr-3" />
              <h2 className="text-xl font-semibold text-gray-900">Contact Us</h2>
            </div>
            <p className="text-gray-600 mb-4">
              Couldn't find a solution? Contact us directly.
            </p>
            <button className="text-blue-600 hover:text-blue-700 text-sm font-medium">
              Contact via Email →
            </button>
          </div>
        </div>

        <div className="mt-8 p-6 bg-blue-50 rounded-lg">
          <h3 className="text-lg font-semibold text-gray-900 mb-2">💡 Tip</h3>
          <p className="text-gray-700">
            Each context is optimized for different situations. 
            Choose the appropriate preset for your work environment, 
            or create your own custom context!
          </p>
        </div>
      </div>
    </div>
  )
} 

================================================
FILE: pickleglass_web/app/layout.tsx
================================================
import './globals.css'
import { Inter } from 'next/font/google'
import ClientLayout from '@/components/ClientLayout'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'pickleglass - AI Assistant',
  description: 'Personalized AI Assistant for various contexts',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ClientLayout>
          {children}
        </ClientLayout>
      </body>
    </html>
  )
} 

================================================
FILE: pickleglass_web/app/login/page.tsx
================================================
'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'

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
            
            // Maybe we don't need this
            // 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 (
    <div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center">
      <div className="text-center mb-8">
        <h1 className="text-3xl font-bold text-gray-900">Welcome to Pickle Glass</h1>
        <p className="text-gray-600 mt-2">Sign in with your Google account to sync your data across all devices.</p>
        {isElectronMode ? (
          <p className="text-sm text-blue-600 mt-1 font-medium">🔗 Login requested from Electron app</p>
        ) : (
          <p className="text-sm text-gray-500 mt-1">Local mode will run if you don't sign in.</p>
        )}
      </div>
      
      <div className="w-full max-w-sm">
        <div className="bg-white p-8 rounded-lg shadow-md border border-gray-200">
          <button
            onClick={handleGoogleSignIn}
            disabled={isLoading}
            className="w-full flex items-center justify-center gap-3 py-3 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
          >
            <Chrome className="h-5 w-5" />
            <span>{isLoading ? 'Signing in...' : 'Sign in with Google'}</span>
          </button>
          
          <div className="mt-4 text-center">
            <button
              onClick={() => {
                if (isElectronMode) {
                  window.location.href = 'pickleglass://auth-success?uid=default_user&email=contact@pickle.com&displayName=Default%20User'
                } else {
                  router.push('/settings')
                }
              }}
              className="text-sm text-gray-500 hover:text-gray-700 underline"
            >
              Continue in local mode
            </button>
          </div>
        </div>
        
        <p className="text-center text-xs text-gray-500 mt-6">
          By signing in, you agree to our Terms of Service and Privacy Policy.
        </p>
      </div>
    </div>
  )
} 

================================================
FILE: pickleglass_web/app/page.tsx
================================================
'use client'

import { useEffect } from 'react'
import { useRouter } from 'next/navigation'

export default function Home() {
  const router = useRouter()

  useEffect(() => {
    router.push('/personalize')
  }, [router])

  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-blue-600 mx-auto"></div>
        <p className="mt-4 text-gray-600">Loading...</p>
      </div>
    </div>
  )
} 

================================================
FILE: pickleglass_web/app/personalize/page.tsx
================================================
'use client'

import { useState, useEffect } from 'react'
import { ChevronDown, Plus, Copy } from 'lucide-react'
import { getPresets, updatePreset, createPreset, PromptPreset } from '@/utils/api'

export default function PersonalizePage() {
  const [allPresets, setAllPresets] = useState<PromptPreset[]>([]);
  const [selectedPreset, setSelectedPreset] = useState<PromptPreset | null>(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);
        }
      } 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;
    }
    setSelectedPreset(preset);
    setEditorContent(preset.prompt);
    setIsDirty(false);
  };

  const handleEditorChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setEditorContent(e.target.value);
    setIsDirty(true);
  };

  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 
      });

      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 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 (
      <div className="flex items-center justify-center h-full">
        <div className="text-gray-500">Loading...</div>
      </div>
    );
  }

  return (
    <div className="flex flex-col h-full">
      <div className="bg-white border-b border-gray-100">
        <div className="px-8 pt-8 pb-6">
          <div className="flex justify-between items-start">
            <div>
              <p className="text-sm text-gray-500 mb-2">Presets</p>
              <h1 className="text-3xl font-bold text-gray-900">Personalize</h1>
            </div>
            <div className="flex gap-2">
              <button
                onClick={handleCreateNewPreset}
                disabled={saving}
                className="px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 bg-blue-600 text-white hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center gap-2"
              >
                <Plus className="h-4 w-4" />
                New Preset
              </button>
              {selectedPreset && (
                <button
                  onClick={handleDuplicatePreset}
                  disabled={saving}
                  className="px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 bg-green-600 text-white hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed flex items-center gap-2"
                >
                  <Copy className="h-4 w-4" />
                  Duplicate
                </button>
              )}
              <button
                onClick={handleSave}
                disabled={saving || !isDirty || selectedPreset?.is_default === 1}
                className={`px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
                  !isDirty && !saving
                    ? 'bg-gray-500 text-white cursor-default'
                    : saving 
                      ? 'bg-gray-400 text-white cursor-not-allowed' 
                      : 'bg-gray-600 text-white hover:bg-gray-700'
                }`}
              >
                {!isDirty && !saving ? 'Saved' : saving ? 'Saving...' : 'Save'}
              </button>
            </div>
          </div>
        </div>
      </div>

      <div className={`transition-colors duration-300 ${showPresets ? 'bg-gray-50' : 'bg-white'}`}>
        <div className="px-8 py-6">
          <div className="mb-6">
            <button
              onClick={() => setShowPresets(!showPresets)}
              className="flex items-center gap-2 text-gray-600 hover:text-gray-800 text-sm font-medium transition-colors"
            >
              <ChevronDown 
                className={`h-4 w-4 transition-transform duration-200 ${showPresets ? 'rotate-180' : ''}`}
              />
              {showPresets ? 'Hide Presets' : 'Show Presets'}
            </button>
          </div>
          
          {showPresets && (
            <div className="grid grid-cols-5 gap-4 mb-6">
              {allPresets.map((preset) => (
                <div
                  key={preset.id}
                  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 relative
                    ${selectedPreset?.id === preset.id
                      ? 'border-2 border-blue-500 shadow-md'
                      : 'border border-gray-200 hover:border-gray-300'
                    }
                  `}
                >
                  {preset.is_default === 1 && (
                    <div className="absolute top-2 right-2 bg-yellow-100 text-yellow-800 text-xs px-2 py-1 rounded-full">
                      Default
                    </div>
                  )}
                  <h3 className="font-semibold text-gray-900 mb-3 text-center text-sm">
                    {preset.title}
                  </h3>
                  <p className="text-xs text-gray-600 leading-relaxed flex-1 overflow-hidden">
                    {preset.prompt.substring(0, 100) + (preset.prompt.length > 100 ? '...' : '')}
                  </p>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>

      <div className="flex-1 bg-white">
        <div className="h-full px-8 py-6 flex flex-col">
          {selectedPreset?.is_default === 1 && (
            <div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
              <div className="flex items-center gap-2">
                <div className="w-4 h-4 bg-yellow-400 rounded-full"></div>
                <p className="text-sm text-yellow-800">
                  <strong>This is a default preset and cannot be edited.</strong> 
                  Use the "Duplicate" button above to create an editable copy, or create a new preset.
                </p>
              </div>
            </div>
          )}
          <textarea
            value={editorContent}
            onChange={handleEditorChange}
            className="w-full flex-1 text-sm text-gray-900 border-0 resize-none focus:outline-none bg-transparent font-mono leading-relaxed"
            placeholder="Select a preset or type directly..."
            readOnly={selectedPreset?.is_default === 1}
          />
        </div>
      </div>
    </div>
  );
} 

================================================
FILE: pickleglass_web/app/settings/billing/page.tsx
================================================
'use client'

import { useRedirectIfNotAuth } from '@/utils/auth'

export default function BillingPage() {
  const userInfo = useRedirectIfNotAuth()

  if (!userInfo) {
    return (
      <div className="min-h-screen bg-stone-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>
    )
  }

  const tabs = [
    { id: 'profile', name: 'Personal profile', href: '/settings' },
    { id: 'privacy', name: 'Data & privacy', href: '/settings/privacy' },
    { id: 'billing', name: 'Billing', href: '/settings/billing' },
  ]

  return (
    <div className="bg-stone-50 min-h-screen">
      <div className="px-8 py-8">
        <div className="mb-6">
          <p className="text-xs text-gray-500 mb-1">Settings</p>
          <h1 className="text-3xl font-bold text-gray-900">Personal settings</h1>
        </div>
        
        <div className="mb-8">
          <nav className="flex space-x-10">
            {tabs.map((tab) => (
              <a
                key={tab.id}
                href={tab.href}
                className={`pb-4 px-2 border-b-2 font-medium text-sm transition-colors ${
                  tab.id === 'billing'
                    ? 'border-gray-900 text-gray-900'
                    : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
                }`}
              >
                {tab.name}
              </a>
            ))}
          </nav>
        </div>

        <div className="flex items-center justify-center h-96">
          <h2 className="text-8xl font-black bg-gradient-to-r from-black to-gray-500 bg-clip-text text-transparent">
            Cl*ely For Free
          </h2>
        </div>
      </div>
    </div>
  )
} 

================================================
FILE: pickleglass_web/app/settings/page.tsx
================================================
'use client'

import { useState, useEffect } from 'react'
import { Check, ExternalLink, Cloud, HardDrive } from 'lucide-react'
import { useAuth } from '@/utils/auth'
import { 
  UserProfile,
  getUserProfile,
  updateUserProfile,
  checkApiKeyStatus,
  saveApiKey,
  deleteAccount,
  logout
} from '@/utils/api'
import { useRouter } from 'next/navigation'

declare global {
  interface Window {
    ipcRenderer?: any;
  }
}

type Tab = 'profile' | 'privacy' | 'billing'
type BillingCycle = 'monthly' | 'annually'

export default function SettingsPage() {
  const { user: userInfo, isLoading, mode } = useAuth()
  const [activeTab, setActiveTab] = useState<Tab>('profile')
  const [billingCycle, setBillingCycle] = useState<BillingCycle>('monthly')
  const [profile, setProfile] = useState<UserProfile | null>(null)
  const [hasApiKey, setHasApiKey] = useState(false)
  const [apiKeyInput, setApiKeyInput] = useState('')
  const [isSaving, setIsSaving] = useState(false)
  const [displayNameInput, setDisplayNameInput] = useState('')
  const router = useRouter()

  const fetchApiKeyStatus = async () => {
      try {
        const apiKeyStatus = await checkApiKeyStatus()
        setHasApiKey(apiKeyStatus.hasApiKey)
      } catch (error) {
        console.error("Failed to fetch API key status:", error);
      }
  }

  useEffect(() => {
    if (!userInfo) return

    const fetchProfileData = async () => {
      try {
        const userProfile = await getUserProfile()
        setProfile(userProfile)
        setDisplayNameInput(userProfile.display_name)
        await fetchApiKeyStatus();
      } catch (error) {
        console.error("Failed to fetch profile data:", error)
      }
    }
    fetchProfileData()

    if (window.ipcRenderer) {
      window.ipcRenderer.on('api-key-updated', () => {
        console.log('Received api-key-updated event from main process.');
        fetchApiKeyStatus();
      });
    }

    return () => {
      if (window.ipcRenderer) {
        window.ipcRenderer.removeAllListeners('api-key-updated');
      }
    }
  }, [userInfo])

  if (isLoading) {
    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) {
    router.push('/login')
    return null
  }

  const isFirebaseMode = mode === 'firebase'

  const tabs = [
    { id: 'profile' as Tab, name: 'Personal Profile', href: '/settings' },
    { id: 'privacy' as Tab, name: 'Data & Privacy', href: '/settings/privacy' },
    { id: 'billing' as Tab, name: 'Billing', href: '/settings/billing' },
  ]

  const handleSaveApiKey = async () => {
    setIsSaving(true)
    try {
      await saveApiKey(apiKeyInput)
      setHasApiKey(true)
      setApiKeyInput('')
      if (window.ipcRenderer) {
        window.ipcRenderer.invoke('save-api-key', apiKeyInput);
      }
    } catch (error) {
      console.error("Failed to save API key:", error)
    } finally {
      setIsSaving(false)
    }
  }

  const handleUpdateDisplayName = async () => {
    if (!profile || displayNameInput === profile.display_name) return;
    setIsSaving(true);
    try {
        await updateUserProfile({ displayName: displayNameInput });
        setProfile(prev => prev ? { ...prev, display_name: displayNameInput } : null);
    } catch (error) {
        console.error("Failed to update display name:", error);
    } finally {
        setIsSaving(false);
    }
  }

  const handleDeleteAccount = async () => {
    const confirmMessage = isFirebaseMode
      ? "Are you sure you want to delete your account? This action cannot be undone and all data stored in Firebase will be deleted."
      : "Are you sure you want to delete your account? This action cannot be undone and all data will be deleted."
    
    if (window.confirm(confirmMessage)) {
      try {
        await deleteAccount()
        router.push('/login');
      } catch (error) {
        console.error("Failed to delete account:", error)
      }
    }
  }

  const handleLogout = async () => {
    try {
      await logout()
    } catch (error) {
      console.error("Logout failed:", error)
    }
  }

  const renderBillingContent = () => (
    <div className="space-y-8">
      <div className={`p-4 rounded-lg border ${isFirebaseMode ? 'bg-blue-50 border-blue-200' : 'bg-gray-50 border-gray-200'}`}>
        <div className="flex items-center gap-2 mb-2">
          {isFirebaseMode ? (
            <Cloud className="h-5 w-5 text-blue-600" />
          ) : (
            <HardDrive className="h-5 w-5 text-gray-600" />
          )}
          <h3 className={`font-semibold ${isFirebaseMode ? 'text-blue-900' : 'text-gray-900'}`}>
            {isFirebaseMode ? 'Firebase Hosting Mode' : 'Local Execution Mode'}
          </h3>
        </div>
        <p className={`text-sm ${isFirebaseMode ? 'text-blue-700' : 'text-gray-700'}`}>
          {isFirebaseMode 
            ? 'All data is safely stored and synchronized in Firebase Cloud.'
            : 'Data is stored in local database and you can use personal API keys.'
          }
        </p>
      </div>

      <div className="flex gap-2">
        <button
          onClick={() => setBillingCycle('monthly')}
          className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
            billingCycle === 'monthly'
              ? 'bg-gray-200 text-gray-900'
              : 'text-gray-600 hover:text-gray-900'
          }`}
        >
          Monthly
        </button>
        <button
          onClick={() => setBillingCycle('annually')}
          className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
            billingCycle === 'annually'
              ? 'bg-gray-200 text-gray-900'
              : 'text-gray-600 hover:text-gray-900'
          }`}
        >
          Annually
        </button>
      </div>

      <div className="grid grid-cols-3 gap-6">
        <div className="bg-white border border-gray-200 rounded-lg p-6">
          <div className="mb-6">
            <h3 className="text-xl font-semibold text-gray-900 mb-2">Free</h3>
            <div className="text-3xl font-bold text-gray-900">
              $0<span className="text-lg font-normal text-gray-600">/month</span>
            </div>
          </div>
          
          <p className="text-gray-600 mb-6">
            Experience how Pickle Glass works with unlimited responses.
          </p>
          
          <ul className="space-y-3 mb-8">
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Daily unlimited responses</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Unlimited access to free models</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Unlimited text output</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Screen viewing, audio listening</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Custom system prompts</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Community support only</span>
            </li>
          </ul>
          
          <button className="w-full py-2 px-4 bg-gray-200 text-gray-700 rounded-md font-medium">
            Current Plan
          </button>
        </div>

        <div className="bg-white border border-gray-200 rounded-lg p-6 opacity-60">
          <div className="mb-6">
            <h3 className="text-xl font-semibold text-gray-900 mb-2">Pro</h3>
            <div className="text-3xl font-bold text-gray-900">
              $25<span className="text-lg font-normal text-gray-600">/month</span>
            </div>
          </div>
          
          <p className="text-gray-600 mb-6">
            Use latest models, get full response output, and work with custom prompts.
          </p>
          
          <ul className="space-y-3 mb-8">
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Unlimited pro responses</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Unlimited access to latest models</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Full access to conversation dashboard</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">Priority support</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-500" />
              <span className="text-sm text-gray-700">All features from free plan</span>
            </li>
          </ul>
          
          <button className="w-full py-2 px-4 bg-cyan-400 text-white rounded-md font-medium">
            Coming Soon
          </button>
        </div>

        <div className="bg-gray-800 text-white rounded-lg p-6 opacity-60">
          <div className="mb-6">
            <h3 className="text-xl font-semibold mb-2">Enterprise</h3>
            <div className="text-xl font-semibold">Custom</div>
          </div>
          
          <p className="text-gray-300 mb-6">
            Specially crafted for teams that need complete customization.
          </p>
          
          <ul className="space-y-3 mb-8">
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-400" />
              <span className="text-sm text-gray-300">Custom integrations</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-400" />
              <span className="text-sm text-gray-300">User provisioning & role-based access</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-400" />
              <span className="text-sm text-gray-300">Advanced post-call analytics</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-400" />
              <span className="text-sm text-gray-300">Single sign-on</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-400" />
              <span className="text-sm text-gray-300">Advanced security features</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-400" />
              <span className="text-sm text-gray-300">Centralized billing</span>
            </li>
            <li className="flex items-center gap-3">
              <Check className="h-5 w-5 text-green-400" />
              <span className="text-sm text-gray-300">Usage analytics & reporting dashboard</span>
            </li>
          </ul>
          
          <button className="w-full py-2 px-4 bg-gray-600 text-white rounded-md font-medium">
            Coming Soon
          </button>
        </div>
      </div>

      <div className="bg-green-50 border border-green-200 rounded-lg p-6">
        <div className="flex items-center gap-3">
          <Check className="h-6 w-6 text-green-600" />
          <div>
            <h4 className="font-semibold text-green-900">All features are currently free!</h4>
            <p className="text-green-700 text-sm">
              {isFirebaseMode 
                ? 'Enjoy all Pickle Glass features for free in Firebase hosting mode. Pro and Enterprise plans will be released soon with additional premium features.'
                : 'Enjoy all Pickle Glass features for free in local mode. You can use personal API keys or continue using the free system.'
              }
            </p>
          </div>
        </div>
      </div>
    </div>
  )

  const renderTabContent = () => {
    switch (activeTab) {
      case 'billing':
        return renderBillingContent()
      case 'profile':
        return (
          <div className="space-y-6">
            <div className={`p-4 rounded-lg border ${isFirebaseMode ? 'bg-blue-50 border-blue-200' : 'bg-gray-50 border-gray-200'}`}>
              <div className="flex items-center justify-between">
                <div className="flex items-center gap-2">
                  {isFirebaseMode ? (
                    <Cloud className="h-5 w-5 text-blue-600" />
                  ) : (
                    <HardDrive className="h-5 w-5 text-gray-600" />
                  )}
                  <div>
                    <h3 className={`font-semibold ${isFirebaseMode ? 'text-blue-900' : 'text-gray-900'}`}>
                      {isFirebaseMode ? 'Firebase Hosting Mode' : 'Local Execution Mode'}
                    </h3>
                    <p className={`text-sm ${isFirebaseMode ? 'text-blue-700' : 'text-gray-700'}`}>
                      {isFirebaseMode 
                        ? `Logged in with Google account (${userInfo.email})`
                        : 'Running as local user'
                      }
                    </p>
                  </div>
                </div>
                {isFirebaseMode && (
                  <button
                    onClick={handleLogout}
                    className="px-3 py-1 text-sm text-blue-600 hover:text-blue-700 underline"
                  >
                    Logout
                  </button>
                )}
              </div>
            </div>

            <div className="bg-white border border-gray-200 rounded-lg p-6">
              <h3 className="text-lg font-semibold text-gray-900 mb-1">Display Name</h3>
              <p className="text-sm text-gray-600 mb-4">Enter your full name or a display name you're comfortable using.</p>
              <div className="max-w-sm">
                 <input
                    type="text"
                    id="display-name"
                    value={displayNameInput}
                    onChange={(e) => setDisplayNameInput(e.target.value)}
                    className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm text-black"
                    maxLength={32}
                  />
                  <p className="text-xs text-gray-500 mt-2">You can use up to 32 characters.</p>
              </div>
              <div className="mt-4 pt-4 border-t border-gray-200 flex justify-end">
                <button
                    onClick={handleUpdateDisplayName}
                    disabled={isSaving || !displayNameInput || displayNameInput === profile?.display_name}
                    className="px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50"
                  >
                    Update
                  </button>
              </div>
            </div>

            {!isFirebaseMode && (
              <div className="bg-white border border-gray-200 rounded-lg p-6">
                <h3 className="text-lg font-semibold text-gray-900 mb-1">API Key</h3>
                <p className="text-sm text-gray-600 mb-4">
                  If you want to use your own LLM API key, you can add it here. It will be used for all requests made by the local application.
                </p>
                
                <div className="max-w-sm">
                  <label htmlFor="api-key" className="block text-sm font-medium text-gray-700 mb-1">
                    API Key
                  </label>
                  <div className="flex gap-2">
                    <input
                      type="password"
                      id="api-key"
                      value={apiKeyInput}
                      onChange={(e) => setApiKeyInput(e.target.value)}
                      className="flex-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm text-black"
                      placeholder="Enter new API key or existing API key"
                    />
                  </div>
                  {hasApiKey ? (
                    <p className="text-xs text-green-600 mt-2">API key is currently set.</p>
                  ) : (
                    <p className="text-xs text-gray-500 mt-2">No API key set. Using free system.</p>
                  )}
                </div>

                <div className="mt-4 pt-4 border-t border-gray-200 flex justify-end">
                   <button
                      onClick={handleSaveApiKey}
                      disabled={isSaving || !apiKeyInput}
                      className="px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50"
                    >
                      {isSaving ? 'Saving...' : 'Save'}
                    </button>
                </div>
              </div>
            )}

            {(isFirebaseMode || (!isFirebaseMode && !hasApiKey)) && (
               <div className="bg-white border border-red-300 rounded-lg p-6">
                 <h3 className="text-lg font-semibold text-gray-900 mb-1">Delete Account</h3>
                 <p className="text-sm text-gray-600 mb-4">
                   {isFirebaseMode 
                     ? 'Permanently remove your Firebase account and all content. This action cannot be undone, so please proceed carefully.'
                     : 'Permanently remove your personal account and all content from the Pickle Glass platform. This action cannot be undone, so please proceed carefully.'
                   }
                 </p>
                 <div className="mt-4 pt-4 border-t border-gray-200 flex justify-end">
                    <button
                        onClick={handleDeleteAccount}
                        className="px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
                    >
                        Delete
                    </button>
                 </div>
               </div>
            )}
          </div>
        )
      case 'privacy':
        return null
      default:
        return renderBillingContent()
    }
  }

  return (
    <div className="bg-stone-50 min-h-screen">
      <div className="px-8 py-8">
        <div className="mb-6">
          <p className="text-xs text-gray-500 mb-1">Settings</p>
          <h1 className="text-3xl font-bold text-gray-900">Personal Settings</h1>
        </div>
        
        <div className="mb-8">
          <nav className="flex space-x-10">
            {tabs.map((tab) => (
              <a
                key={tab.id}
                href={tab.href}
                onClick={tab.id === 'privacy' ? undefined : () => setActiveTab(tab.id)}
                className={`pb-4 px-2 border-b-2 font-medium text-sm transition-colors ${
                  activeTab === tab.id
                    ? 'border-gray-900 text-gray-900'
                    : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
                }`}
              >
                {tab.name}
              </a>
            ))}
          </nav>
        </div>

        {renderTabContent()}
      </div>
    </div>
  )
} 

================================================
FILE: pickleglass_web/app/settings/privacy/page.tsx
================================================
'use client'

import { ExternalLink } from 'lucide-react'
import { useRedirectIfNotAuth } from '@/utils/auth'

export default function PrivacySettingsPage() {
  const userInfo = useRedirectIfNotAuth()

  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>
    )
  }

  const tabs = [
    { id: 'profile', name: 'Personal profile', href: '/settings' },
    { id: 'privacy', name: 'Data & privacy', href: '/settings/privacy' },
    { id: 'billing', name: 'Billing', href: '/settings/billing' },
  ]

  return (
    <div className="bg-stone-50 min-h-screen">
      <div className="px-8 py-8">
        <div className="mb-6">
          <p className="text-xs text-gray-500 mb-1">Settings</p>
          <h1 className="text-3xl font-bold text-gray-900">Personal settings</h1>
        </div>
        
        <div className="mb-8">
          <nav className="flex space-x-10">
            {tabs.map((tab) => (
              <a
                key={tab.id}
                href={tab.href}
                className={`pb-4 px-2 border-b-2 font-medium text-sm transition-colors ${
                  tab.id === 'privacy'
                    ? 'border-gray-900 text-gray-900'
                    : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
                }`}
              >
                {tab.name}
              </a>
            ))}
          </nav>
        </div>

        <div className="grid grid-cols-2 gap-6">
          <div className="bg-white border border-gray-200 rounded-lg p-6 flex flex-col">
            <div className="flex-grow">
              <h3 className="text-lg font-semibold text-gray-900 mb-3">Privacy Policy</h3>
              <p className="text-gray-500 text-sm leading-relaxed">
                Understand how we collect, use, and protect your personal information.
              </p>
            </div>
            <div className="flex justify-end mt-6">
              <button
                onClick={() => window.open('https://www.pickle.com/ko/privacy-policy', '_blank')}
                className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium transition-colors"
              >
                Privacy
                <ExternalLink className="h-4 w-4" />
              </button>
            </div>
          </div>

          <div className="bg-white border border-gray-200 rounded-lg p-6 flex flex-col">
            <div className="flex-grow">
              <h3 className="text-lg font-semibold text-gray-900 mb-3">Terms of Service</h3>
              <p className="text-gray-500 text-sm leading-relaxed">
                Understand your rights and responsibilities when using our platform.
              </p>
            </div>
            <div className="flex justify-end mt-6">
              <button
                onClick={() => window.open('https://www.pickle.com/ko/terms-of-service', '_blank')}
                className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium transition-colors"
              >
                Terms
                <ExternalLink className="h-4 w-4" />
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
} 

================================================
FILE: pickleglass_web/backend_node/index.js
================================================
const express = require('express');
const cors = require('cors');
// const db = require('./db'); // No longer needed
const { identifyUser } = require('./middleware/auth');

function createApp(eventBridge) {
    const app = express();

    const webUrl = process.env.pickleglass_WEB_URL || 'http://localhost:3000';
    console.log(`🔧 Backend CORS configured for: ${webUrl}`);

    app.use(cors({
        origin: webUrl,
        credentials: true,
    }));

    app.use(express.json());

    app.get('/', (req, res) => {
        res.json({ message: "pickleglass API is running" });
    });

    app.use((req, res, next) => {
        req.bridge = eventBridge;
        next();
    });

    app.use('/api', identifyUser);

    app.use('/api/auth', require('./routes/auth'));
    app.use('/api/user', require('./routes/user'));
    app.use('/api/conversations', require('./routes/conversations'));
    app.use('/api/presets', require('./routes/presets'));

    app.get('/api/sync/status', (req, res) => {
        res.json({
            status: 'online',
            timestamp: new Date().toISOString(),
            version: '1.0.0'
        });
    });

    app.post('/api/desktop/set-user', (req, res) => {
        res.json({
            success: true,
            message: "Direct IPC communication is now used. This endpoint is deprecated.",
            user: req.body,
            deprecated: true
        });
    });

    app.get('/api/desktop/status', (req, res) => {
        res.json({
            connected: true,
            current_user: null,
            communication_method: "IPC",
            file_based_deprecated: true
        });
    });

    return app;
}

module.exports = createApp;


================================================
FILE: pickleglass_web/backend_node/ipcBridge.js
================================================
const crypto = require('crypto');

function ipcRequest(req, channel, payload) {
    return new Promise((resolve, reject) => {
        // Immediately check bridge status and fail if it's not available.
        if (!req.bridge || typeof req.bridge.emit !== 'function') {
            reject(new Error('IPC bridge is not available'));
            return;
        }

        const responseChannel = `${channel}-${crypto.randomUUID()}`;
        
        req.bridge.once(responseChannel, (response) => {
            if (!response) {
                reject(new Error(`No response received from ${channel}`));
                return;
            }
            
            if (response.success) {
                resolve(response.data);
            } else {
                reject(new Error(response.error || `IPC request to ${channel} failed`));
            }
        });

        try {
            req.bridge.emit('web-data-request', channel, responseChannel, payload);
        } catch (error) {
            req.bridge.removeAllListeners(responseChannel);
            reject(new Error(`Failed to emit IPC request: ${error.message}`));
        }
    });
}

module.exports = { ipcRequest }; 

================================================
FILE: pickleglass_web/backend_node/middleware/auth.js
================================================
function identifyUser(req, res, next) {
    const userId = req.get('X-User-ID');

    if (userId) {
        req.uid = userId;
    } else {
        req.uid = 'default_user';
    }
    
    next();
}

module.exports = { identifyUser }; 

================================================
FILE: pickleglass_web/backend_node/routes/auth.js
================================================
const express = require('express');
const router = express.Router();
const { ipcRequest } = require('../ipcBridge');

router.get('/status', async (req, res) => {
    try {
        const user = await ipcRequest(req, 'get-user-profile');
        if (!user) {
            return res.status(500).json({ error: 'Default user not initialized' });
        }
        res.json({ 
            authenticated: true, 
            user: {
                id: user.uid,
                name: user.display_name
            }
        });
    } catch (error) {
        console.error('Failed to get auth status via IPC:', error);
        res.status(500).json({ error: 'Failed to retrieve auth status' });
    }
});

module.exports = router;


================================================
FILE: pickleglass_web/backend_node/routes/conversations.js
================================================
const express = require('express');
const router = express.Router();
const { ipcRequest } = require('../ipcBridge');

router.get('/', async (req, res) => {
    try {
        const sessions = await ipcRequest(req, 'get-sessions');
        res.json(sessions);
    } catch (error) {
        console.error('Failed to get sessions via IPC:', error);
        res.status(500).json({ error: 'Failed to retrieve sessions' });
    }
});

router.post('/', async (req, res) => {
    try {
        const result = await ipcRequest(req, 'create-session', req.body);
        res.status(201).json({ ...result, message: 'Session created successfully' });
    } catch (error) {
        console.error('Failed to create session via IPC:', error);
        res.status(500).json({ error: 'Failed to create session' });
    }
});

router.get('/:session_id', async (req, res) => {
    try {
        const details = await ipcRequest(req, 'get-session-details', req.params.session_id);
        if (!details) {
            return res.status(404).json({ error: 'Session not found' });
        }
        res.json(details);
    } catch (error) {
        console.error(`Failed to get session details via IPC for ${req.params.session_id}:`, error);
        res.status(500).json({ error: 'Failed to retrieve session details' });
    }
});

router.delete('/:session_id', async (req, res) => {
    try {
        await ipcRequest(req, 'delete-session', req.params.session_id);
        res.status(200).json({ message: 'Session deleted successfully' });
    } catch (error) {
        console.error(`Failed to delete session via IPC for ${req.params.session_id}:`, error);
        res.status(500).json({ error: 'Failed to delete session' });
    }
});

// The search functionality will be more complex to move to IPC.
// For now, we can disable it or leave it as is, knowing it's a future task.
router.get('/search', (req, res) => {
    res.status(501).json({ error: 'Search not implemented for IPC bridge yet.' });
});

module.exports = router; 

================================================
FILE: pickleglass_web/backend_node/routes/presets.js
================================================
const express = require('express');
const router = express.Router();
const { ipcRequest } = require('../ipcBridge');

router.get('/', async (req, res) => {
    try {
        const presets = await ipcRequest(req, 'get-presets');
        res.json(presets);
    } catch (error) {
        console.error('Failed to get presets via IPC:', error);
        res.status(500).json({ error: 'Failed to retrieve presets' });
    }
});

router.post('/', async (req, res) => {
    try {
        const result = await ipcRequest(req, 'create-preset', req.body);
        res.status(201).json({ ...result, message: 'Preset created successfully' });
    } catch (error) {
        console.error('Failed to create preset via IPC:', error);
        res.status(500).json({ error: 'Failed to create preset' });
    }
});

router.put('/:id', async (req, res) => {
    try {
        await ipcRequest(req, 'update-preset', { id: req.params.id, data: req.body });
        res.json({ message: 'Preset updated successfully' });
    } catch (error) {
        console.error('Failed to update preset via IPC:', error);
        res.status(500).json({ error: 'Failed to update preset' });
    }
});

router.delete('/:id', async (req, res) => {
    try {
        await ipcRequest(req, 'delete-preset', req.params.id);
        res.json({ message: 'Preset deleted successfully' });
    } catch (error) {
        console.error('Failed to delete preset via IPC:', error);
        res.status(500).json({ error: 'Failed to delete preset' });
    }
});

module.exports = router; 

================================================
FILE: pickleglass_web/backend_node/routes/user.js
================================================
const express = require('express');
const router = express.Router();
const { ipcRequest } = require('../ipcBridge');

router.put('/profile', async (req, res) => {
    try {
        await ipcRequest(req, 'update-user-profile', req.body);
        res.json({ message: 'Profile updated successfully' });
    } catch (error) {
        console.error('Failed to update profile via IPC:', error);
        res.status(500).json({ error: 'Failed to update profile' });
    }
});

router.get('/profile', async (req, res) => {
    try {
        const user = await ipcRequest(req, 'get-user-profile');
        if (!user) return res.status(404).json({ error: 'User not found' });
        res.json(user);
    } catch (error) {
        console.error('Failed to get profile via IPC:', error);
        res.status(500).json({ error: 'Failed to get profile' });
    }
});

router.post('/find-or-create', async (req, res) => {
    try {
        console.log('[API] find-or-create request received:', req.body);
        
        if (!req.body || !req.body.uid) {
            return res.status(400).json({ error: 'User data with uid is required' });
        }
        
        const user = await ipcRequest(req, 'find-or-create-user', req.body);
        console.log('[API] find-or-create response:', user);
        res.status(200).json(user);
    } catch (error) {
        console.error('Failed to find or create user via IPC:', error);
        console.error('Request body:', req.body);
        res.status(500).json({ 
            error: 'Failed to find or create user',
            details: error.message 
        });
    }
});

router.post('/api-key', async (req, res) => {
    try {
        const { apiKey, provider = 'openai' } = req.body;
        await ipcRequest(req, 'save-api-key', { apiKey, provider });
        res.json({ message: 'API key saved successfully' });
    } catch (error) {
        console.error('Failed to save API key via IPC:', error);
        res.status(500).json({ error: 'Failed to save API key' });
    }
});

router.get('/api-key-status', async (req, res) => {
    try {
        const status = await ipcRequest(req, 'check-api-key-status');
        res.json(status);
    } catch (error) {
        console.error('Failed to get API key status via IPC:', error);
        res.status(500).json({ error: 'Failed to get API key status' });
    }
});

router.delete('/profile', async (req, res) => {
    try {
        await ipcRequest(req, 'delete-account');
        res.status(200).json({ message: 'User account and all data deleted successfully.' });
    } catch (error) {
        console.error('Failed to delete user account via IPC:', error);
        res.status(500).json({ error: 'Failed to delete user account' });
    }
});

router.get('/batch', async (req, res) => {
    try {
        const result = await ipcRequest(req, 'get-batch-data', req.query.include);
        res.json(result);
    } catch(error) {
        console.error('Failed to get batch data via IPC:', error);
        res.status(500).json({ error: 'Failed to get batch data' });
    }
});

module.exports = router;


================================================
FILE: pickleglass_web/components/ClientLayout.tsx
================================================
'use client'

import { useState, useEffect } from 'react'
import Sidebar from '@/components/Sidebar'
import SearchPopup from '@/components/SearchPopup'

export default function ClientLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
  const [isSearchOpen, setIsSearchOpen] = useState(false)

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault()
        setIsSearchOpen(true)
      }
    }

    document.addEventListener('keydown', handleKeyDown)
    return () => document.removeEventListener('keydown', handleKeyDown)
  }, [])

  return (
    <div className="flex h-screen">
      <Sidebar 
        isCollapsed={isSidebarCollapsed} 
        onToggle={setIsSidebarCollapsed}
        onSearchClick={() => setIsSearchOpen(true)}
      />
      <main className="flex-1 overflow-auto bg-white">
        {children}
      </main>
      
      <SearchPopup 
        isOpen={isSearchOpen}
        onClose={() => setIsSearchOpen(false)}
      />
    </div>
  )
} 

================================================
FILE: pickleglass_web/components/SearchPopup.tsx
================================================
'use client'

import { useState, useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { Search, X } from 'lucide-react'
import { searchConversations, Session } from '@/utils/api'
import { MessageSquare } from 'lucide-react'

interface SearchPopupProps {
  isOpen: boolean
  onClose: () => void
}

export default function SearchPopup({ isOpen, onClose }: SearchPopupProps) {
  const [searchQuery, setSearchQuery] = useState('')
  const [searchResults, setSearchResults] = useState<Session[]>([])
  const [isLoading, setIsLoading] = useState(false)
  const inputRef = useRef<HTMLInputElement>(null)
  const router = useRouter()

  useEffect(() => {
    if (isOpen && inputRef.current) {
      inputRef.current.focus()
    }
  }, [isOpen])

  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape' && isOpen) {
        onClose()
      }
    }

    document.addEventListener('keydown', handleEscape)
    return () => document.removeEventListener('keydown', handleEscape)
  }, [isOpen, onClose])

  const handleSearch = async (query: string) => {
    if (!query.trim()) {
      setSearchResults([])
      return
    }

    setIsLoading(true)
    try {
      const results = await searchConversations(query)
      setSearchResults(results)
    } catch (error) {
      console.error('Search failed:', error)
      setSearchResults([])
    } finally {
      setIsLoading(false)
    }
  }

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const query = e.target.value
    setSearchQuery(query)
    handleSearch(query)
  }

  const handleBackgroundClick = (e: React.MouseEvent) => {
    if (e.target === e.currentTarget) {
      onClose()
    }
  }

  if (!isOpen) return null

  return (
    <div 
      className="fixed inset-0 bg-black bg-opacity-25 flex items-start justify-center pt-16 z-50"
      onClick={handleBackgroundClick}
    >
      <div className="bg-white rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden">
        <div className="flex items-center px-4 py-3">
          <Search className="h-5 w-5 text-gray-400 mr-3 flex-shrink-0" />
          <input
            ref={inputRef}
            type="text"
            value={searchQuery}
            onChange={handleInputChange}
            placeholder="Search..."
            className="flex-1 text-gray-900 text-base border-0 focus:outline-none placeholder-gray-400 bg-transparent"
          />
          <button
            onClick={onClose}
            className="ml-3 p-1 hover:bg-gray-100 rounded-full flex-shrink-0"
          >
            <X className="h-4 w-4 text-gray-400" />
          </button>
        </div>

        <div className="px-4 py-2 bg-gray-50 border-t border-gray-100">
          <div className="flex items-center text-sm text-gray-600">
            <span>Type</span>
            <span className="mx-2 px-1.5 py-0.5 bg-white border border-gray-200 rounded text-xs font-mono">#</span>
            <span>to access summaries,</span>
            <span className="mx-2 px-1.5 py-0.5 bg-white border border-gray-200 rounded text-xs font-mono">?</span>
            <span>for help.</span>
          </div>
        </div>

        {searchQuery && (
          <div className="max-h-[400px] overflow-y-auto">
            {isLoading ? (
              <div className="p-6 text-center">
                <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto mb-3"></div>
                <p className="text-gray-500 text-sm">Searching...</p>
              </div>
            ) : searchResults.length > 0 ? (
              <div className="divide-y divide-gray-100">
                {searchResults.map((result) => {
                  const timestamp = new Date(result.started_at * 1000).toLocaleString()

                  return (
                    <div
                      key={result.id}
                      className="p-3 hover:bg-gray-50 cursor-pointer transition-colors"
                      onClick={() => {
                        router.push(`/activity/${result.id}`)
                        onClose()
                      }}
                    >
                      <div className="flex items-start gap-3">
                        <MessageSquare className="h-5 w-5 text-gray-400 mt-0.5 shrink-0" />
                        <div className="flex-1 min-w-0">
                          <h3 className="text-sm font-medium text-gray-900 mb-1 truncate">
                            {result.title || 'Untitled Conversation'}
                          </h3>
                          <div className="flex items-center gap-2 mt-2">
                            <span className="text-xs text-gray-500">{timestamp}</span>
                          </div>
                        </div>
                      </div>
                    </div>
                  )
                })}
              </div>
            ) : (
              <div className="p-6 text-center">
                <Search className="h-8 w-8 text-gray-300 mx-auto mb-3" />
                <p className="text-gray-500 text-sm">No results found for "{searchQuery}"</p>
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  )
} 

================================================
FILE: pickleglass_web/components/Sidebar.tsx
================================================
'use client';

import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import Image from 'next/image';
import { useState, createElement, useEffect, useMemo, useCallback, memo } from 'react';
import { Search, Activity, HelpCircle, Download, ChevronDown, User, Shield, Database, CreditCard, LogOut, LucideIcon } from 'lucide-react';
import { logout, UserProfile, checkApiKeyStatus } from '@/utils/api';
import { useAuth } from '@/utils/auth';

const ANIMATION_DURATION = {
    SIDEBAR: 500,
    TEXT: 300,
    SUBMENU: 500,
    ICON_HOVER: 200,
    COLOR_TRANSITION: 200,
    HOVER_SCALE: 200,
} as const;

const DIMENSIONS = {
    SIDEBAR_EXPANDED: 220,
    SIDEBAR_COLLAPSED: 64,
    ICON_SIZE: 18,
    USER_AVATAR_SIZE: 32,
    HEADER_HEIGHT: 64,
} as const;

const ANIMATION_DELAYS = {
    BASE: 0,
    INCREMENT: 50,
    TEXT_BASE: 250,
    SUBMENU_INCREMENT: 30,
} as const;

interface NavigationItem {
    name: string;
    href?: string;
    action?: () => void;
    icon: LucideIcon | string;
    isLucide: boolean;
    hasSubmenu?: boolean;
    ariaLabel?: string;
}

interface SubmenuItem {
    name: string;
    href: string;
    icon: LucideIcon | string;
    isLucide: boolean;
    ariaLabel?: string;
}

interface SidebarProps {
    isCollapsed: boolean;
    onToggle: (collapsed: boolean) => void;
    onSearchClick?: () => void;
}

interface AnimationStyles {
    text: React.CSSProperties;
    submenu: React.CSSProperties;
    sidebarContainer: React.CSSProperties;
    textContainer: React.CSSProperties;
}

const useAnimationStyles = (isCollapsed: boolean) => {
    const [isAnimating, setIsAnimating] = useState(false);

    useEffect(() => {
        setIsAnimating(true);
        const timer = setTimeout(() => setIsAnimating(false), ANIMATION_DURATION.SIDEBAR);
        return () => clearTimeout(timer);
    }, [isCollapsed]);

    const getTextAnimationStyle = useCallback(
        (delay = 0): React.CSSProperties => ({
            willChange: 'opacity',
            transition: `opacity ${ANIMATION_DURATION.TEXT}ms ease-out`,
            transitionDelay: `${delay}ms`,
            opacity: isCollapsed ? 0 : 1,
            pointerEvents: isCollapsed ? 'none' : 'auto',
        }),
        [isCollapsed]
    );

    const getSubmenuAnimationStyle = useCallback(
        (isExpanded: boolean): React.CSSProperties => ({
            willChange: 'opacity, max-height',
            transition: `all ${ANIMATION_DURATION.SUBMENU}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`,
            maxHeight: isCollapsed || !isExpanded ? '0px' : '400px',
            opacity: isCollapsed || !isExpanded ? 0 : 1,
        }),
        [isCollapsed]
    );

    const sidebarContainerStyle: React.CSSProperties = useMemo(
        () => ({
            willChange: 'width',
            transition: `width ${ANIMATION_DURATION.SIDEBAR}ms cubic-bezier(0.4, 0, 0.2, 1)`,
        }),
        []
    );

    const getTextContainerStyle = useCallback(
        (): React.CSSProperties => ({
            width: isCollapsed ? '0px' : '150px',
            overflow: 'hidden',
            transition: `width ${ANIMATION_DURATION.SIDEBAR}ms cubic-bezier(0.4, 0, 0.2, 1)`,
        }),
        [isCollapsed]
    );

    const getUniformTextStyle = useCallback(
        (): React.CSSProperties => ({
            willChange: 'opacity',
            opacity: isCollapsed ? 0 : 1,
            transition: `opacity 300ms ease ${isCollapsed ? '0ms' : '200ms'}`,
            whiteSpace: 'nowrap' as const,
        }),
        [isCollapsed]
    );

    return {
        isAnimating,
        getTextAnimationStyle,
        getSubmenuAnimationStyle,
        sidebarContainerStyle,
        getTextContainerStyle,
        getUniformTextStyle,
    };
};

const IconComponent = memo<{
    icon: LucideIcon | string;
    isLucide: boolean;
    alt: string;
    className?: string;
}>(({ icon, isLucide, alt, className = 'h-[18px] w-[18px] transition-transform duration-200' }) => {
    if (isLucide) {
        return createElement(icon as LucideIcon, { className, 'aria-hidden': true });
    }

    return <Image src={icon as string} alt={alt} width={18} height={18} className={className} loading="lazy" />;
});

IconComponent.displayName = 'IconComponent';

const SidebarComponent = ({ isCollapsed, onToggle, onSearchClick }: SidebarProps) => {
    const pathname = usePathname();
    const router = useRouter();
    const [isSettingsExpanded, setIsSettingsExpanded] = useState(pathname.startsWith('/settings'));
    const { user: userInfo, isLoading: authLoading } = useAuth();
    const [hasApiKey, setHasApiKey] = useState<boolean | null>(null);

    const { isAnimating, getTextAnimationStyle, getSubmenuAnimationStyle, sidebarContainerStyle, getTextContainerStyle, getUniformTextStyle } =
        useAnimationStyles(isCollapsed);

    useEffect(() => {
        checkApiKeyStatus()
            .then(status => setHasApiKey(status.hasApiKey))
            .catch(err => {
                console.error('Failed to check API key status:', err);
                setHasApiKey(null); // Set to null on error
            });
    }, []);

    useEffect(() => {
        if (pathname.startsWith('/settings')) {
            setIsSettingsExpanded(true);
        }
    }, [pathname]);

    const navigation = useMemo<NavigationItem[]>(
        () => [
            {
                name: 'Search',
                action: onSearchClick,
                icon: '/search.svg',
                isLucide: false,
                ariaLabel: 'Open search',
            },
            {
                name: 'My Activity',
                href: '/activity',
                icon: '/activity.svg',
                isLucide: false,
                ariaLabel: 'View my activity',
            },
            {
                name: 'Personalize',
                href: '/personalize',
                icon: '/book.svg',
                isLucide: false,
                ariaLabel: 'Personalization settings',
            },
            {
                name: 'Settings',
                href: '/settings',
                icon: '/setting.svg',
                isLucide: false,
                hasSubmenu: true,
                ariaLabel: 'Settings menu',
            },
        ],
        [onSearchClick]
    );

    const settingsSubmenu = useMemo<SubmenuItem[]>(
        () => [
            { name: 'Personal Profile', href: '/settings', icon: '/user.svg', isLucide: false, ariaLabel: 'Personal profile settings' },
            { name: 'Data & privacy', href: '/settings/privacy', icon: '/privacy.svg', isLucide: false, ariaLabel: 'Data and privacy settings' },
            { name: 'Billing', href: '/settings/billing', icon: '/credit-card.svg', isLucide: false, ariaLabel: 'Billing settings' },
        ],
        []
    );

    const bottomItems = useMemo(
        () => [
            {
                href: 'https://discord.gg/UCZH5B5Hpd',
                icon: '/linkout.svg',
                text: 'Join Discord',
                ariaLabel: 'Help Center (new window)',
            },
            {
                href: 'https://www.dropbox.com/scl/fi/esk4h8z45sryvbremy57v/Pickle_latest.dmg?rlkey=92y535bz6p6gov6vd17x6q53b&st=9kl0annj&dl=1',
                icon: '/download.svg',
                text: 'Download Pickle Camera',
                ariaLabel: 'Download Pickle Camera (new window)',
            },
            {
                href: 'hhttps://www.dropbox.com/scl/fi/znid09apxiwtwvxer6oc9/Glass_latest.dmg?rlkey=gwvvyb3bizkl25frhs4k1zwds&st=37q31b4w&dl=1',
                icon: '/download.svg',
                text: 'Download Pickle Glass',
                ariaLabel: 'Download Pickle Glass (new window)',
            },
        ],
        []
    );

    const toggleSidebar = useCallback(() => {
        onToggle(!isCollapsed);
    }, [isCollapsed, onToggle]);

    const toggleSettings = useCallback(() => {
        if (!pathname.startsWith('/settings')) {
            setIsSettingsExpanded(prev => !prev);
        }
    }, [pathname]);

    const handleLogout = useCallback(async () => {
        try {
            await logout();
        } catch (error) {
            console.error('An error occurred during logout:', error);
        }
    }, []);

    const handleKeyDown = useCallback((event: React.KeyboardEvent, action?: () => void) => {
        if (event.key === 'Enter' || event.key === ' ') {
            event.preventDefault();
            action?.();
        }
    }, []);

    const renderNavigationItem = useCallback(
        (item: NavigationItem, index: number) => {
            const isActive = item.href ? pathname.startsWith(item.href) : false;
            const animationDelay = 0;

            const baseButtonClasses = `
      group flex items-center rounded-[8px] px-[12px] py-[10px] text-[14px] text-[#282828] w-full relative
      transition-colors duration-${ANIMATION_DURATION.COLOR_TRANSITION} ease-out
      focus:outline-none
    `;

            const getStateClasses = (isActive: boolean) =>
                isActive ? 'bg-[#f2f2f2] text-[#282828]' : 'text-[#282828] hover:text-[#282828] hover:bg-[#f7f7f7]';

            if (item.action) {
                return (
                    <li key={item.name}>
                        <button
                            onClick={item.action}
                            onKeyDown={e => handleKeyDown(e, item.action)}
                            className={`${baseButtonClasses} ${getStateClasses(false)}`}
                            title={isCollapsed ? item.name : undefined}
                            aria-label={item.ariaLabel || item.name}
                            style={{ willChange: 'background-color, color' }}
                        >
                            <div className="shrink-0 flex items-center justify-center w-5 h-5">
                                <IconComponent icon={item.icon} isLucide={item.isLucide} alt={`${item.name} icon`} />
                            </div>

                            <div className="ml-[12px] overflow-hidden" style={getTextContainerStyle()}>
                                <span className="block text-left" style={getUniformTextStyle()}>
                                    {item.name}
                                </span>
                            </div>
                        </button>
                    </li>
                );
            }

            if (item.hasSubmenu) {
                return (
                    <li key={item.name}>
                        <button
                            onClick={toggleSettings}
                            onKeyDown={e => handleKeyDown(e, toggleSettings)}
                            className={`${baseButtonClasses} ${getStateClasses(isActive)}`}
                            title={isCollapsed ? item.name : undefined}
                            aria-label={item.ariaLabel || item.name}
                            aria-expanded={isSettingsExpanded}
                            aria-controls="settings-submenu"
                            style={{ willChange: 'background-color, color' }}
                        >
                            <div className="shrink-0 flex items-center justify-center w-5 h-5">
                                <IconComponent icon={item.icon} isLucide={item.isLucide} alt={`${item.name} icon`} />
                            </div>

                            <div className="ml-[12px] overflow-hidden flex items-center" style={getTextContainerStyle()}>
                                <span className="flex-1 text-left" style={getUniformTextStyle()}>
                                    {item.name}
                                </span>
                                <ChevronDown
                                    className="h-3 w-3 ml-1.5 shrink-0"
                                    aria-hidden="true"
                                    style={{
                                        willChange: 'transform, opacity',
                                        transition: `all ${ANIMATION_DURATION.HOVER_SCALE}ms cubic-bezier(0.4, 0, 0.2, 1)`,
                                        transform: `rotate(${isSettingsExpanded ? 180 : 0}deg) ${isCollapsed ? 'scale(0)' : 'scale(1)'}`,
                                        opacity: isCollapsed ? 0 : 1,
                                    }}
                                />
                            </div>
                        </button>

                        <div
                            id="settings-submenu"
                            className="overflow-hidden"
                            style={getSubmenuAnimationStyle(isSettingsExpanded)}
                            role="region"
                            aria-labelledby="settings-button"
                        >
                            <ul className="mt-[4px] space-y-0 pl-[22px]" role="menu">
                                {settingsSubmenu.map((subItem, subIndex) => (
                                    <li key={subItem.name} role="none">
                                        <Link
                                            href={subItem.href}
                                            className={`
                                  group flex items-center rounded-lg px-[12px] py-[8px] text-[13px] gap-x-[9px]
                      focus:outline-none
                                  ${
                                      pathname === subItem.href
                                          ? 'bg-subtle-active-bg text-[#282828]'
                                          : 'text-[#282828] hover:text-[#282828] hover:bg-[#f7f7f7]'
                                  }
                      transition-colors duration-${ANIMATION_DURATION.COLOR_TRANSITION} ease-out
                                `}
                                            style={{
                                                willChange: 'background-color, color',
                                            }}
                                            role="menuitem"
                                            aria-label={subItem.ariaLabel || subItem.name}
                                        >
                                            <IconComponent
                                                icon={subItem.icon}
                                                isLucide={subItem.isLucide}
                                                alt={`${subItem.name} icon`}
                                                className="h-4 w-4 shrink-0"
                                            />
                                            <span className="whitespace-nowrap">{subItem.name}</span>
                                        </Link>
                                    </li>
                                ))}
                                <li role="none">
                                    {isFirebaseUser ? (
                                        <button
                                            onClick={handleLogout}
                                            onKeyDown={e => handleKeyDown(e, handleLogout)}
                                            className={`
                                    group flex items-center rounded-lg px-[12px] py-[8px] text-[13px] gap-x-[9px]
                                    text-red-600 hover:text-red-700 hover:bg-[#f7f7f7] w-full 
                                    transition-colors duration-${ANIMATION_DURATION.COLOR_TRANSITION} ease-out
                                    focus:outline-none
                                  `}
                                            style={{ willChange: 'background-color, color' }}
                                            role="menuitem"
                                            aria-label="Logout"
                                        >
                                            <LogOut className="h-4 w-4 shrink-0" aria-hidden="true" />
                                            <span className="whitespace-nowrap">Logout</span>
                                        </button>
                                    ) : (
                                        <Link
                                            href="/login"
                                            className={`
                                    group flex items-center rounded-lg px-[12px] py-[8px] text-[13px] gap-x-[9px] 
                                    text-[#282828] hover:text-[#282828] hover:bg-[#f7f7f7] w-full 
                                    transition-colors duration-${ANIMATION_DURATION.COLOR_TRANSITION} ease-out
                                    focus:outline-none
                                  `}
                                            style={{ willChange: 'background-color, color' }}
                                            role="menuitem"
                                            aria-label="Login"
                                        >
                                            <LogOut className="h-3.5 w-3.5 shrink-0 transform -scale-x-100" aria-hidden="true" />
                                            <span className="whitespace-nowrap">Login</span>
                                        </Link>
                                    )}
                                </li>
                            </ul>
                        </div>
                    </li>
                );
            }

            return (
                <li key={item.name}>
                    <Link
                        href={item.href || '#'}
                        className={`
                        group flex items-center rounded-[8px] text-[14px] px-[12px] py-[10px] relative
            focus:outline-none
            ${getStateClasses(isActive)}
            transition-colors duration-${ANIMATION_DURATION.COLOR_TRANSITION} ease-out
                        ${isCollapsed ? '' : ''}
                      `}
                        title={isCollapsed ? item.name : undefined}
                        aria-label={item.ariaLabel || item.name}
                        style={{ willChange: 'background-color, color' }}
                    >
                        <div className="shrink-0 flex items-center justify-center w-5 h-5">
                            <IconComponent icon={item.icon} isLucide={item.isLucide} alt={`${item.name} icon`} />
                        </div>

                        <div className="ml-[12px] overflow-hidden" style={getTextContainerStyle()}>
                            <span className="block text-left" style={getUniformTextStyle()}>
                                {item.name}
                            </span>
                        </div>
                    </Link>
                </li>
            );
        },
        [
            pathname,
            isCollapsed,
            isSettingsExpanded,
            toggleSettings,
            handleLogout,
            handleKeyDown,
            getUniformTextStyle,
            getTextContainerStyle,
            getSubmenuAnimationStyle,
            settingsSubmenu,
        ]
    );

    const getUserDisplayName = useCallback(() => {
        if (authLoading) return 'Loading...';
        return userInfo?.display_name || 'Guest';
    }, [userInfo, authLoading]);

    const getUserInitial = useCallback(() => {
        if (authLoading) return 'L';
        return userInfo?.display_name ? userInfo.display_name.charAt(0).toUpperCase() : 'G';
    }, [userInfo, authLoading]);

    const isFirebaseUser = userInfo && userInfo.uid !== 'default_user';

    return (
        <aside
            className={`flex h-full flex-col bg-white border-r py-3 px-2 border-[#e5e5e5] relative ${isCollapsed ? 'w-[60px]' : 'w-[220px]'}`}
            style={sidebarContainerStyle}
            role="navigation"
            aria-label="main navigation"
            aria-expanded={!isCollapsed}
        >
            <header className={`group relative h-6 flex shrink-0 items-center justify-between`}>
                {isCollapsed ? (
                    <Link href="https://pickle.com" target="_blank" rel="noopener noreferrer" className="flex items-center">
                        <Image src="/symbol.svg" alt="Logo" width={20} height={20} className="mx-3 shrink-0" />
                        <button
                            onClick={toggleSidebar}
                            onKeyDown={e => handleKeyDown(e, toggleSidebar)}
                            className={`${
                                isCollapsed ? '' : ''
                            } "absolute inset-0 flex items-center justify-center text-gray-500 hover:text-gray-800 rounded-md opacity-0 scale-90 group-hover:opacity-100 group-hover:scale-100 transition-all duration-300 ease-out focus:outline-none`}
                            aria-label="Open sidebar"
                        >
                            <Image src="/unfold.svg" alt="Open" width={18} height={18} className="h-4.5 w-4.5" />
                        </button>
                    </Link>
                ) : (
                    <>
                        <Link href="https://pickle.com" target="_blank" rel="noopener noreferrer" className="flex items-center">
                            <Image
                                src={isCollapsed ? '/symbol.svg' : '/word.svg'}
                                alt="pickleglass Logo"
                                width={50}
                                height={14}
                                className="mx-3 shrink-0"
                            />
                        </Link>
                        <button
                            onClick={toggleSidebar}
                            onKeyDown={e => handleKeyDown(e, toggleSidebar)}
                            className={`${
                                isCollapsed ? '' : ''
                            } text-gray-500 hover:text-gray-800 p-1 rounded-[4px] hover:bg-[#f7f7f7] h-6 w-6 transition-colors focus:outline-none`}
                            aria-label="Close sidebar"
                        >
                            <Image src="/unfold.svg" alt="Close" width={16} height={16} className="transform rotate-180" />
                        </button>
                    </>
                )}
            </header>

            <nav className="flex flex-1 flex-col pt-8" role="navigation" aria-label="Main menu">
                <ul role="list" className="flex flex-1 flex-col">
                    <li>
                        <ul role="list" className="">
                            {navigation.map(renderNavigationItem)}
                        </ul>
                    </li>
                </ul>

                <button
                    onClick={toggleSidebar}
                    onKeyDown={e => handleKeyDown(e, toggleSidebar)}
                    className={`${
                        isCollapsed ? '' : 'opacity-0'
                    } "absolute inset-0 flex items-center justify-center w-full h-[36px] mb-[8px] rounded-[20px] flex justify-center items-center text-gray-500 hover:text-gray-800 rounded-md scale-90 group-hover:opacity-100 group-hover:scale-100 transition-all duration-300 ease-out focus:outline-none`}
                    aria-label="Open sidebar"
                >
                    <div className="w-[36px] h-[36px] flex items-center justify-center bg-[#f7f7f7] rounded-[20px]">
                        <Image src="/unfold.svg" alt="Open" width={18} height={18} className="h-4.5 w-4.5" />
                    </div>
                </button>

                {!isCollapsed && hasApiKey !== null && (
                    <div className="px-2.5 py-2 text-center">
                        <span className={`text-xs px-2 py-1 rounded-full ${hasApiKey ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'}`}>
                            {hasApiKey ? 'Local running' : 'Pickle Free System'}
                        </span>
                    </div>
                )}

                <div className="mt-auto space-y-[0px]" role="navigation" aria-label="Additional links">
                    {bottomItems.map((item, index) => (
                        <Link
                            key={item.text}
                            href={item.href}
                            target="_blank"
                            rel="noopener noreferrer"
                            className={`
                group flex items-center rounded-[6px] px-[12px] py-[8px] text-[13px] text-[#282828]
                hover:text-[#282828] hover:bg-[#f7f7f7] ${isCollapsed ? '' : 'gap-x-[10px]'}
                transition-colors duration-${ANIMATION_DURATION.COLOR_TRANSITION} ease-out 
                focus:outline-none
              `}
                            title={isCollapsed ? item.text : undefined}
                            aria-label={item.ariaLabel}
                            style={{ willChange: 'background-color, color' }}
                        >
                            <div className=" overflow-hidden">
                                <span className="" style={getUniformTextStyle()}>
                                    {item.text}
                                </span>
                            </div>
                            <div className="shrink-0 flex items-center justify-center w-4 h-4">
                                <IconComponent
                                    icon={item.icon}
                                    isLucide={false}
                                    alt={`${item.text} icon`}
                                    className={`h-[16px] w-[16px] transition-transform duration-${ANIMATION_DURATION.ICON_HOVER}`}
                                />
                            </div>
                        </Link>
                    ))}
                </div>

                <div className="mt-[0px] flex items-center w-full h-[1px] px-[4px] mt-[8px] mb-[8px]">
                    <div className="w-full h-[1px] bg-[#d9d9d9]"></div>
                </div>

                <div
                    className={`mt-[0px] flex items-center ${isCollapsed ? '' : 'gap-x-[10px]'}`}
                    style={{
                        padding: isCollapsed ? '6px 8px' : '6px 8px',
                        justifyContent: isCollapsed ? 'flex-start' : 'flex-start',
                        transition: `all ${ANIMATION_DURATION.SIDEBAR}ms cubic-bezier(0.4, 0, 0.2, 1)`,
                    }}
                    role="region"
                    aria-label="User profile"
                >
                    <div
                        className={`
              h-[30px] w-[30px] rounded-full border border-[#8d8d8d] flex items-center justify-center text-[#282828] text-[13px] 
              shrink-0 cursor-pointer transition-all duration-${ANIMATION_DURATION.ICON_HOVER} 
              hover:bg-[#f7f7f7] focus:outline-none
            `}
                        title={getUserDisplayName()}
                        style={{ willChange: 'background-color, transform' }}
                        tabIndex={0}
                        role="button"
                        aria-label={`User: ${getUserDisplayName()}`}
                        onKeyDown={e =>
                            handleKeyDown(e, () => {
                                if (isFirebaseUser) {
                                    router.push('/settings');
                                } else {
                                    router.push('/login');
                                }
                            })
                        }
                    >
                        {getUserInitial()}
                    </div>

                    <div className="ml-[0px] overflow-hidden" style={getTextContainerStyle()}>
                        <span className="block text-[13px] leading-6 text-[#282828]" style={getUniformTextStyle()}>
                            {getUserDisplayName()}
                        </span>
                    </div>
                </div>
            </nav>
        </aside>
    );
};

const Sidebar = memo(SidebarComponent);
Sidebar.displayName = 'Sidebar';

export default Sidebar;


================================================
FILE: pickleglass_web/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.


================================================
FILE: pickleglass_web/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  output: 'export',

  images: { unoptimized: true },
}

module.exports = nextConfig 

================================================
FILE: pickleglass_web/package.json
================================================
{
  "name": "pickleglass-frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@headlessui/react": "^1.7.17",
    "autoprefixer": "^10.4.16",
    "axios": "^1.6.0",
    "firebase": "^11.10.0",
    "lucide-react": "^0.294.0",
    "next": "^14.2.30",
    "postcss": "^8.4.32",
    "react": "^18",
    "react-dom": "^18",
    "react-hot-toast": "^2.5.2",
    "tailwindcss": "^3.3.0"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.0.4",
    "typescript": "^5"
  }
}


================================================
FILE: pickleglass_web/postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
} 

================================================
FILE: pickleglass_web/public/README.md
================================================
# Public Assets

This folder contains static files.

## Logo Image

**@symbol.svg** - Logo image for the pickleglass application

### Requirements:
- Filename: `symbol.png`
- Recommended size: 32x32px or 64x64px  
- Format: PNG
- Transparent background recommended

### Usage:
- Used as logo in sidebar header
- Loaded optimized through Next.js Image component

Currently there is a placeholder file, please replace it with the actual logo image. 

================================================
FILE: pickleglass_web/requirements.txt
================================================
fastapi==0.104.1
uvicorn[standard]==0.24.0
aiosqlite==0.19.0
pydantic==2.5.0
python-dotenv==1.0.0
python-multipart==0.0.6
bcrypt==4.1.2
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-dateutil==2.8.2
email-validator==2.1.0
fastapi-cors==0.0.6
PyJWT==2.8.0 

================================================
FILE: pickleglass_web/tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#64748b',
        accent: '#06b6d4',
        'subtle-bg': '#f8f7f4',
        'subtle-active-bg': '#e7e5e4',
      },
    },
  },
  plugins: [],
} 

================================================
FILE: pickleglass_web/tsconfig.json
================================================
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "es6"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
} 

================================================
FILE: pickleglass_web/utils/api.ts
================================================
import { auth as firebaseAuth } from './firebase';
import { 
  FirestoreUserService, 
  FirestoreSessionService, 
  FirestoreTranscriptService, 
  FirestoreAiMessageService, 
  FirestoreSummaryService, 
  FirestorePromptPresetService,
  FirestoreSession,
  FirestoreTranscript,
  FirestoreAiMessage,
  FirestoreSummary,
  FirestorePromptPreset
} from './firestore';
import { Timestamp } from 'firebase/firestore';

export interface UserProfile {
  uid: string;
  display_name: string;
  email: string;
}

export interface Session {
  id: string;
  uid: string;
  title: string;
  session_type: string;
  started_at: number;
  ended_at?: number;
  sync_state: 'clean' | 'dirty';
  updated_at: number;
}

export interface Transcript {
  id: string;
  session_id: string;
  start_at: number;
  end_at?: number;
  speaker?: string;
  text: string;
  lang?: string;
  created_at: number;
  sync_state: 'clean' | 'dirty';
}

export interface AiMessage {
  id: string;
  session_id: string;
  sent_at: number;
  role: 'user' | 'assistant';
  content: string;
  tokens?: number;
  model?: string;
  created_at: number;
  sync_state: 'clean' | 'dirty';
}

export interface Summary {
  session_id: string;
  generated_at: number;
  model?: string;
  text: string;
  tldr: string;
  bullet_json: string;
  action_json: string;
  tokens_used?: number;
  updated_at: number;
  sync_state: 'clean' | 'dirty';
}

export interface PromptPreset {
  id: string;
  uid: string;
  title: string;
  prompt: string;
  is_default: 0 | 1;
  created_at: number;
  sync_state: 'clean' | 'dirty';
}

export interface SessionDetails {
    session: Session;
    transcripts: Transcript[];
    ai_messages: AiMessage[];
    summary: Summary | null;
}


const isFirebaseMode = (): boolean => {
  // The web frontend can no longer directly access Firebase state,
  // so we assume communication always goes through the backend API.
  // In the future, we can create an endpoint like /api/auth/status 
  // in the backend to retrieve the authentication state.
  return false;
};

const timestampToUnix = (timestamp: Timestamp): number => {
  return timestamp.seconds * 1000 + Math.floor(timestamp.nanoseconds / 1000000);
};

const unixToTimestamp = (unix: number): Timestamp => {
  return Timestamp.fromMillis(unix);
};

const convertFirestoreSession = (session: { id: string } & FirestoreSession, uid: string): Session => {
  return {
    id: session.id,
    uid,
    title: session.title,
    session_type: session.session_type,
    started_at: timestampToUnix(session.startedAt),
    ended_at: session.endedAt ? timestampToUnix(session.endedAt) : undefined,
    sync_state: 'clean',
    updated_at: timestampToUnix(session.startedAt)
  };
};

const convertFirestoreTranscript = (transcript: { id: string } & FirestoreTranscript): Transcript => {
  return {
    id: transcript.id,
    session_id: '',
    start_at: timestampToUnix(transcript.startAt),
    end_at: transcript.endAt ? timestampToUnix(transcript.endAt) : undefined,
    speaker: transcript.speaker,
    text: transcript.text,
    lang: transcript.lang,
    created_at: timestampToUnix(transcript.createdAt),
    sync_state: 'clean'
  };
};

const convertFirestoreAiMessage = (message: { id: string } & FirestoreAiMessage): AiMessage => {
  return {
    id: message.id,
    session_id: '',
    sent_at: timestampToUnix(message.sentAt),
    role: message.role,
    content: message.content,
    tokens: message.tokens,
    model: message.model,
    created_at: timestampToUnix(message.createdAt),
    sync_state: 'clean'
  };
};

const convertFirestoreSummary = (summary: FirestoreSummary, sessionId: string): Summary => {
  return {
    session_id: sessionId,
    generated_at: timestampToUnix(summary.generatedAt),
    model: summary.model,
    text: summary.text,
    tldr: summary.tldr,
    bullet_json: JSON.stringify(summary.bulletPoints),
    action_json: JSON.stringify(summary.actionItems),
    tokens_used: summary.tokensUsed,
    updated_at: timestampToUnix(summary.generatedAt),
    sync_state: 'clean'
  };
};

const convertFirestorePreset = (preset: { id: string } & FirestorePromptPreset, uid: string): PromptPreset => {
  return {
    id: preset.id,
    uid,
    title: preset.title,
    prompt: preset.prompt,
    is_default: preset.isDefault ? 1 : 0,
    created_at: timestampToUnix(preset
Download .txt
gitextract_d8n6n_v9/

├── .firebaserc
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       ├── assign-on-comment.yml
│       └── build.yml
├── .gitignore
├── .gitmodules
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode/
│   └── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.js
├── docs/
│   ├── DESIGN_PATTERNS.md
│   └── refactor-plan.md
├── electron-builder.yml
├── entitlements.plist
├── firebase.json
├── firestore.indexes.json
├── functions/
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── index.js
│   └── package.json
├── notarize.js
├── package.json
├── pickleglass_web/
│   ├── app/
│   │   ├── activity/
│   │   │   ├── details/
│   │   │   │   └── page.tsx
│   │   │   └── page.tsx
│   │   ├── download/
│   │   │   └── page.tsx
│   │   ├── globals.css
│   │   ├── help/
│   │   │   └── page.tsx
│   │   ├── layout.tsx
│   │   ├── login/
│   │   │   └── page.tsx
│   │   ├── page.tsx
│   │   ├── personalize/
│   │   │   └── page.tsx
│   │   └── settings/
│   │       ├── billing/
│   │       │   └── page.tsx
│   │       ├── page.tsx
│   │       └── privacy/
│   │           └── page.tsx
│   ├── backend_node/
│   │   ├── index.js
│   │   ├── ipcBridge.js
│   │   ├── middleware/
│   │   │   └── auth.js
│   │   └── routes/
│   │       ├── auth.js
│   │       ├── conversations.js
│   │       ├── presets.js
│   │       └── user.js
│   ├── components/
│   │   ├── ClientLayout.tsx
│   │   ├── SearchPopup.tsx
│   │   └── Sidebar.tsx
│   ├── next-env.d.ts
│   ├── next.config.js
│   ├── package.json
│   ├── postcss.config.js
│   ├── public/
│   │   └── README.md
│   ├── requirements.txt
│   ├── tailwind.config.js
│   ├── tsconfig.json
│   └── utils/
│       ├── api.ts
│       ├── auth.ts
│       ├── firebase.ts
│       └── firestore.ts
├── preload.js
└── src/
    ├── bridge/
    │   ├── featureBridge.js
    │   ├── internalBridge.js
    │   └── windowBridge.js
    ├── features/
    │   ├── ask/
    │   │   ├── askService.js
    │   │   └── repositories/
    │   │       ├── firebase.repository.js
    │   │       ├── index.js
    │   │       └── sqlite.repository.js
    │   ├── common/
    │   │   ├── ai/
    │   │   │   ├── factory.js
    │   │   │   └── providers/
    │   │   │       ├── anthropic.js
    │   │   │       ├── deepgram.js
    │   │   │       ├── gemini.js
    │   │   │       ├── ollama.js
    │   │   │       ├── openai.js
    │   │   │       └── whisper.js
    │   │   ├── config/
    │   │   │   ├── checksums.js
    │   │   │   ├── config.js
    │   │   │   └── schema.js
    │   │   ├── prompts/
    │   │   │   ├── promptBuilder.js
    │   │   │   └── promptTemplates.js
    │   │   ├── repositories/
    │   │   │   ├── firestoreConverter.js
    │   │   │   ├── ollamaModel/
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── permission/
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── preset/
    │   │   │   │   ├── firebase.repository.js
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── providerSettings/
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── session/
    │   │   │   │   ├── firebase.repository.js
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   ├── user/
    │   │   │   │   ├── firebase.repository.js
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   └── whisperModel/
    │   │   │       └── index.js
    │   │   ├── services/
    │   │   │   ├── authService.js
    │   │   │   ├── databaseInitializer.js
    │   │   │   ├── encryptionService.js
    │   │   │   ├── firebaseClient.js
    │   │   │   ├── localAIManager.js
    │   │   │   ├── migrationService.js
    │   │   │   ├── modelStateService.js
    │   │   │   ├── ollamaService.js
    │   │   │   ├── permissionService.js
    │   │   │   ├── sqliteClient.js
    │   │   │   └── whisperService.js
    │   │   └── utils/
    │   │       └── spawnHelper.js
    │   ├── listen/
    │   │   ├── listenService.js
    │   │   ├── stt/
    │   │   │   ├── repositories/
    │   │   │   │   ├── firebase.repository.js
    │   │   │   │   ├── index.js
    │   │   │   │   └── sqlite.repository.js
    │   │   │   └── sttService.js
    │   │   └── summary/
    │   │       ├── repositories/
    │   │       │   ├── firebase.repository.js
    │   │       │   ├── index.js
    │   │       │   └── sqlite.repository.js
    │   │       └── summaryService.js
    │   ├── settings/
    │   │   ├── repositories/
    │   │   │   ├── firebase.repository.js
    │   │   │   ├── index.js
    │   │   │   └── sqlite.repository.js
    │   │   └── settingsService.js
    │   └── shortcuts/
    │       ├── repositories/
    │       │   ├── index.js
    │       │   └── sqlite.repository.js
    │       └── shortcutsService.js
    ├── index.js
    ├── preload.js
    ├── ui/
    │   ├── app/
    │   │   ├── ApiKeyHeader.js
    │   │   ├── HeaderController.js
    │   │   ├── MainHeader.js
    │   │   ├── PermissionHeader.js
    │   │   ├── PickleGlassApp.js
    │   │   ├── WelcomeHeader.js
    │   │   ├── content.html
    │   │   └── header.html
    │   ├── ask/
    │   │   └── AskView.js
    │   ├── assets/
    │   │   ├── SystemAudioDump
    │   │   ├── logo.icns
    │   │   └── smd.js
    │   ├── listen/
    │   │   ├── ListenView.js
    │   │   ├── audioCore/
    │   │   │   ├── aec.js
    │   │   │   ├── listenCapture.js
    │   │   │   └── renderer.js
    │   │   ├── stt/
    │   │   │   └── SttView.js
    │   │   └── summary/
    │   │       └── SummaryView.js
    │   ├── settings/
    │   │   ├── SettingsView.js
    │   │   └── ShortCutSettingsView.js
    │   └── styles/
    │       └── glass-bypass.css
    └── window/
        ├── smoothMovementManager.js
        ├── windowLayoutManager.js
        └── windowManager.js
Download .txt
SYMBOL INDEX (1197 symbols across 100 files)

FILE: build.js
  function build (line 21) | async function build() {
  function watch (line 36) | async function watch() {

FILE: pickleglass_web/app/activity/details/page.tsx
  type ConversationItem (line 16) | type ConversationItem = (Transcript & { type: 'transcript' }) | (AiMessa...
  function SessionDetailsContent (line 27) | function SessionDetailsContent() {
  function SessionDetailsPage (line 188) | function SessionDetailsPage() {

FILE: pickleglass_web/app/activity/page.tsx
  function ActivityPage (line 13) | function ActivityPage() {

FILE: pickleglass_web/app/download/page.tsx
  function DownloadPage (line 6) | function DownloadPage() {

FILE: pickleglass_web/app/help/page.tsx
  function HelpPage (line 6) | function HelpPage() {

FILE: pickleglass_web/app/layout.tsx
  function RootLayout (line 12) | function RootLayout({

FILE: pickleglass_web/app/login/page.tsx
  function LoginPage (line 9) | function LoginPage() {

FILE: pickleglass_web/app/page.tsx
  function Home (line 6) | function Home() {

FILE: pickleglass_web/app/personalize/page.tsx
  function PersonalizePage (line 7) | function PersonalizePage() {

FILE: pickleglass_web/app/settings/billing/page.tsx
  function BillingPage (line 5) | function BillingPage() {

FILE: pickleglass_web/app/settings/page.tsx
  type Window (line 18) | interface Window {
  type Tab (line 23) | type Tab = 'profile' | 'privacy' | 'billing'
  type BillingCycle (line 24) | type BillingCycle = 'monthly' | 'annually'
  function SettingsPage (line 26) | function SettingsPage() {

FILE: pickleglass_web/app/settings/privacy/page.tsx
  function PrivacySettingsPage (line 6) | function PrivacySettingsPage() {

FILE: pickleglass_web/backend_node/index.js
  function createApp (line 6) | function createApp(eventBridge) {

FILE: pickleglass_web/backend_node/ipcBridge.js
  function ipcRequest (line 3) | function ipcRequest(req, channel, payload) {

FILE: pickleglass_web/backend_node/middleware/auth.js
  function identifyUser (line 1) | function identifyUser(req, res, next) {

FILE: pickleglass_web/components/ClientLayout.tsx
  function ClientLayout (line 7) | function ClientLayout({

FILE: pickleglass_web/components/SearchPopup.tsx
  type SearchPopupProps (line 9) | interface SearchPopupProps {
  function SearchPopup (line 14) | function SearchPopup({ isOpen, onClose }: SearchPopupProps) {

FILE: pickleglass_web/components/Sidebar.tsx
  constant ANIMATION_DURATION (line 11) | const ANIMATION_DURATION = {
  constant DIMENSIONS (line 20) | const DIMENSIONS = {
  constant ANIMATION_DELAYS (line 28) | const ANIMATION_DELAYS = {
  type NavigationItem (line 35) | interface NavigationItem {
  type SubmenuItem (line 45) | interface SubmenuItem {
  type SidebarProps (line 53) | interface SidebarProps {
  type AnimationStyles (line 59) | interface AnimationStyles {

FILE: pickleglass_web/utils/api.ts
  type UserProfile (line 17) | interface UserProfile {
  type Session (line 23) | interface Session {
  type Transcript (line 34) | interface Transcript {
  type AiMessage (line 46) | interface AiMessage {
  type Summary (line 58) | interface Summary {
  type PromptPreset (line 71) | interface PromptPreset {
  type SessionDetails (line 81) | interface SessionDetails {
  constant API_ORIGIN (line 174) | let API_ORIGIN = process.env.NODE_ENV === 'development'
  type BatchData (line 541) | interface BatchData {

FILE: pickleglass_web/utils/firestore.ts
  type FirestoreUserProfile (line 19) | interface FirestoreUserProfile {
  type FirestoreSession (line 25) | interface FirestoreSession {
  type FirestoreTranscript (line 32) | interface FirestoreTranscript {
  type FirestoreAiMessage (line 41) | interface FirestoreAiMessage {
  type FirestoreSummary (line 50) | interface FirestoreSummary {
  type FirestorePromptPreset (line 60) | interface FirestorePromptPreset {
  class FirestoreUserService (line 67) | class FirestoreUserService {
    method createUser (line 68) | static async createUser(uid: string, profile: Omit<FirestoreUserProfil...
    method getUser (line 76) | static async getUser(uid: string): Promise<FirestoreUserProfile | null> {
    method updateUser (line 82) | static async updateUser(uid: string, updates: Partial<FirestoreUserPro...
    method deleteUser (line 87) | static async deleteUser(uid: string) {
  class FirestoreSessionService (line 121) | class FirestoreSessionService {
    method createSession (line 122) | static async createSession(uid: string, session: Omit<FirestoreSession...
    method getSession (line 131) | static async getSession(uid: string, sessionId: string): Promise<Fires...
    method getSessions (line 137) | static async getSessions(uid: string): Promise<Array<{ id: string } & ...
    method updateSession (line 148) | static async updateSession(uid: string, sessionId: string, updates: Pa...
    method deleteSession (line 153) | static async deleteSession(uid: string, sessionId: string) {
  class FirestoreTranscriptService (line 174) | class FirestoreTranscriptService {
    method addTranscript (line 175) | static async addTranscript(uid: string, sessionId: string, transcript:...
    method getTranscripts (line 184) | static async getTranscripts(uid: string, sessionId: string): Promise<A...
  class FirestoreAiMessageService (line 196) | class FirestoreAiMessageService {
    method addAiMessage (line 197) | static async addAiMessage(uid: string, sessionId: string, message: Omi...
    method getAiMessages (line 206) | static async getAiMessages(uid: string, sessionId: string): Promise<Ar...
  class FirestoreSummaryService (line 218) | class FirestoreSummaryService {
    method setSummary (line 219) | static async setSummary(uid: string, sessionId: string, summary: Fires...
    method getSummary (line 224) | static async getSummary(uid: string, sessionId: string): Promise<Fires...
  class FirestorePromptPresetService (line 231) | class FirestorePromptPresetService {
    method createPreset (line 232) | static async createPreset(uid: string, preset: Omit<FirestorePromptPre...
    method getPresets (line 241) | static async getPresets(uid: string): Promise<Array<{ id: string } & F...
    method updatePreset (line 252) | static async updatePreset(uid: string, presetId: string, updates: Part...
    method deletePreset (line 257) | static async deletePreset(uid: string, presetId: string) {

FILE: src/bridge/featureBridge.js
  method initialize (line 18) | initialize() {
  method sendAskProgress (line 236) | sendAskProgress(win, progress) {

FILE: src/bridge/windowBridge.js
  method initialize (line 6) | initialize() {
  method notifyFocusChange (line 31) | notifyFocusChange(win, isFocused) {

FILE: src/features/ask/askService.js
  function captureScreenshot (line 38) | async function captureScreenshot(options = {}) {
  class AskService (line 126) | class AskService {
    method constructor (line 127) | constructor() {
    method _broadcastState (line 140) | _broadcastState() {
    method toggleAskButton (line 147) | async toggleAskButton(inputScreenOnly = false) {
    method closeAskWindow (line 178) | async closeAskWindow () {
    method _formatConversationForPrompt (line 206) | _formatConversationForPrompt(conversationTexts) {
    method sendMessage (line 218) | async sendMessage(userPrompt, conversationHistoryRaw=[]) {
    method _processStream (line 370) | async _processStream(reader, askWin, sessionId, signal) {
    method _isMultimodalError (line 432) | _isMultimodalError(error) {

FILE: src/features/ask/repositories/firebase.repository.js
  function aiMessagesCol (line 7) | function aiMessagesCol(sessionId) {
  function addAiMessage (line 13) | async function addAiMessage({ uid, sessionId, role, content, model = 'un...
  function getAllAiMessagesBySessionId (line 29) | async function getAllAiMessagesBySessionId(sessionId) {

FILE: src/features/ask/repositories/index.js
  function getBaseRepository (line 5) | function getBaseRepository() {

FILE: src/features/ask/repositories/sqlite.repository.js
  function addAiMessage (line 3) | function addAiMessage({ uid, sessionId, role, content, model = 'unknown'...
  function getAllAiMessagesBySessionId (line 19) | function getAllAiMessagesBySessionId(sessionId) {

FILE: src/features/common/ai/factory.js
  constant PROVIDERS (line 20) | const PROVIDERS = {
  function sanitizeModelId (line 98) | function sanitizeModelId(model) {
  function createSTT (line 102) | function createSTT(provider, opts) {
  function createLLM (line 115) | function createLLM(provider, opts) {
  function createStreamingLLM (line 128) | function createStreamingLLM(provider, opts) {
  function getProviderClass (line 141) | function getProviderClass(providerId) {
  function getAvailableProviders (line 168) | function getAvailableProviders() {

FILE: src/features/common/ai/providers/anthropic.js
  class AnthropicProvider (line 3) | class AnthropicProvider {
    method validateApiKey (line 4) | static async validateApiKey(key) {
  function createSTT (line 47) | async function createSTT({ apiKey, language = "en", callbacks = {}, ...c...
  function createLLM (line 71) | function createLLM({ apiKey, model = "claude-3-5-sonnet-20241022", tempe...
  function createStreamingLLM (line 196) | function createStreamingLLM({

FILE: src/features/common/ai/providers/deepgram.js
  class DeepgramProvider (line 9) | class DeepgramProvider {
    method validateApiKey (line 15) | static async validateApiKey(key) {
  function createSTT (line 39) | function createSTT({
  function createLLM (line 97) | function createLLM(opts) {
  function createStreamingLLM (line 101) | function createStreamingLLM(opts) {

FILE: src/features/common/ai/providers/gemini.js
  class GeminiProvider (line 4) | class GeminiProvider {
    method validateApiKey (line 5) | static async validateApiKey(key) {
  function createSTT (line 37) | async function createSTT({ apiKey, language = "en-US", callbacks = {}, ....
  function createLLM (line 70) | function createLLM({ apiKey, model = "gemini-2.5-flash", temperature = 0...
  function createStreamingLLM (line 204) | function createStreamingLLM({ apiKey, model = "gemini-2.5-flash", temper...

FILE: src/features/common/ai/providers/ollama.js
  class RequestQueue (line 5) | class RequestQueue {
    method constructor (line 6) | constructor() {
    method addStreamingRequest (line 12) | async addStreamingRequest(requestFn) {
    method add (line 30) | async add(requestFn) {
    method process (line 37) | async process() {
  class OllamaProvider (line 77) | class OllamaProvider {
    method validateApiKey (line 78) | static async validateApiKey() {
  function convertMessagesToOllamaFormat (line 93) | function convertMessagesToOllamaFormat(messages) {
  function createLLM (line 119) | function createLLM({
  function createStreamingLLM (line 230) | function createStreamingLLM({

FILE: src/features/common/ai/providers/openai.js
  class OpenAIProvider (line 8) | class OpenAIProvider {
    method validateApiKey (line 9) | static async validateApiKey(key) {
  function createSTT (line 44) | async function createSTT({ apiKey, language = 'en', callbacks = {}, useP...
  function createLLM (line 168) | function createLLM({ apiKey, model = 'gpt-4.1', temperature = 0.7, maxTo...
  function createStreamingLLM (line 264) | function createStreamingLLM({ apiKey, model = 'gpt-4.1', temperature = 0...

FILE: src/features/common/ai/providers/whisper.js
  class DummyEventEmitter (line 8) | class DummyEventEmitter {
    method on (line 9) | on() {}
    method emit (line 10) | emit() {}
    method removeAllListeners (line 11) | removeAllListeners() {}
  class WhisperSTTSession (line 16) | class WhisperSTTSession extends EventEmitter {
    method constructor (line 17) | constructor(model, whisperService, sessionId) {
    method initialize (line 29) | async initialize() {
    method startProcessingLoop (line 42) | startProcessingLoop() {
    method processAudioChunk (line 52) | async processAudioChunk() {
    method sendRealtimeInput (line 124) | sendRealtimeInput(audioData) {
    method close (line 157) | async close() {
  class WhisperProvider (line 175) | class WhisperProvider {
    method validateApiKey (line 176) | static async validateApiKey() {
    method constructor (line 181) | constructor() {
    method initialize (line 185) | async initialize() {
    method createSTT (line 194) | async createSTT(config) {
    method createLLM (line 228) | async createLLM() {
    method createStreamingLLM (line 232) | async createStreamingLLM() {

FILE: src/features/common/config/checksums.js
  constant DOWNLOAD_CHECKSUMS (line 1) | const DOWNLOAD_CHECKSUMS = {

FILE: src/features/common/config/config.js
  class Config (line 6) | class Config {
    method constructor (line 7) | constructor() {
    method loadEnvironmentConfig (line 40) | loadEnvironmentConfig() {
    method loadUserConfig (line 80) | loadUserConfig() {
    method getUserConfigPath (line 93) | getUserConfigPath() {
    method get (line 101) | get(key) {
    method set (line 105) | set(key, value) {
    method getAll (line 109) | getAll() {
    method saveUserConfig (line 113) | saveUserConfig() {
    method reset (line 131) | reset() {
    method isDevelopment (line 136) | isDevelopment() {
    method isProduction (line 140) | isProduction() {
    method shouldLog (line 144) | shouldLog(level) {

FILE: src/features/common/config/schema.js
  constant LATEST_SCHEMA (line 1) | const LATEST_SCHEMA = {

FILE: src/features/common/prompts/promptBuilder.js
  function buildSystemPrompt (line 3) | function buildSystemPrompt(promptParts, customPrompt = '', googleSearchE...
  function getSystemPrompt (line 15) | function getSystemPrompt(profile, customPrompt = '', googleSearchEnabled...

FILE: src/features/common/repositories/firestoreConverter.js
  function createEncryptedConverter (line 10) | function createEncryptedConverter(fieldsToEncrypt = []) {

FILE: src/features/common/repositories/ollamaModel/index.js
  function getRepository (line 6) | function getRepository() {

FILE: src/features/common/repositories/ollamaModel/sqlite.repository.js
  function getAllModels (line 6) | function getAllModels() {
  function getModel (line 21) | function getModel(name) {
  function upsertModel (line 36) | function upsertModel({ name, size, installed = false, installing = false...
  function updateInstallStatus (line 59) | function updateInstallStatus(name, installed, installing = false) {
  function initializeDefaultModels (line 75) | function initializeDefaultModels() {
  function deleteModel (line 85) | function deleteModel(name) {
  function getInstalledModels (line 101) | function getInstalledModels() {
  function getInstallingModels (line 116) | function getInstallingModels() {

FILE: src/features/common/repositories/permission/index.js
  function getRepository (line 4) | function getRepository() {

FILE: src/features/common/repositories/permission/sqlite.repository.js
  function markKeychainCompleted (line 3) | function markKeychainCompleted(uid) {
  function checkKeychainCompleted (line 10) | function checkKeychainCompleted(uid) {

FILE: src/features/common/repositories/preset/firebase.repository.js
  function userPresetsCol (line 16) | function userPresetsCol() {
  function defaultPresetsCol (line 21) | function defaultPresetsCol() {
  function getPresets (line 27) | async function getPresets(uid) {
  function getPresetTemplates (line 48) | async function getPresetTemplates() {
  function create (line 54) | async function create({ uid, title, prompt }) {
  function update (line 67) | async function update(id, { title, prompt }, uid) {
  function del (line 89) | async function del(id, uid) {

FILE: src/features/common/repositories/preset/index.js
  function getBaseRepository (line 5) | function getBaseRepository() {

FILE: src/features/common/repositories/preset/sqlite.repository.js
  function getPresets (line 3) | function getPresets(uid) {
  function getPresetTemplates (line 19) | function getPresetTemplates() {
  function create (line 35) | function create({ uid, title, prompt }) {
  function update (line 49) | function update(id, { title, prompt }, uid) {
  function del (line 64) | function del(id, uid) {

FILE: src/features/common/repositories/providerSettings/index.js
  function getBaseRepository (line 3) | function getBaseRepository() {
  method getByProvider (line 10) | async getByProvider(provider) {
  method getAll (line 15) | async getAll() {
  method upsert (line 20) | async upsert(provider, settings) {
  method remove (line 34) | async remove(provider) {
  method removeAll (line 39) | async removeAll() {
  method getRawApiKeys (line 44) | async getRawApiKeys() {
  method getActiveProvider (line 50) | async getActiveProvider(type) {
  method setActiveProvider (line 55) | async setActiveProvider(provider, type) {
  method getActiveSettings (line 60) | async getActiveSettings() {

FILE: src/features/common/repositories/providerSettings/sqlite.repository.js
  function getByProvider (line 4) | function getByProvider(provider) {
  function getAll (line 16) | function getAll() {
  function upsert (line 29) | function upsert(provider, settings) {
  function remove (line 64) | function remove(provider) {
  function removeAll (line 71) | function removeAll() {
  function getRawApiKeys (line 78) | function getRawApiKeys() {
  function getActiveProvider (line 85) | function getActiveProvider(type) {
  function setActiveProvider (line 99) | function setActiveProvider(provider, type) {
  function getActiveSettings (line 120) | function getActiveSettings() {

FILE: src/features/common/repositories/session/firebase.repository.js
  function sessionsCol (line 8) | function sessionsCol() {
  function subCollections (line 14) | function subCollections(sessionId) {
  function getById (line 24) | async function getById(id) {
  function create (line 30) | async function create(uid, type = 'ask') {
  function getAllByUserId (line 46) | async function getAllByUserId(uid) {
  function updateTitle (line 52) | async function updateTitle(id, title) {
  function deleteWithRelatedData (line 61) | async function deleteWithRelatedData(id) {
  function end (line 83) | async function end(id) {
  function updateType (line 89) | async function updateType(id, type) {
  function touch (line 95) | async function touch(id) {
  function getOrCreateActive (line 101) | async function getOrCreateActive(uid, requestedType = 'ask') {
  function endAllActiveSessions (line 133) | async function endAllActiveSessions(uid) {

FILE: src/features/common/repositories/session/index.js
  function setAuthService (line 6) | function setAuthService(service) {
  function getBaseRepository (line 10) | function getBaseRepository() {

FILE: src/features/common/repositories/session/sqlite.repository.js
  function getById (line 3) | function getById(id) {
  function create (line 8) | function create(uid, type = 'ask') {
  function getAllByUserId (line 24) | function getAllByUserId(uid) {
  function updateTitle (line 30) | function updateTitle(id, title) {
  function deleteWithRelatedData (line 36) | function deleteWithRelatedData(id) {
  function end (line 53) | function end(id) {
  function updateType (line 61) | function updateType(id, type) {
  function touch (line 69) | function touch(id) {
  function getOrCreateActive (line 77) | function getOrCreateActive(uid, requestedType = 'ask') {
  function endAllActiveSessions (line 111) | function endAllActiveSessions(uid) {

FILE: src/features/common/repositories/user/firebase.repository.js
  function usersCol (line 8) | function usersCol() {
  function findOrCreate (line 16) | async function findOrCreate(user) {
  function getById (line 35) | async function getById(uid) {
  function update (line 43) | async function update({ uid, displayName }) {
  function deleteById (line 49) | async function deleteById(uid) {

FILE: src/features/common/repositories/user/index.js
  function getAuthService (line 6) | function getAuthService() {
  function getBaseRepository (line 13) | function getBaseRepository() {

FILE: src/features/common/repositories/user/sqlite.repository.js
  function findOrCreate (line 3) | function findOrCreate(user) {
  function getById (line 38) | function getById(uid) {
  function update (line 45) | function update({ uid, displayName }) {
  function setMigrationComplete (line 51) | function setMigrationComplete(uid) {
  function deleteById (line 61) | function deleteById(uid) {

FILE: src/features/common/repositories/whisperModel/index.js
  class WhisperModelRepository (line 3) | class WhisperModelRepository extends BaseModelRepository {
    method constructor (line 4) | constructor(db, tableName = 'whisper_models') {
    method initializeModels (line 8) | async initializeModels(availableModels) {
    method getInstalledModels (line 25) | async getInstalledModels() {
    method setInstalled (line 29) | async setInstalled(modelId, installed = true) {
    method setInstalling (line 36) | async setInstalling(modelId, installing = true) {
    method isInstalled (line 42) | async isInstalled(modelId) {
    method isInstalling (line 47) | async isInstalling(modelId) {

FILE: src/features/common/services/authService.js
  function getVirtualKeyByEmail (line 11) | async function getVirtualKeyByEmail(email, idToken) {
  class AuthService (line 38) | class AuthService {
    method constructor (line 39) | constructor() {
    method initialize (line 51) | initialize() {
    method startFirebaseAuthFlow (line 128) | async startFirebaseAuthFlow() {
    method signInWithCustomToken (line 141) | async signInWithCustomToken(token) {
    method signOut (line 153) | async signOut() {
    method broadcastUserState (line 168) | broadcastUserState() {
    method getCurrentUserId (line 178) | getCurrentUserId() {
    method getCurrentUser (line 182) | getCurrentUser() {

FILE: src/features/common/services/databaseInitializer.js
  class DatabaseInitializer (line 7) | class DatabaseInitializer {
    method constructor (line 8) | constructor() {
    method ensureDatabaseExists (line 25) | ensureDatabaseExists() {
    method initialize (line 49) | async initialize() {
    method ensureDataDirectory (line 76) | async ensureDataDirectory() {
    method checkDatabaseExists (line 90) | async checkDatabaseExists() {
    method createNewDatabase (line 101) | async createNewDatabase() {
    method connectToExistingDatabase (line 120) | async connectToExistingDatabase() {
    method validateAndRecoverData (line 140) | async validateAndRecoverData() {
    method getStatus (line 177) | async getStatus() {
    method reset (line 187) | async reset() {
    method close (line 210) | close() {
    method getDatabasePath (line 218) | getDatabasePath() {

FILE: src/features/common/services/encryptionService.js
  constant SERVICE_NAME (line 14) | const SERVICE_NAME = 'com.pickle.glass';
  constant ALGORITHM (line 17) | const ALGORITHM = 'aes-256-gcm';
  constant IV_LENGTH (line 18) | const IV_LENGTH = 16;
  constant AUTH_TAG_LENGTH (line 19) | const AUTH_TAG_LENGTH = 16;
  function initializeKey (line 31) | async function initializeKey(userId) {
  function resetSessionKey (line 79) | function resetSessionKey() {
  function encrypt (line 88) | function encrypt(text) {
  function decrypt (line 120) | function decrypt(encryptedText) {
  function looksEncrypted (line 156) | function looksEncrypted(str) {

FILE: src/features/common/services/firebaseClient.js
  function createElectronStorePersistence (line 16) | function createElectronStorePersistence(storeName = 'firebase-auth-sessi...
  function initializeFirebase (line 74) | function initializeFirebase() {
  function getFirebaseAuth (line 101) | function getFirebaseAuth() {
  function getFirestoreInstance (line 108) | function getFirestoreInstance() {

FILE: src/features/common/services/localAIManager.js
  class LocalAIManager (line 7) | class LocalAIManager extends EventEmitter {
    method constructor (line 8) | constructor() {
    method setupEventListeners (line 37) | setupEventListeners() {
    method installService (line 84) | async installService(serviceName, options = {}) {
    method getServiceStatus (line 111) | async getServiceStatus(serviceName) {
    method startService (line 135) | async startService(serviceName) {
    method stopService (line 149) | async stopService(serviceName) {
    method installModel (line 171) | async installModel(serviceName, modelId, options = {}) {
    method getInstalledModels (line 187) | async getInstalledModels(serviceName) {
    method warmUpModel (line 203) | async warmUpModel(modelName, forceRefresh = false) {
    method autoWarmUp (line 210) | async autoWarmUp() {
    method runDiagnostics (line 217) | async runDiagnostics(serviceName) {
    method repairService (line 340) | async repairService(serviceName) {
    method updateServiceState (line 494) | async updateServiceState(serviceName) {
    method getAllServiceStates (line 509) | async getAllServiceStates() {
    method startPeriodicSync (line 529) | startPeriodicSync(interval = 30000) {
    method stopPeriodicSync (line 547) | stopPeriodicSync() {
    method shutdown (line 560) | async shutdown() {
    method handleError (line 584) | async handleError(serviceName, errorType, details = {}) {

FILE: src/features/common/services/migrationService.js
  constant MAX_BATCH_OPERATIONS (line 12) | const MAX_BATCH_OPERATIONS = 500;
  function checkAndRunMigration (line 14) | async function checkAndRunMigration(firebaseUser) {

FILE: src/features/common/services/modelStateService.js
  class ModelStateService (line 9) | class ModelStateService extends EventEmitter {
    method constructor (line 10) | constructor() {
    method initialize (line 17) | async initialize() {
    method _initializeEncryption (line 26) | async _initializeEncryption() {
    method _runMigrations (line 41) | async _runMigrations() {
    method setupLocalAIStateSync (line 97) | setupLocalAIStateSync() {
    method handleLocalAIStateChange (line 104) | async handleLocalAIStateChange(service, state) {
    method getLiveState (line 113) | async getLiveState() {
    method _autoSelectAvailableModels (line 130) | async _autoSelectAvailableModels(forceReselectionForTypes = [], isInit...
    method setFirebaseVirtualKey (line 169) | async setFirebaseVirtualKey(virtualKey) {
    method setApiKey (line 207) | async setApiKey(provider, key) {
    method getAllApiKeys (line 234) | async getAllApiKeys() {
    method removeApiKey (line 245) | async removeApiKey(provider) {
    method isLoggedInWithFirebase (line 260) | isLoggedInWithFirebase() {
    method hasValidApiKey (line 267) | async hasValidApiKey() {
    method getProviderForModel (line 274) | getProviderForModel(arg1, arg2) {
    method getSelectedModels (line 298) | async getSelectedModels() {
    method setSelectedModel (line 306) | async setSelectedModel(type, modelId) {
    method getAvailableModels (line 336) | async getAvailableModels(type) {
    method getCurrentModelInfo (line 355) | async getCurrentModelInfo(type) {
    method validateApiKey (line 371) | async validateApiKey(provider, key) {
    method getProviderConfig (line 386) | getProviderConfig() {
    method handleRemoveApiKey (line 395) | async handleRemoveApiKey(provider) {
    method handleValidateKey (line 407) | async handleValidateKey(provider, key) {
    method handleSetSelectedModel (line 411) | async handleSetSelectedModel(type, modelId) {
    method areProvidersConfigured (line 415) | async areProvidersConfigured() {

FILE: src/features/common/services/ollamaService.js
  class OllamaService (line 17) | class OllamaService extends EventEmitter {
    method constructor (line 18) | constructor() {
    method getPlatform (line 62) | getPlatform() {
    method checkCommand (line 66) | async checkCommand(command) {
    method waitForService (line 77) | async waitForService(checkFn, maxAttempts = 30, delayMs = 1000) {
    method getInstallProgress (line 88) | getInstallProgress(modelName) {
    method setInstallProgress (line 92) | setInstallProgress(modelName, progress) {
    method clearInstallProgress (line 96) | clearInstallProgress(modelName) {
    method getStatus (line 100) | async getStatus() {
    method getOllamaCliPath (line 120) | getOllamaCliPath() {
    method makeRequest (line 128) | async makeRequest(endpoint, options = {}) {
    method isInstalled (line 153) | async isInstalled() {
    method isServiceRunning (line 175) | async isServiceRunning() {
    method startService (line 190) | async startService() {
    method stopService (line 225) | async stopService() {
    method healthCheck (line 230) | async healthCheck() {
    method getInstalledModels (line 281) | async getInstalledModels() {
    method getLoadedModels (line 302) | async getLoadedModels() {
    method getLoadedModelsWithMemoryInfo (line 329) | async getLoadedModelsWithMemoryInfo() {
    method isModelLoaded (line 349) | async isModelLoaded(modelName) {
    method getInstalledModelsList (line 354) | async getInstalledModelsList() {
    method getModelSuggestions (line 392) | async getModelSuggestions() {
    method isModelInstalled (line 406) | async isModelInstalled(modelName) {
    method pullModel (line 411) | async pullModel(modelName) {
    method _parseOllamaPullProgress (line 513) | _parseOllamaPullProgress(data, modelName) {
    method downloadFile (line 543) | async downloadFile(url, destination, options = {}) {
    method downloadWithRetry (line 626) | async downloadWithRetry(url, destination, options = {}) {
    method verifyChecksum (line 663) | async verifyChecksum(filePath, expectedChecksum) {
    method autoInstall (line 679) | async autoInstall(onProgress) {
    method installMacOS (line 700) | async installMacOS(onProgress) {
    method installWindows (line 769) | async installWindows(onProgress) {
    method installLinux (line 808) | async installLinux() {
    method saveCheckpoint (line 819) | async saveCheckpoint(name) {
    method rollbackToLastCheckpoint (line 827) | async rollbackToLastCheckpoint() {
    method _executeRollback (line 836) | async _executeRollback(checkpoint) {
    method syncState (line 851) | async syncState() {
    method startPeriodicSync (line 933) | startPeriodicSync() {
    method stopPeriodicSync (line 941) | stopPeriodicSync() {
    method warmUpModel (line 948) | async warmUpModel(modelName, forceRefresh = false) {
    method _performWarmUp (line 994) | async _performWarmUp(modelName) {
    method autoWarmUpSelectedModel (line 1060) | async autoWarmUpSelectedModel() {
    method _clearWarmUpCache (line 1104) | _clearWarmUpCache() {
    method getWarmUpStatus (line 1111) | async getWarmUpStatus() {
    method shutdown (line 1122) | async shutdown(force = false) {
    method shutdownMacOS (line 1174) | async shutdownMacOS(force) {
    method shutdownWindows (line 1222) | async shutdownWindows(force) {
    method shutdownLinux (line 1240) | async shutdownLinux(force) {
    method getAllModelsWithStatus (line 1254) | async getAllModelsWithStatus() {
    method handleGetStatus (line 1300) | async handleGetStatus() {
    method handleInstall (line 1320) | async handleInstall() {
    method handleStartService (line 1359) | async handleStartService() {
    method handleEnsureReady (line 1374) | async handleEnsureReady() {
    method handleGetModels (line 1387) | async handleGetModels() {
    method handleGetModelSuggestions (line 1397) | async handleGetModelSuggestions() {
    method handlePullModel (line 1407) | async handlePullModel(modelName) {
    method handleIsModelInstalled (line 1432) | async handleIsModelInstalled(modelName) {
    method handleWarmUpModel (line 1442) | async handleWarmUpModel(modelName) {
    method handleAutoWarmUp (line 1452) | async handleAutoWarmUp() {
    method handleGetWarmUpStatus (line 1462) | async handleGetWarmUpStatus() {
    method handleShutdown (line 1472) | async handleShutdown(force = false) {
    method verifyInstallation (line 1492) | async verifyInstallation() {

FILE: src/features/common/services/permissionService.js
  class PermissionService (line 4) | class PermissionService {
    method _getAuthService (line 5) | _getAuthService() {
    method checkSystemPermissions (line 9) | async checkSystemPermissions() {
    method requestMicrophonePermission (line 44) | async requestMicrophonePermission() {
    method openSystemPreferences (line 70) | async openSystemPreferences(section) {
    method markKeychainCompleted (line 97) | async markKeychainCompleted() {
    method checkKeychainCompleted (line 108) | async checkKeychainCompleted(uid) {

FILE: src/features/common/services/sqliteClient.js
  constant LATEST_SCHEMA (line 3) | const LATEST_SCHEMA = require('../config/schema');
  class SQLiteClient (line 5) | class SQLiteClient {
    method constructor (line 6) | constructor() {
    method connect (line 12) | connect(dbPath) {
    method getDb (line 29) | getDb() {
    method _validateAndQuoteIdentifier (line 36) | _validateAndQuoteIdentifier(identifier) {
    method _migrateProviderSettings (line 43) | _migrateProviderSettings() {
    method synchronizeSchema (line 113) | async synchronizeSchema() {
    method getTablesFromDb (line 135) | getTablesFromDb() {
    method createTable (line 140) | createTable(tableName, tableSchema) {
    method updateTable (line 154) | updateTable(tableName, tableSchema) {
    method runQuery (line 179) | runQuery(query, params = []) {
    method cleanupEmptySessions (line 183) | cleanupEmptySessions() {
    method initTables (line 209) | async initTables() {
    method initDefaultData (line 214) | initDefaultData() {
    method close (line 243) | close() {
    method query (line 255) | query(sql, params = []) {

FILE: src/features/common/services/whisperService.js
  class WhisperService (line 16) | class WhisperService extends EventEmitter {
    method constructor (line 17) | constructor() {
    method getPlatform (line 64) | getPlatform() {
    method checkCommand (line 68) | async checkCommand(command) {
    method waitForService (line 79) | async waitForService(checkFn, maxAttempts = 30, delayMs = 1000) {
    method downloadFile (line 90) | async downloadFile(url, destination, options = {}) {
    method downloadWithRetry (line 173) | async downloadWithRetry(url, destination, options = {}) {
    method verifyChecksum (line 210) | async verifyChecksum(filePath, expectedChecksum) {
    method autoInstall (line 226) | async autoInstall(onProgress) {
    method shutdown (line 247) | async shutdown(force = false) {
    method initialize (line 276) | async initialize() {
    method ensureDirectories (line 307) | async ensureDirectories() {
    method getSession (line 314) | async getSession(config) {
    method releaseSession (line 335) | async releaseSession(sessionId) {
    method cleanup (line 353) | async cleanup() {
    method ensureWhisperBinary (line 363) | async ensureWhisperBinary() {
    method installViaHomebrew (line 411) | async installViaHomebrew() {
    method ensureModelAvailable (line 434) | async ensureModelAvailable(modelId) {
    method downloadModel (line 455) | async downloadModel(modelId) {
    method handleDownloadModel (line 482) | async handleDownloadModel(modelId) {
    method handleGetInstalledModels (line 499) | async handleGetInstalledModels() {
    method getModelPath (line 512) | async getModelPath(modelId) {
    method getWhisperPath (line 519) | async getWhisperPath() {
    method saveAudioToTemp (line 523) | async saveAudioToTemp(audioBuffer, sessionId = '') {
    method createWavHeader (line 536) | createWavHeader(dataSize) {
    method cleanupTempFile (line 559) | async cleanupTempFile(filePath) {
    method getInstalledModels (line 586) | async getInstalledModels() {
    method isServiceRunning (line 615) | async isServiceRunning() {
    method startService (line 619) | async startService() {
    method stopService (line 626) | async stopService() {
    method isInstalled (line 630) | async isInstalled() {
    method installMacOS (line 639) | async installMacOS() {
    method installWindows (line 643) | async installWindows() {
    method findWhisperExecutables (line 712) | async findWhisperExecutables(dir) {
    method removeDirectory (line 736) | async removeDirectory(dir) {
    method installLinux (line 756) | async installLinux() {
    method shutdownMacOS (line 776) | async shutdownMacOS(force) {
    method shutdownWindows (line 780) | async shutdownWindows(force) {
    method shutdownLinux (line 784) | async shutdownLinux(force) {
  class WhisperSession (line 790) | class WhisperSession {
    method constructor (line 791) | constructor(config, service) {
    method initialize (line 800) | async initialize() {
    method reconfigure (line 805) | async reconfigure(config) {
    method startProcessingLoop (line 810) | startProcessingLoop() {
    method cleanup (line 814) | async cleanup() {
    method cleanupTempFiles (line 819) | async cleanupTempFiles() {
    method destroy (line 823) | async destroy() {

FILE: src/features/common/utils/spawnHelper.js
  function spawnAsync (line 3) | function spawnAsync(command, args = [], options = {}) {

FILE: src/features/listen/listenService.js
  class ListenService (line 9) | class ListenService {
    method constructor (line 10) | constructor() {
    method setupServiceCallbacks (line 20) | setupServiceCallbacks() {
    method sendToRenderer (line 42) | sendToRenderer(channel, data) {
    method initialize (line 51) | initialize() {
    method handleListenRequest (line 56) | async handleListenRequest(listenButtonText) {
    method handleTranscriptionComplete (line 99) | async handleTranscriptionComplete(speaker, text) {
    method saveConversationTurn (line 109) | async saveConversationTurn(speaker, transcription) {
    method initializeNewSession (line 129) | async initializeNewSession() {
    method initializeSession (line 157) | async initializeSession(language = 'en') {
    method sendMicAudioContent (line 212) | async sendMicAudioContent(data, mimeType) {
    method startMacOSAudioCapture (line 216) | async startMacOSAudioCapture() {
    method stopMacOSAudioCapture (line 223) | async stopMacOSAudioCapture() {
    method isSessionActive (line 227) | isSessionActive() {
    method closeSession (line 231) | async closeSession() {
    method getCurrentSessionData (line 257) | getCurrentSessionData() {
    method getConversationHistory (line 266) | getConversationHistory() {
    method _createHandler (line 270) | _createHandler(asyncFn, successMessage, errorMessage) {

FILE: src/features/listen/stt/repositories/firebase.repository.js
  function transcriptsCol (line 7) | function transcriptsCol(sessionId) {
  function addTranscript (line 13) | async function addTranscript({ uid, sessionId, speaker, text }) {
  function getAllTranscriptsBySessionId (line 27) | async function getAllTranscriptsBySessionId(sessionId) {

FILE: src/features/listen/stt/repositories/index.js
  function getBaseRepository (line 5) | function getBaseRepository() {

FILE: src/features/listen/stt/repositories/sqlite.repository.js
  function addTranscript (line 3) | function addTranscript({ uid, sessionId, speaker, text }) {
  function getAllTranscriptsBySessionId (line 19) | function getAllTranscriptsBySessionId(sessionId) {

FILE: src/features/listen/stt/sttService.js
  constant COMPLETION_DEBOUNCE_MS (line 6) | const COMPLETION_DEBOUNCE_MS = 2000;
  constant KEEP_ALIVE_INTERVAL_MS (line 12) | const KEEP_ALIVE_INTERVAL_MS = 60 * 1000;
  constant SESSION_RENEW_INTERVAL_MS (line 17) | const SESSION_RENEW_INTERVAL_MS = 20 * 60 * 1000;
  constant SOCKET_OVERLAP_MS (line 21) | const SOCKET_OVERLAP_MS = 2 * 1000;
  class SttService (line 23) | class SttService {
    method constructor (line 24) | constructor() {
    method setCallbacks (line 50) | setCallbacks({ onTranscriptionComplete, onStatusUpdate }) {
    method sendToRenderer (line 55) | sendToRenderer(channel, data) {
    method handleSendSystemAudioContent (line 65) | async handleSendSystemAudioContent(data, mimeType) {
    method flushMyCompletion (line 76) | flushMyCompletion() {
    method flushTheirCompletion (line 103) | flushTheirCompletion() {
    method debounceMyCompletion (line 130) | debounceMyCompletion(text) {
    method debounceTheirCompletion (line 141) | debounceTheirCompletion(text) {
    method initializeSttSessions (line 152) | async initializeSttSessions(language = 'en') {
    method _sendKeepAlive (line 500) | _sendKeepAlive() {
    method renewSessions (line 517) | async renewSessions(language = 'en') {
    method sendMicAudioContent (line 546) | async sendMicAudioContent(data, mimeType) {
    method sendSystemAudioContent (line 574) | async sendSystemAudioContent(data, mimeType) {
    method killExistingSystemAudioDump (line 600) | killExistingSystemAudioDump() {
    method startMacOSAudioCapture (line 629) | async startMacOSAudioCapture() {
    method convertStereoToMono (line 722) | convertStereoToMono(stereoBuffer) {
    method stopMacOSAudioCapture (line 734) | stopMacOSAudioCapture() {
    method isSessionActive (line 742) | isSessionActive() {
    method closeSessions (line 746) | async closeSessions() {

FILE: src/features/listen/summary/repositories/firebase.repository.js
  function summaryDocRef (line 9) | function summaryDocRef(sessionId) {
  function saveSummary (line 17) | async function saveSummary({ uid, sessionId, tldr, text, bullet_json, ac...
  function getSummaryBySessionId (line 39) | async function getSummaryBySessionId(sessionId) {

FILE: src/features/listen/summary/repositories/index.js
  function getBaseRepository (line 5) | function getBaseRepository() {

FILE: src/features/listen/summary/repositories/sqlite.repository.js
  function saveSummary (line 3) | function saveSummary({ uid, sessionId, tldr, text, bullet_json, action_j...
  function getSummaryBySessionId (line 31) | function getSummaryBySessionId(sessionId) {

FILE: src/features/listen/summary/summaryService.js
  class SummaryService (line 8) | class SummaryService {
    method constructor (line 9) | constructor() {
    method setCallbacks (line 20) | setCallbacks({ onAnalysisComplete, onStatusUpdate }) {
    method setSessionId (line 25) | setSessionId(sessionId) {
    method sendToRenderer (line 29) | sendToRenderer(channel, data) {
    method addConversationTurn (line 38) | addConversationTurn(speaker, text) {
    method getConversationHistory (line 48) | getConversationHistory() {
    method resetConversationHistory (line 52) | resetConversationHistory() {
    method formatConversationForPrompt (line 65) | formatConversationForPrompt(conversationTexts, maxTurns = 30) {
    method makeOutlineAndRequests (line 70) | async makeOutlineAndRequests(conversationTexts, maxTurns = 30) {
    method parseResponseText (line 189) | parseResponseText(responseText, previousResult) {
    method triggerAnalysisIfNeeded (line 305) | async triggerAnalysisIfNeeded() {
    method getCurrentAnalysisData (line 324) | getCurrentAnalysisData() {

FILE: src/features/settings/repositories/firebase.repository.js
  function userPresetsCol (line 16) | function userPresetsCol() {
  function defaultPresetsCol (line 21) | function defaultPresetsCol() {
  function getPresets (line 26) | async function getPresets(uid) {
  function getPresetTemplates (line 47) | async function getPresetTemplates() {
  function createPreset (line 53) | async function createPreset({ uid, title, prompt }) {
  function updatePreset (line 66) | async function updatePreset(id, { title, prompt }, uid) {
  function deletePreset (line 87) | async function deletePreset(id, uid) {
  function getAutoUpdate (line 99) | async function getAutoUpdate(uid) {
  function setAutoUpdate (line 123) | async function setAutoUpdate(uid, isEnabled) {

FILE: src/features/settings/repositories/index.js
  function getBaseRepository (line 5) | function getBaseRepository() {

FILE: src/features/settings/repositories/sqlite.repository.js
  function getPresets (line 3) | function getPresets(uid) {
  function getPresetTemplates (line 19) | function getPresetTemplates() {
  function createPreset (line 35) | function createPreset({ uid, title, prompt }) {
  function updatePreset (line 53) | function updatePreset(id, { title, prompt }, uid) {
  function deletePreset (line 74) | function deletePreset(id, uid) {
  function getAutoUpdate (line 93) | function getAutoUpdate(uid) {
  function setAutoUpdate (line 117) | function setAutoUpdate(uid, isEnabled) {

FILE: src/features/settings/settingsService.js
  constant NOTIFICATION_CONFIG (line 19) | const NOTIFICATION_CONFIG = {
  function getModelSettings (line 27) | async function getModelSettings() {
  function clearApiKey (line 44) | async function clearApiKey(provider) {
  function setSelectedModel (line 49) | async function setSelectedModel(type, modelId) {
  function getOllamaStatus (line 55) | async function getOllamaStatus() {
  function ensureOllamaReady (line 59) | async function ensureOllamaReady() {
  function shutdownOllama (line 67) | async function shutdownOllama() {
  class WindowNotificationManager (line 73) | class WindowNotificationManager {
    method constructor (line 74) | constructor() {
    method notifyRelevantWindows (line 84) | notifyRelevantWindows(event, data = null, options = {}) {
    method sendToTargetWindows (line 99) | sendToTargetWindows(event, data, windowTypes) {
    method getRelevantWindows (line 122) | getRelevantWindows(windowTypes) {
    method debounceNotification (line 142) | debounceNotification(key, fn, delay) {
    method cleanup (line 157) | cleanup() {
  constant DEFAULT_KEYBINDS (line 168) | const DEFAULT_KEYBINDS = {
  function getDefaultSettings (line 202) | function getDefaultSettings() {
  function getSettings (line 221) | async function getSettings() {
  function saveSettings (line 237) | async function saveSettings(settings) {
  function getPresets (line 258) | async function getPresets() {
  function getPresetTemplates (line 269) | async function getPresetTemplates() {
  function createPreset (line 279) | async function createPreset(title, prompt) {
  function updatePreset (line 297) | async function updatePreset(id, title, prompt) {
  function deletePreset (line 315) | async function deletePreset(id) {
  function saveApiKey (line 332) | async function saveApiKey(apiKey, provider = 'openai') {
  function removeApiKey (line 356) | async function removeApiKey() {
  function updateContentProtection (line 385) | async function updateContentProtection(enabled) {
  function getAutoUpdateSetting (line 404) | async function getAutoUpdateSetting() {
  function setAutoUpdateSetting (line 413) | async function setAutoUpdateSetting(isEnabled) {
  function initialize (line 423) | function initialize() {
  function cleanup (line 431) | function cleanup() {
  function notifyPresetUpdate (line 436) | function notifyPresetUpdate(action, presetId, title = null) {

FILE: src/features/shortcuts/repositories/sqlite.repository.js
  function getAllKeybinds (line 4) | function getAllKeybinds() {
  function upsertKeybinds (line 15) | function upsertKeybinds(keybinds) {

FILE: src/features/shortcuts/shortcutsService.js
  class ShortcutsService (line 7) | class ShortcutsService {
    method constructor (line 8) | constructor() {
    method initialize (line 15) | initialize(windowPool) {
    method openShortcutSettingsWindow (line 24) | async openShortcutSettingsWindow () {
    method closeShortcutSettingsWindow (line 35) | async closeShortcutSettingsWindow () {
    method handleSaveShortcuts (line 42) | async handleSaveShortcuts(newKeybinds) {
    method handleRestoreDefaults (line 54) | async handleRestoreDefaults() {
    method getDefaultKeybinds (line 59) | getDefaultKeybinds() {
    method loadKeybinds (line 77) | async loadKeybinds() {
    method saveKeybinds (line 109) | async saveKeybinds(newKeybinds) {
    method toggleAllWindowsVisibility (line 123) | async toggleAllWindowsVisibility() {
    method registerShortcuts (line 138) | async registerShortcuts(registerOnlyToggleVisibility = false) {
    method unregisterAll (line 279) | unregisterAll() {

FILE: src/index.js
  constant WEB_PORT (line 34) | let WEB_PORT = 3000;
  function setupProtocolHandling (line 48) | function setupProtocolHandling() {
  function focusMainWindow (line 121) | function focusMainWindow() {
  function setupWebDataHandlers (line 317) | function setupWebDataHandlers() {
  function handleCustomUrl (line 447) | async function handleCustomUrl(url) {
  function handleFirebaseAuthCallback (line 498) | async function handleFirebaseAuthCallback(params) {
  function handlePersonalizeFromUrl (line 564) | function handlePersonalizeFromUrl(params) {
  function startWebStack (line 590) | async function startWebStack() {
  function initAutoUpdater (line 697) | async function initAutoUpdater() {

FILE: src/ui/app/ApiKeyHeader.js
  class ApiKeyHeader (line 3) | class ApiKeyHeader extends LitElement {
    method constructor (line 327) | constructor() {
    method updated (line 406) | updated(changedProperties) {
    method reset (line 411) | reset() {
    method handleBack (line 420) | handleBack() {
    method loadProviderConfig (line 426) | async loadProviderConfig() {
    method handleMouseDown (line 477) | async handleMouseDown(e) {
    method handleMouseMove (line 499) | handleMouseMove(e) {
    method handleMouseUp (line 517) | handleMouseUp(e) {
    method handleInput (line 533) | handleInput(e) {
    method clearMessages (line 547) | clearMessages() {
    method handleProviderChange (line 555) | handleProviderChange(e) {
    method handleLlmProviderChange (line 562) | async handleLlmProviderChange(e, providerId) {
    method _initializeOllamaConnection (line 594) | async _initializeOllamaConnection() {
    method _attemptOllamaConnection (line 623) | async _attemptOllamaConnection() {
    method _cancelAllActiveOperations (line 627) | _cancelAllActiveOperations() {
    method getOperationMetrics (line 651) | getOperationMetrics() {
    method _adjustBackpressureThresholds (line 666) | _adjustBackpressureThresholds() {
    method _startHealthMonitoring (line 687) | _startHealthMonitoring() {
    method _stopHealthMonitoring (line 698) | _stopHealthMonitoring() {
    method _performHealthCheck (line 710) | async _performHealthCheck() {
    method _handleHealthCheckFailure (line 750) | _handleHealthCheckFailure() {
    method _restartHealthMonitoring (line 763) | _restartHealthMonitoring() {
    method getHealthStatus (line 771) | getHealthStatus() {
    method handleSttProviderChange (line 794) | async handleSttProviderChange(e, providerId) {
    method _executeOperation (line 821) | async _executeOperation(operationType, operation, options = {}) {
    method _queueOperation (line 839) | async _queueOperation(operationId, operationType, operation, options) {
    method _executeImmediately (line 863) | async _executeImmediately(operationId, operationType, operation, timeo...
    method _recordOperationSuccess (line 904) | _recordOperationSuccess(startTime) {
    method _recordOperationFailure (line 910) | _recordOperationFailure(error, operationType) {
    method _updateAverageResponseTime (line 919) | _updateAverageResponseTime(responseTime) {
    method _processQueue (line 924) | async _processQueue() {
    method _cancelOperation (line 948) | _cancelOperation(operationType) {
    method _cleanupOperation (line 956) | _cleanupOperation(operationId, operationType) {
    method _updateConnectionState (line 964) | _updateConnectionState(newState, reason = '') {
    method _handleStateChange (line 975) | _handleStateChange(state, reason) {
    method refreshOllamaStatus (line 998) | async refreshOllamaStatus() {
    method loadModelSuggestions (line 1029) | async loadModelSuggestions() {
    method ensureOllamaReady (line 1056) | async ensureOllamaReady() {
    method ensureOllamaReadyWithUI (line 1086) | async ensureOllamaReadyWithUI() {
    method _handleOllamaSetupCompletion (line 1189) | async _handleOllamaSetupCompletion(success, errorMessage = null) {
    method handleModelInput (line 1203) | async handleModelInput(e) {
    method handleModelKeyPress (line 1216) | async handleModelKeyPress(e) {
    method loadUserModelHistory (line 1235) | loadUserModelHistory() {
    method saveToUserHistory (line 1247) | saveToUserHistory(modelName) {
    method getCombinedModelSuggestions (line 1267) | getCombinedModelSuggestions() {
    method installModel (line 1296) | async installModel(modelName) {
    method _isOperationCancelled (line 1365) | _isOperationCancelled(modelName) {
    method downloadWhisperModel (line 1369) | async downloadWhisperModel(modelId) {
    method handlePaste (line 1430) | handlePaste(e) {
    method handleKeyPress (line 1453) | handleKeyPress(e) {
    method handleSttModelChange (line 1461) | async handleSttModelChange(e) {
    method handleSubmit (line 1477) | async handleSubmit() {
    method startSlideOutAnimation (line 1626) | startSlideOutAnimation() {
    method handleClose (line 1639) | handleClose() {
    method handleAnimationEnd (line 1646) | handleAnimationEnd(e) {
    method connectedCallback (line 1692) | connectedCallback() {
    method handleMessageFadeEnd (line 1697) | handleMessageFadeEnd(e) {
    method disconnectedCallback (line 1710) | disconnectedCallback() {
    method _performCompleteCleanup (line 1718) | _performCompleteCleanup() {
    method _renderOllamaStateUI (line 1756) | _renderOllamaStateUI() {
    method _getOllamaUIState (line 1777) | _getOllamaUIState() {
    method _renderConnectingState (line 1802) | _renderConnectingState(state) {
    method _renderInstallRequiredState (line 1815) | _renderInstallRequiredState() {
    method _renderStartRequiredState (line 1819) | _renderStartRequiredState() {
    method _renderReadyState (line 1823) | _renderReadyState() {
    method _renderFailedState (line 1866) | _renderFailedState(state) {
    method _renderInstallingState (line 1884) | _renderInstallingState(state) {
    method _renderUnknownState (line 1897) | _renderUnknownState() {
    method renderModelStatus (line 1905) | renderModelStatus() {
    method shouldFadeMessage (line 1909) | shouldFadeMessage(type) {
    method openPrivacyPolicy (line 1914) | openPrivacyPolicy() {
    method render (line 1921) | render() {

FILE: src/ui/app/HeaderController.js
  class HeaderTransitionManager (line 6) | class HeaderTransitionManager {
    method constructor (line 7) | constructor() {
    method notifyHeaderState (line 107) | notifyHeaderState(stateOverride) {
    method _bootstrap (line 114) | async _bootstrap() {
    method handleStateUpdate (line 129) | async handleStateUpdate(userState) {
    method handleLoginOption (line 148) | async handleLoginOption() {
    method handleApiKeyOption (line 155) | async handleApiKeyOption() {
    method transitionToWelcomeHeader (line 165) | async transitionToWelcomeHeader() {
    method transitionToPermissionHeader (line 175) | async transitionToPermissionHeader() {
    method transitionToMainHeader (line 220) | async transitionToMainHeader(animate = true) {
    method _resizeForMain (line 229) | async _resizeForMain() {
    method _resizeForApiKey (line 235) | async _resizeForApiKey(height = 370) {
    method _resizeForPermissionHeader (line 241) | async _resizeForPermissionHeader(height) {
    method _resizeForWelcome (line 248) | async _resizeForWelcome() {
    method checkPermissions (line 255) | async checkPermissions() {

FILE: src/ui/app/MainHeader.js
  class MainHeader (line 3) | class MainHeader extends LitElement {
    method constructor (line 343) | constructor() {
    method _getListenButtonText (line 360) | _getListenButtonText(status) {
    method handleMouseDown (line 369) | async handleMouseDown(e) {
    method handleMouseMove (line 386) | handleMouseMove(e) {
    method handleMouseUp (line 402) | handleMouseUp(e) {
    method toggleVisibility (line 418) | toggleVisibility() {
    method hide (line 438) | hide() {
    method show (line 443) | show() {
    method handleAnimationEnd (line 448) | handleAnimationEnd(e) {
    method startSlideInAnimation (line 465) | startSlideInAnimation() {
    method connectedCallback (line 470) | connectedCallback() {
    method disconnectedCallback (line 498) | disconnectedCallback() {
    method showSettingsWindow (line 517) | showSettingsWindow(element) {
    method hideSettingsWindow (line 526) | hideSettingsWindow() {
    method _handleListenClick (line 534) | async _handleListenClick() {
    method _handleAskClick (line 553) | async _handleAskClick() {
    method _handleToggleAllWindowsVisibility (line 565) | async _handleToggleAllWindowsVisibility() {
    method renderShortcut (line 578) | renderShortcut(accelerator) {
    method render (line 601) | render() {

FILE: src/ui/app/PermissionHeader.js
  class PermissionHeader (line 3) | class PermissionHeader extends LitElement {
    method constructor (line 273) | constructor() {
    method updated (line 283) | updated(changedProperties) {
    method connectedCallback (line 296) | async connectedCallback() {
    method disconnectedCallback (line 325) | disconnectedCallback() {
    method checkPermissions (line 332) | async checkPermissions() {
    method handleMicrophoneClick (line 373) | async handleMicrophoneClick() {
    method handleScreenClick (line 405) | async handleScreenClick() {
    method handleKeychainClick (line 432) | async handleKeychainClick() {
    method handleContinue (line 450) | async handleContinue() {
    method handleClose (line 472) | handleClose() {
    method render (line 479) | render() {

FILE: src/ui/app/PickleGlassApp.js
  class PickleGlassApp (line 9) | class PickleGlassApp extends LitElement {
    method constructor (line 51) | constructor() {
    method connectedCallback (line 74) | connectedCallback() {
    method disconnectedCallback (line 84) | disconnectedCallback() {
    method updated (line 91) | updated(changedProperties) {
    method handleClose (line 120) | async handleClose() {
    method render (line 129) | render() {

FILE: src/ui/app/WelcomeHeader.js
  class WelcomeHeader (line 3) | class WelcomeHeader extends LitElement {
    method constructor (line 168) | constructor() {
    method updated (line 175) | updated(changedProperties) {
    method handleClose (line 180) | handleClose() {
    method render (line 186) | render() {
    method openPrivacyPolicy (line 228) | openPrivacyPolicy() {

FILE: src/ui/ask/AskView.js
  class AskView (line 4) | class AskView extends LitElement {
    method constructor (line 714) | constructor() {
    method connectedCallback (line 750) | connectedCallback() {
    method disconnectedCallback (line 810) | disconnectedCallback() {
    method loadLibraries (line 842) | async loadLibraries() {
    method handleCloseAskWindow (line 898) | handleCloseAskWindow() {
    method handleCloseIfNoContent (line 903) | handleCloseIfNoContent() {
    method handleEscKey (line 909) | handleEscKey(e) {
    method clearResponseContent (line 916) | clearResponseContent() {
    method handleInputFocus (line 928) | handleInputFocus() {
    method focusTextInput (line 932) | focusTextInput() {
    method loadScript (line 942) | loadScript(src) {
    method parseMarkdown (line 952) | parseMarkdown(text) {
    method fixIncompleteCodeBlocks (line 967) | fixIncompleteCodeBlocks(text) {
    method handleScroll (line 980) | handleScroll(direction) {
    method renderContent (line 993) | renderContent() {
    method resetStreamingParser (line 1023) | resetStreamingParser() {
    method renderStreamingMarkdown (line 1029) | renderStreamingMarkdown(responseContainer) {
    method renderFallbackContent (line 1077) | renderFallbackContent(responseContainer) {
    method requestWindowResize (line 1124) | requestWindowResize(targetHeight) {
    method animateHeaderText (line 1130) | animateHeaderText(text) {
    method startHeaderAnimation (line 1141) | startHeaderAnimation() {
    method renderMarkdown (line 1153) | renderMarkdown(content) {
    method fixIncompleteMarkdown (line 1166) | fixIncompleteMarkdown(text) {
    method handleCopy (line 1204) | async handleCopy() {
    method handleLineCopy (line 1241) | async handleLineCopy(lineIndex) {
    method handleSendText (line 1272) | async handleSendText(e, overridingText = '') {
    method handleTextKeydown (line 1286) | handleTextKeydown(e) {
    method updated (line 1301) | updated(changedProperties) {
    method firstUpdated (line 1318) | firstUpdated() {
    method getTruncatedQuestion (line 1323) | getTruncatedQuestion(question, maxLength = 30) {
    method render (line 1331) | render() {
    method adjustWindowHeight (line 1407) | adjustWindowHeight() {
    method adjustWindowHeightThrottled (line 1431) | adjustWindowHeightThrottled() {

FILE: src/ui/assets/smd.js
  constant DOCUMENT (line 8) | const
  constant PARAGRAPH (line 8) | const
  constant HEADING_1 (line 8) | const
  constant HEADING_2 (line 8) | const
  constant HEADING_3 (line 8) | const
  constant HEADING_4 (line 8) | const
  constant HEADING_5 (line 8) | const
  constant HEADING_6 (line 8) | const
  constant CODE_BLOCK (line 8) | const
  constant CODE_FENCE (line 8) | const
  constant CODE_INLINE (line 8) | const
  constant ITALIC_AST (line 8) | const
  constant ITALIC_UND (line 8) | const
  constant STRONG_AST (line 8) | const
  constant STRONG_UND (line 8) | const
  constant STRIKE (line 8) | const
  constant LINK (line 8) | const
  constant RAW_URL (line 8) | const
  constant IMAGE (line 8) | const
  constant BLOCKQUOTE (line 8) | const
  constant LINE_BREAK (line 8) | const
  constant RULE (line 8) | const
  constant LIST_UNORDERED (line 8) | const
  constant LIST_ORDERED (line 8) | const
  constant LIST_ITEM (line 8) | const
  constant CHECKBOX (line 8) | const
  constant TABLE (line 8) | const
  constant TABLE_ROW (line 8) | const
  constant TABLE_CELL (line 8) | const
  constant EQUATION_BLOCK (line 8) | const
  constant EQUATION_INLINE (line 8) | const
  constant NEWLINE (line 8) | const
  constant MAYBE_URL (line 8) | const
  constant MAYBE_TASK (line 8) | const
  constant MAYBE_BR (line 8) | const
  constant MAYBE_EQ_BLOCK (line 8) | const
  function token_to_string (line 84) | function token_to_string(type) {
  constant HREF (line 120) | const
  constant SRC (line 120) | const
  constant LANG (line 120) | const
  constant CHECKED (line 120) | const
  constant START (line 120) | const
  function attr_to_html_attr (line 139) | function attr_to_html_attr(type) {
  constant TOKEN_ARRAY_CAP (line 198) | const TOKEN_ARRAY_CAP = 24
  function parser (line 204) | function parser(renderer) {
  function parser_end (line 230) | function parser_end(p) {
  function add_text (line 239) | function add_text(p) {
  function ensure_paragraph (line 249) | function ensure_paragraph(p) {
  function push_text (line 264) | function push_text(p, text) {
  function end_token (line 272) | function end_token(p) {
  function add_token (line 283) | function add_token(p, token) {
  function idx_of_token (line 310) | function idx_of_token(p, token, start_idx) {
  function end_tokens_to_len (line 325) | function end_tokens_to_len(p, len) {
  function end_tokens_to_indent (line 339) | function end_tokens_to_indent(p, indent) {
  function continue_or_add_list (line 366) | function continue_or_add_list(p, list_token) {
  function add_list_item (line 410) | function add_list_item(p, prefix_length) {
  function clear_root_pending (line 420) | function clear_root_pending(p) {
  function is_digit (line 429) | function is_digit(charcode) {
  function is_delimeter (line 442) | function is_delimeter(charcode) {
  function is_delimeter_or_number (line 455) | function is_delimeter_or_number(charcode) {
  function is_alnum (line 462) | function is_alnum(charcode) {
  function parser_write (line 473) | function parser_write(p, chunk) {
  function default_renderer (line 1530) | function default_renderer(root) {
  function default_add_token (line 1544) | function default_add_token(data, type) {
  function default_end_token (line 1611) | function default_end_token(data) {
  function default_add_text (line 1616) | function default_add_text(data, text) {
  function default_set_attr (line 1621) | function default_set_attr(data, type, value) {
  function logger_renderer (line 1637) | function logger_renderer() {
  function logger_add_token (line 1648) | function logger_add_token(data, type) {
  function logger_end_token (line 1653) | function logger_end_token(data) {
  function logger_add_text (line 1658) | function logger_add_text(data, text) {
  function logger_set_attr (line 1663) | function logger_set_attr(data, type, value) {

FILE: src/ui/listen/ListenView.js
  class ListenView (line 5) | class ListenView extends LitElement {
    method constructor (line 432) | constructor() {
    method connectedCallback (line 450) | connectedCallback() {
    method disconnectedCallback (line 482) | disconnectedCallback() {
    method startTimer (line 495) | startTimer() {
    method stopTimer (line 508) | stopTimer() {
    method adjustWindowHeight (line 515) | adjustWindowHeight() {
    method toggleViewMode (line 546) | toggleViewMode() {
    method handleCopyHover (line 551) | handleCopyHover(isHovering) {
    method handleCopy (line 561) | async handleCopy() {
    method adjustWindowHeightThrottled (line 594) | adjustWindowHeightThrottled() {
    method updated (line 608) | updated(changedProperties) {
    method handleSttMessagesUpdated (line 616) | handleSttMessagesUpdated(event) {
    method firstUpdated (line 621) | firstUpdated() {
    method render (line 626) | render() {

FILE: src/ui/listen/audioCore/aec.js
  function updateMemoryViews (line 7) | function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array...
  function preRun (line 7) | function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="func...
  function initRuntime (line 7) | function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!...
  function postRun (line 7) | function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="f...
  function addRunDependency (line 7) | function addRunDependency(id){runDependencies++;Module["monitorRunDepend...
  function removeRunDependency (line 7) | function removeRunDependency(id){runDependencies--;Module["monitorRunDep...
  function abort (line 7) | function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";...
  function findWasmBinary (line 7) | function findWasmBinary(){return base64Decode("AGFzbQEAAAABhgETYAJ/fwF/Y...
  function getBinarySync (line 7) | function getBinarySync(file){if(ArrayBuffer.isView(file)){return file}if...
  function getWasmBinary (line 7) | async function getWasmBinary(binaryFile){return getBinarySync(binaryFile)}
  function instantiateArrayBuffer (line 7) | async function instantiateArrayBuffer(binaryFile,imports){try{var binary...
  function instantiateAsync (line 7) | async function instantiateAsync(binary,binaryFile,imports){return instan...
  function getWasmImports (line 7) | function getWasmImports(){return{a:wasmImports}}
  function createWasm (line 7) | async function createWasm(){function receiveInstance(instance,module){wa...
  class ExitStatus (line 7) | class ExitStatus{name="ExitStatus";constructor(status){this.message=`Pro...
    method constructor (line 7) | constructor(status){this.message=`Program terminated with exit(${statu...
  class ExceptionInfo (line 7) | class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excP...
    method constructor (line 7) | constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}
    method set_type (line 7) | set_type(type){HEAPU32[this.ptr+4>>2]=type}
    method get_type (line 7) | get_type(){return HEAPU32[this.ptr+4>>2]}
    method set_destructor (line 7) | set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}
    method get_destructor (line 7) | get_destructor(){return HEAPU32[this.ptr+8>>2]}
    method set_caught (line 7) | set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}
    method get_caught (line 7) | get_caught(){return HEAP8[this.ptr+12]!=0}
    method set_rethrown (line 7) | set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}
    method get_rethrown (line 7) | get_rethrown(){return HEAP8[this.ptr+13]!=0}
    method init (line 7) | init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);thi...
    method set_adjusted_ptr (line 7) | set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}
    method get_adjusted_ptr (line 7) | get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}
  function ___syscall_getcwd (line 7) | function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=F...
  function trim (line 7) | function trim(arr){var start=0;for(;start<arr.length;start++){if(arr[sta...
  method init (line 7) | init(){}
  method shutdown (line 7) | shutdown(){}
  method register (line 7) | register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevi...
  method open (line 7) | open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.Er...
  method close (line 7) | close(stream){stream.tty.ops.fsync(stream.tty)}
  method fsync (line 7) | fsync(stream){stream.tty.ops.fsync(stream.tty)}
  method read (line 7) | read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.ge...
  method write (line 7) | write(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.p...
  method get_char (line 7) | get_char(tty){return FS_stdin_getChar()}
  method put_char (line 7) | put_char(tty,val){if(val===null||val===10){out(UTF8ArrayToString(tty.out...
  method fsync (line 7) | fsync(tty){if(tty.output?.length>0){out(UTF8ArrayToString(tty.output));t...
  method ioctl_tcgets (line 7) | ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:353...
  method ioctl_tcsets (line 7) | ioctl_tcsets(tty,optional_actions,data){return 0}
  method ioctl_tiocgwinsz (line 7) | ioctl_tiocgwinsz(tty){return[24,80]}
  method put_char (line 7) | put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.out...
  method fsync (line 7) | fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));t...
  method mount (line 7) | mount(mount){return MEMFS.createNode(null,"/",16895,0)}
  method createNode (line 7) | createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){...
  method getFileDataAsTypedArray (line 7) | getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0)...
  method expandFileStorage (line 7) | expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node....
  method resizeFileStorage (line 7) | resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(new...
  method getattr (line 7) | getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr...
  method setattr (line 7) | setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(...
  method lookup (line 7) | lookup(parent,name){throw MEMFS.doesNotExistError}
  method mknod (line 7) | mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)}
  method rename (line 7) | rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNod...
  method unlink (line 7) | unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mti...
  method rmdir (line 7) | rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node...
  method readdir (line 7) | readdir(node){return[".","..",...Object.keys(node.contents)]}
  method symlink (line 7) | symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname...
  method readlink (line 7) | readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}ret...
  method read (line 7) | read(stream,buffer,offset,length,position){var contents=stream.node.cont...
  method write (line 7) | write(stream,buffer,offset,length,position,canOwn){if(buffer.buffer===HE...
  method llseek (line 7) | llseek(stream,offset,whence){var position=offset;if(whence===1){position...
  method mmap (line 7) | mmap(stream,length,position,prot,flags){if(!FS.isFile(stream.node.mode))...
  method msync (line 7) | msync(stream,buffer,offset,length,mmapFlags){MEMFS.stream_ops.write(stre...
  function processData (line 7) | function processData(byteArray){function finish(byteArray){preFinish?.()...
  method constructor (line 7) | constructor(errno){this.errno=errno}
  method object (line 7) | get object(){return this.node}
  method object (line 7) | set object(val){this.node=val}
  method isRead (line 7) | get isRead(){return(this.flags&2097155)!==1}
  method isWrite (line 7) | get isWrite(){return(this.flags&2097155)!==0}
  method isAppend (line 7) | get isAppend(){return this.flags&1024}
  method flags (line 7) | get flags(){return this.shared.flags}
  method flags (line 7) | set flags(val){this.shared.flags=val}
  method position (line 7) | get position(){return this.shared.position}
  method position (line 7) | set position(val){this.shared.position=val}
  method constructor (line 7) | constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=p...
  method read (line 7) | get read(){return(this.mode&this.readMode)===this.readMode}
  method read (line 7) | set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}
  method write (line 7) | get write(){return(this.mode&this.writeMode)===this.writeMode}
  method write (line 7) | set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}
  method isFolder (line 7) | get isFolder(){return FS.isDir(this.mode)}
  method isDevice (line 7) | get isDevice(){return FS.isChrdev(this.mode)}
  method lookupPath (line 7) | lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.foll...
  method getPath (line 7) | getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mo...
  method hashName (line 7) | hashName(parentid,name){var hash=0;for(var i=0;i<name.length;i++){hash=(...
  method hashAddNode (line 7) | hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.na...
  method hashRemoveNode (line 7) | hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(F...
  method lookupNode (line 7) | lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){thr...
  method createNode (line 7) | createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mod...
  method destroyNode (line 7) | destroyNode(node){FS.hashRemoveNode(node)}
  method isRoot (line 7) | isRoot(node){return node===node.parent}
  method isMountpoint (line 7) | isMountpoint(node){return!!node.mounted}
  method isFile (line 7) | isFile(mode){return(mode&61440)===32768}
  method isDir (line 7) | isDir(mode){return(mode&61440)===16384}
  method isLink (line 7) | isLink(mode){return(mode&61440)===40960}
  method isChrdev (line 7) | isChrdev(mode){return(mode&61440)===8192}
  method isBlkdev (line 7) | isBlkdev(mode){return(mode&61440)===24576}
  method isFIFO (line 7) | isFIFO(mode){return(mode&61440)===4096}
  method isSocket (line 7) | isSocket(mode){return(mode&49152)===49152}
  method flagsToPermissionString (line 7) | flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&5...
  method nodePermissions (line 7) | nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.i...
  method mayLookup (line 7) | mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermi...
  method mayCreate (line 7) | mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lo...
  method mayDelete (line 7) | mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catc...
  method mayOpen (line 7) | mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return ...
  method checkOpExists (line 7) | checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op}
  method nextfd (line 7) | nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){retu...
  method getStreamChecked (line 7) | getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new F...
  method createStream (line 7) | createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);...
  method closeStream (line 7) | closeStream(fd){FS.streams[fd]=null}
  method dupStream (line 7) | dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);st...
  method doSetAttr (line 7) | doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var a...
  method open (line 7) | open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops...
  method llseek (line 7) | llseek(){throw new FS.ErrnoError(70)}
  method registerDevice (line 7) | registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}}
  method getMounts (line 7) | getMounts(mount){var mounts=[];var check=[mount];while(check.length){var...
  method syncfs (line 7) | syncfs(populate,callback){if(typeof populate=="function"){callback=popul...
  method mount (line 7) | mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountp...
  method unmount (line 7) | unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:fa...
  method lookup (line 7) | lookup(parent,name){return parent.node_ops.lookup(parent,name)}
  method mknod (line 7) | mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var pa...
  method statfs (line 7) | statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)}
  method statfsStream (line 7) | statfsStream(stream){return FS.statfsNode(stream.node)}
  method statfsNode (line 7) | statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,ba...
  method create (line 7) | create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)}
  method mkdir (line 7) | mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode...
  method mkdirTree (line 7) | mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of di...
  method mkdev (line 7) | mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|...
  method symlink (line 7) | symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.Errn...
  method rename (line 7) | rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new...
  method rmdir (line 7) | rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=look...
  method readdir (line 7) | readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=look...
  method unlink (line 7) | unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=loo...
  method readlink (line 7) | readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!l...
  method stat (line 7) | stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow}...
  method fstat (line 7) | fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var ge...
  method lstat (line 7) | lstat(path){return FS.stat(path,true)}
  method doChmod (line 7) | doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode...
  method chmod (line 7) | chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var looku...
  method lchmod (line 7) | lchmod(path,mode){FS.chmod(path,mode,true)}
  method fchmod (line 7) | fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,str...
  method doChown (line 7) | doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date...
  method chown (line 7) | chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lo...
  method lchown (line 7) | lchown(path,uid,gid){FS.chown(path,uid,gid,true)}
  method fchown (line 7) | fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,...
  method doTruncate (line 7) | doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoEr...
  method truncate (line 7) | truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typ...
  method ftruncate (line 7) | ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.f...
  method utime (line 7) | utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var...
  method open (line 7) | open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flag...
  method close (line 7) | close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stre...
  method isClosed (line 7) | isClosed(stream){return stream.fd===null}
  method llseek (line 7) | llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoE...
  method read (line 7) | read(stream,buffer,offset,length,position){if(length<0||position<0){thro...
  method write (line 7) | write(stream,buffer,offset,length,position,canOwn){if(length<0||position...
  method mmap (line 7) | mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&...
  method msync (line 7) | msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync...
  method ioctl (line 7) | ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoErr...
  method readFile (line 7) | readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encod...
  method writeFile (line 7) | writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.op...
  method chdir (line 7) | chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node=...
  method createDefaultDirectories (line 7) | createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("...
  method createDefaultDevices (line 7) | createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3...
  method createSpecialDirectories (line 7) | createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/pr...
  method createStandardStreams (line 7) | createStandardStreams(input,output,error){if(input){FS.createDevice("/de...
  method staticInit (line 7) | staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.crea...
  method init (line 7) | init(input,output,error){FS.initialized=true;input??=Module["stdin"];out...
  method quit (line 7) | quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS....
  method findObject (line 7) | findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontRes...
  method analyzePath (line 7) | analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,...
  method createPath (line 7) | createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?...
  method createFile (line 7) | createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(...
  method createDataFile (line 7) | createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;i...
  method createDevice (line 7) | createDevice(parent,name,input,output){var path=PATH.join2(typeof parent...
  method forceLoadFile (line 7) | forceLoadFile(obj){if(obj.isDevice||obj.isFolder||obj.link||obj.contents...
  method createLazyFile (line 7) | createLazyFile(parent,name,url,canRead,canWrite){class LazyUint8Array{le...
  method calculateAt (line 7) | calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var ...
  method writeStat (line 7) | writeStat(buf,stat){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;H...
  method writeStatFs (line 7) | writeStatFs(buf,stats){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=st...
  method doMsync (line 7) | doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){t...
  method getStreamFromFD (line 7) | getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream}
  method getStr (line 7) | getStr(ptr){var ret=UTF8ToString(ptr);return ret}
  function _fd_close (line 7) | function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.cl...
  function _fd_seek (line 7) | function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(...
  function _fd_write (line 7) | function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStream...
  function convertReturnValue (line 7) | function convertReturnValue(ret){if(returnType==="string"){return UTF8To...
  function onDone (line 7) | function onDone(ret){if(stack!==0)stackRestore(stack);return convertRetu...
  function assignWasmExports (line 7) | function assignWasmExports(wasmExports){Module["_AecNew"]=_AecNew=wasmEx...
  function invoke_vi (line 7) | function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(in...
  function invoke_viii (line 7) | function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTable...
  function invoke_vii (line 7) | function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntr...
  function invoke_viiii (line 7) | function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmT...
  function invoke_iiii (line 7) | function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWa...
  function invoke_viiiii (line 7) | function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getW...
  function invoke_ii (line 7) | function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableE...
  function invoke_iiiiii (line 7) | function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{retu...
  function run (line 7) | function run(){if(runDependencies>0){dependenciesFulfilled=run;return}pr...
  function preInit (line 7) | function preInit(){if(Module["preInit"]){if(typeof Module["preInit"]=="f...

FILE: src/ui/listen/audioCore/listenCapture.js
  function getAec (line 8) | async function getAec () {
  constant SAMPLE_RATE (line 32) | const SAMPLE_RATE = 24000;
  constant AUDIO_CHUNK_DURATION (line 33) | const AUDIO_CHUNK_DURATION = 0.1;
  constant BUFFER_SIZE (line 34) | const BUFFER_SIZE = 4096;
  constant MAX_SYSTEM_BUFFER_SIZE (line 47) | const MAX_SYSTEM_BUFFER_SIZE = 10;
  function isVoiceActive (line 52) | function isVoiceActive(audioFloat32Array, threshold = 0.005) {
  function base64ToFloat32Array (line 68) | function base64ToFloat32Array(base64) {
  function convertFloat32ToInt16 (line 86) | function convertFloat32ToInt16(float32Array) {
  function arrayBufferToBase64 (line 96) | function arrayBufferToBase64(buffer) {
  function int16PtrFromFloat32 (line 107) | function int16PtrFromFloat32(mod, f32) {
  function float32FromInt16View (line 121) | function float32FromInt16View(i16) {
  function disposeAec (line 128) | function disposeAec () {
  function runAecSync (line 134) | function runAecSync(micF32, sysF32) {
  method addTokens (line 212) | addTokens(count, type = 'image') {
  method calculateImageTokens (line 223) | calculateImageTokens(width, height) {
  method trackAudioTokens (line 233) | trackAudioTokens() {
  method cleanOldTokens (line 250) | cleanOldTokens() {
  method getTokensInLastMinute (line 255) | getTokensInLastMinute() {
  method shouldThrottle (line 260) | shouldThrottle() {
  method reset (line 278) | reset() {
  function setupMicProcessing (line 292) | async function setupMicProcessing(micStream) {
  function setupLinuxMicProcessing (line 345) | function setupLinuxMicProcessing(micStream) {
  function setupSystemAudioProcessing (line 378) | function setupSystemAudioProcessing(systemStream) {
  function startCapture (line 417) | async function startCapture(screenshotIntervalSeconds = 5, imageQuality ...
  function stopCapture (line 574) | function stopCapture() {

FILE: src/ui/listen/stt/SttView.js
  class SttView (line 3) | class SttView extends LitElement {
    method constructor (line 86) | constructor() {
    method connectedCallback (line 96) | connectedCallback() {
    method disconnectedCallback (line 103) | disconnectedCallback() {
    method resetTranscript (line 111) | resetTranscript() {
    method handleSttUpdate (line 116) | handleSttUpdate(event, { speaker, text, isFinal, isPartial }) {
    method scrollToBottom (line 178) | scrollToBottom() {
    method getSpeakerClass (line 187) | getSpeakerClass(speaker) {
    method getTranscriptText (line 191) | getTranscriptText() {
    method updated (line 195) | updated(changedProperties) {
    method render (line 206) | render() {

FILE: src/ui/listen/summary/SummaryView.js
  class SummaryView (line 3) | class SummaryView extends LitElement {
    method constructor (line 242) | constructor() {
    method connectedCallback (line 263) | connectedCallback() {
    method disconnectedCallback (line 273) | disconnectedCallback() {
    method resetAnalysis (line 281) | resetAnalysis() {
    method loadLibraries (line 291) | async loadLibraries() {
    method loadScript (line 346) | loadScript(src) {
    method parseMarkdown (line 356) | parseMarkdown(text) {
    method handleMarkdownClick (line 371) | handleMarkdownClick(originalText) {
    method renderMarkdownContent (line 375) | renderMarkdownContent() {
    method handleRequestClick (line 406) | async handleRequestClick(requestText) {
    method getSummaryText (line 424) | getSummaryText() {
    method updated (line 447) | updated(changedProperties) {
    method render (line 452) | render() {

FILE: src/ui/settings/SettingsView.js
  class SettingsView (line 4) | class SettingsView extends LitElement {
    method constructor (line 511) | constructor() {
    method loadAutoUpdateSetting (line 544) | async loadAutoUpdateSetting() {
    method handleToggleAutoUpdate (line 559) | async handleToggleAutoUpdate() {
    method loadLocalAIStatus (line 578) | async loadLocalAIStatus() {
    method loadInitialData (line 611) | async loadInitialData() {
    method handleSaveKey (line 654) | async handleSaveKey(provider) {
    method handleClearKey (line 711) | async handleClearKey(provider) {
    method refreshModelData (line 720) | async refreshModelData() {
    method toggleModelList (line 735) | async toggleModelList(type) {
    method selectModel (line 752) | async selectModel(type, modelId) {
    method refreshOllamaStatus (line 785) | async refreshOllamaStatus() {
    method installOllamaModel (line 793) | async installOllamaModel(modelName) {
    method downloadWhisperModel (line 835) | async downloadWhisperModel(modelId) {
    method getProviderForModel (line 890) | getProviderForModel(type, modelId) {
    method handleUsePicklesKey (line 901) | handleUsePicklesKey(e) {
    method openShortcutEditor (line 910) | openShortcutEditor() {
    method connectedCallback (line 914) | connectedCallback() {
    method disconnectedCallback (line 925) | disconnectedCallback() {
    method setupEventListeners (line 940) | setupEventListeners() {
    method cleanupEventListeners (line 945) | cleanupEventListeners() {
    method setupIpcListeners (line 950) | setupIpcListeners() {
    method cleanupIpcListeners (line 1000) | cleanupIpcListeners() {
    method setupWindowResize (line 1017) | setupWindowResize() {
    method cleanupWindowResize (line 1028) | cleanupWindowResize() {
    method updateScrollHeight (line 1034) | updateScrollHeight() {
    method getMainShortcuts (line 1059) | getMainShortcuts() {
    method renderShortcutKeys (line 1068) | renderShortcutKeys(accelerator) {
    method togglePresets (line 1087) | togglePresets() {
    method handlePresetSelect (line 1091) | async handlePresetSelect(preset) {
    method handleMoveLeft (line 1097) | handleMoveLeft() {
    method handleMoveRight (line 1102) | handleMoveRight() {
    method handlePersonalize (line 1107) | async handlePersonalize() {
    method handleToggleInvisibility (line 1116) | async handleToggleInvisibility() {
    method handleSaveApiKey (line 1122) | async handleSaveApiKey() {
    method handleQuit (line 1141) | handleQuit() {
    method handleFirebaseLogout (line 1146) | handleFirebaseLogout() {
    method handleOllamaShutdown (line 1151) | async handleOllamaShutdown() {
    method render (line 1180) | render() {

FILE: src/ui/settings/ShortCutSettingsView.js
  class ShortcutSettingsView (line 16) | class ShortcutSettingsView extends LitElement {
    method constructor (line 99) | constructor() {
    method connectedCallback (line 107) | connectedCallback() {
    method disconnectedCallback (line 117) | disconnectedCallback() {
    method handleKeydown (line 124) | handleKeydown(e, shortcutKey){
    method _parseAccelerator (line 140) | _parseAccelerator(e){
    method startCapture (line 161) | startCapture(key){ this.capturingKey = key; this.feedback = {...this.f...
    method disableShortcut (line 163) | disableShortcut(key){
    method stopCapture (line 168) | stopCapture() {
    method handleSave (line 172) | async handleSave() {
    method handleClose (line 181) | handleClose() {
    method handleResetToDefault (line 187) | async handleResetToDefault() {
    method formatShortcutName (line 200) | formatShortcutName(name) {
    method render (line 208) | render(){

FILE: src/window/smoothMovementManager.js
  class SmoothMovementManager (line 3) | class SmoothMovementManager {
    method constructor (line 4) | constructor(windowPool) {
    method _isWindowValid (line 22) | _isWindowValid(win) {
    method animateWindow (line 44) | animateWindow(win, targetX, targetY, options = {}) {
    method fade (line 81) | fade(win, { from, to, duration = 250, onComplete }) {
    method animateWindowBounds (line 107) | animateWindowBounds(win, targetBounds, options = {}) {
    method animateWindowPosition (line 157) | animateWindowPosition(win, targetPosition, options = {}) {
    method animateLayout (line 167) | animateLayout(layout, animated = true) {
    method destroy (line 182) | destroy() {

FILE: src/window/windowLayoutManager.js
  function getCurrentDisplay (line 8) | function getCurrentDisplay(window) {
  class WindowLayoutManager (line 20) | class WindowLayoutManager {
    method constructor (line 24) | constructor(windowPool) {
    method determineLayoutStrategy (line 44) | determineLayoutStrategy(headerBounds, screenWidth, screenHeight, relat...
    method calculateSettingsWindowPosition (line 71) | calculateSettingsWindowPosition() {
    method calculateHeaderResize (line 97) | calculateHeaderResize(header, { width, height }) {
    method calculateClampedPosition (line 108) | calculateClampedPosition(header, { x: newX, y: newY }) {
    method calculateWindowHeightAdjustment (line 118) | calculateWindowHeightAdjustment(senderWindow, targetHeight) {
    method calculateFeatureWindowLayout (line 132) | calculateFeatureWindowLayout(visibility, headerBoundsOverride = null) {
    method calculateShortcutSettingsWindowPosition (line 222) | calculateShortcutSettingsWindowPosition() {
    method calculateStepMovePosition (line 240) | calculateStepMovePosition(header, direction) {
    method calculateEdgePosition (line 257) | calculateEdgePosition(header, direction) {
    method calculateNewPositionForDisplay (line 275) | calculateNewPositionForDisplay(window, targetDisplayId) {
    method boundsOverlap (line 303) | boundsOverlap(bounds1, bounds2) {

FILE: src/window/windowManager.js
  function updateChildWindowLayouts (line 44) | function updateChildWindowLayouts(animated = true) {
  function setupWindowController (line 104) | function setupWindowController(windowPool, layoutManager, movementManage...
  function changeAllWindowsVisibility (line 217) | function changeAllWindowsVisibility(windowPool, targetVisibility) {
  function handleWindowVisibilityRequest (line 260) | async function handleWindowVisibilityRequest(windowPool, layoutManager, ...
  function createFeatureWindows (line 435) | function createFeatureWindows(header, namesToCreate) {
  function destroyFeatureWindows (line 611) | function destroyFeatureWindows() {
  function getCurrentDisplay (line 626) | function getCurrentDisplay(window) {
  function createWindows (line 640) | function createWindows() {
  function setupIpcHandlers (line 753) | function setupIpcHandlers(windowPool, layoutManager) {
Condensed preview — 153 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,424K chars).
[
  {
    "path": ".firebaserc",
    "chars": 54,
    "preview": "{\n  \"projects\": {\n    \"default\": \"pickle-3651a\"\n  }\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 661,
    "preview": "---\nname: Bug Report\nabout: Create a report to help us improve\ntitle: \"[BUG] \"\nlabels: bug\nassignees: ''\n\n---\n\n**Describ"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 607,
    "preview": "---\nname: Feature Request\nabout: Suggest an idea for this project\ntitle: \"[FEAT] \"\nlabels: feature\nassignees: ''\n\n---\n\n*"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 1119,
    "preview": "---\nname: Pull Request\nabout: Propose a change to the codebase\n---\n\n## Summary of Changes\n\nPlease provide a brief, high-"
  },
  {
    "path": ".github/workflows/assign-on-comment.yml",
    "chars": 2301,
    "preview": "name: Assign on Comment\n\non:\n  issue_comment:\n    types: [created]\n\njobs:\n  # Job 1: Any contributor can self-assign\n  s"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1314,
    "preview": "name: Build & Verify\n\non:\n  push:\n    branches: [ \"main\" ] # Runs on every push to main branch\n\njobs:\n  build:\n    # Cur"
  },
  {
    "path": ".gitignore",
    "chars": 1485,
    "preview": "# Logs\nsrc/data\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https:"
  },
  {
    "path": ".gitmodules",
    "chars": 71,
    "preview": "[submodule \"aec\"]\n\tpath = aec\n\turl = https://github.com/samtiz/aec.git\n"
  },
  {
    "path": ".npmrc",
    "chars": 60,
    "preview": "better-sqlite3:ignore-scripts=true\nsharp:ignore-scripts=true"
  },
  {
    "path": ".prettierignore",
    "chars": 27,
    "preview": "src/ui/assets\nnode_modules\n"
  },
  {
    "path": ".prettierrc",
    "chars": 195,
    "preview": "{\n    \"semi\": true,\n    \"tabWidth\": 4,\n    \"printWidth\": 150,\n    \"singleQuote\": true,\n    \"trailingComma\": \"es5\",\n    \""
  },
  {
    "path": ".vscode/settings.json",
    "chars": 37,
    "preview": "{\n    \"search.useIgnoreFiles\": true\n}"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3550,
    "preview": "# Contributing to Glass\n\nThank you for considering contributing to **Glass by Pickle**! Contributions make the open-sour"
  },
  {
    "path": "LICENSE",
    "chars": 35148,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.md",
    "chars": 5389,
    "preview": "<p align=\"center\">\n  <a href=\"https://pickle.com/glass\">\n   <img src=\"./public/assets/banner.gif\" alt=\"Logo\">\n  </a>\n\n  "
  },
  {
    "path": "build.js",
    "chars": 1484,
    "preview": "const esbuild = require('esbuild');\nconst path = require('path');\n\nconst baseConfig = {\n    bundle: true,\n    platform: "
  },
  {
    "path": "docs/DESIGN_PATTERNS.md",
    "chars": 8910,
    "preview": "# Glass: Design Patterns and Architectural Overview\n\nWelcome to the Glass project! This document is the definitive guide"
  },
  {
    "path": "docs/refactor-plan.md",
    "chars": 1308,
    "preview": "# Refactor Plan: Non-Window Logic Migration from windowManager.js\n\n## Goal\n`windowManager.js`를 순수 창 관리 모듈로 만들기 위해 비즈니스 로"
  },
  {
    "path": "electron-builder.yml",
    "chars": 1990,
    "preview": "# electron-builder.yml\n\n# The unique application ID\nappId: com.pickle.glass\n\n# The user-facing application name\nproductN"
  },
  {
    "path": "entitlements.plist",
    "chars": 954,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "firebase.json",
    "chars": 666,
    "preview": "{\n  \"functions\": [\n    {\n      \"source\": \"functions\",\n      \"codebase\": \"pickle-glass\",\n      \"ignore\": [\n        \"node_"
  },
  {
    "path": "firestore.indexes.json",
    "chars": 44,
    "preview": "{\n  \"indexes\": [],\n  \"fieldOverrides\": []\n} "
  },
  {
    "path": "functions/.eslintrc.js",
    "chars": 492,
    "preview": "module.exports = {\n  env: {\n    es6: true,\n    node: true,\n  },\n  parserOptions: {\n    \"ecmaVersion\": 2018,\n  },\n  exten"
  },
  {
    "path": "functions/.gitignore",
    "chars": 21,
    "preview": "node_modules/\n*.local"
  },
  {
    "path": "functions/index.js",
    "chars": 2661,
    "preview": "/**\n * Import function triggers from their respective submodules:\n *\n * const {onCall} = require(\"firebase-functions/v2/"
  },
  {
    "path": "functions/package.json",
    "chars": 666,
    "preview": "{\n  \"name\": \"functions\",\n  \"description\": \"Cloud Functions for Firebase\",\n  \"scripts\": {\n    \"lint\": \"eslint . --fix\",\n "
  },
  {
    "path": "notarize.js",
    "chars": 849,
    "preview": "const { notarize } = require('@electron/notarize');\n\nexports.notarizeApp = async function (context) {\n  if (context.elec"
  },
  {
    "path": "package.json",
    "chars": 2531,
    "preview": "{\n    \"name\": \"pickle-glass\",\n    \"productName\": \"Glass\",\n    \"version\": \"0.2.4\",\n    \"description\": \"Cl*ely for Free\",\n"
  },
  {
    "path": "pickleglass_web/app/activity/details/page.tsx",
    "chars": 9124,
    "preview": "'use client'\n\nimport { useState, useEffect, Suspense } from 'react'\nimport { useRedirectIfNotAuth } from '@/utils/auth'\n"
  },
  {
    "path": "pickleglass_web/app/activity/page.tsx",
    "chars": 4817,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport Link from 'next/link'\nimport { useRedirectIfNotAuth } f"
  },
  {
    "path": "pickleglass_web/app/download/page.tsx",
    "chars": 4360,
    "preview": "'use client'\n\nimport { Download, Smartphone, Monitor, Tablet } from 'lucide-react'\nimport { useRedirectIfNotAuth } from "
  },
  {
    "path": "pickleglass_web/app/globals.css",
    "chars": 633,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --foreground-rgb: 0, 0, 0;\n  --background-start-rg"
  },
  {
    "path": "pickleglass_web/app/help/page.tsx",
    "chars": 4572,
    "preview": "'use client'\n\nimport { HelpCircle, Book, MessageCircle, Mail } from 'lucide-react'\nimport { useRedirectIfNotAuth } from "
  },
  {
    "path": "pickleglass_web/app/layout.tsx",
    "chars": 556,
    "preview": "import './globals.css'\nimport { Inter } from 'next/font/google'\nimport ClientLayout from '@/components/ClientLayout'\n\nco"
  },
  {
    "path": "pickleglass_web/app/login/page.tsx",
    "chars": 4857,
    "preview": "'use client'\n\nimport { useRouter } from 'next/navigation'\nimport { GoogleAuthProvider, signInWithPopup } from 'firebase/"
  },
  {
    "path": "pickleglass_web/app/page.tsx",
    "chars": 538,
    "preview": "'use client'\n\nimport { useEffect } from 'react'\nimport { useRouter } from 'next/navigation'\n\nexport default function Hom"
  },
  {
    "path": "pickleglass_web/app/personalize/page.tsx",
    "chars": 9612,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { ChevronDown, Plus, Copy } from 'lucide-react'\nimport "
  },
  {
    "path": "pickleglass_web/app/settings/billing/page.tsx",
    "chars": 1897,
    "preview": "'use client'\n\nimport { useRedirectIfNotAuth } from '@/utils/auth'\n\nexport default function BillingPage() {\n  const userI"
  },
  {
    "path": "pickleglass_web/app/settings/page.tsx",
    "chars": 20561,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport { Check, ExternalLink, Cloud, HardDrive } from 'lucide-"
  },
  {
    "path": "pickleglass_web/app/settings/privacy/page.tsx",
    "chars": 3559,
    "preview": "'use client'\n\nimport { ExternalLink } from 'lucide-react'\nimport { useRedirectIfNotAuth } from '@/utils/auth'\n\nexport de"
  },
  {
    "path": "pickleglass_web/backend_node/index.js",
    "chars": 1696,
    "preview": "const express = require('express');\nconst cors = require('cors');\n// const db = require('./db'); // No longer needed\ncon"
  },
  {
    "path": "pickleglass_web/backend_node/ipcBridge.js",
    "chars": 1182,
    "preview": "const crypto = require('crypto');\n\nfunction ipcRequest(req, channel, payload) {\n    return new Promise((resolve, reject)"
  },
  {
    "path": "pickleglass_web/backend_node/middleware/auth.js",
    "chars": 234,
    "preview": "function identifyUser(req, res, next) {\n    const userId = req.get('X-User-ID');\n\n    if (userId) {\n        req.uid = us"
  },
  {
    "path": "pickleglass_web/backend_node/routes/auth.js",
    "chars": 722,
    "preview": "const express = require('express');\nconst router = express.Router();\nconst { ipcRequest } = require('../ipcBridge');\n\nro"
  },
  {
    "path": "pickleglass_web/backend_node/routes/conversations.js",
    "chars": 2005,
    "preview": "const express = require('express');\nconst router = express.Router();\nconst { ipcRequest } = require('../ipcBridge');\n\nro"
  },
  {
    "path": "pickleglass_web/backend_node/routes/presets.js",
    "chars": 1535,
    "preview": "const express = require('express');\nconst router = express.Router();\nconst { ipcRequest } = require('../ipcBridge');\n\nro"
  },
  {
    "path": "pickleglass_web/backend_node/routes/user.js",
    "chars": 3084,
    "preview": "const express = require('express');\nconst router = express.Router();\nconst { ipcRequest } = require('../ipcBridge');\n\nro"
  },
  {
    "path": "pickleglass_web/components/ClientLayout.tsx",
    "chars": 1131,
    "preview": "'use client'\n\nimport { useState, useEffect } from 'react'\nimport Sidebar from '@/components/Sidebar'\nimport SearchPopup "
  },
  {
    "path": "pickleglass_web/components/SearchPopup.tsx",
    "chars": 5241,
    "preview": "'use client'\n\nimport { useState, useEffect, useRef } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { S"
  },
  {
    "path": "pickleglass_web/components/Sidebar.tsx",
    "chars": 28201,
    "preview": "'use client';\n\nimport Link from 'next/link';\nimport { usePathname, useRouter } from 'next/navigation';\nimport Image from"
  },
  {
    "path": "pickleglass_web/next-env.d.ts",
    "chars": 228,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "pickleglass_web/next.config.js",
    "chars": 191,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  swcMinify: true,\n  output: 'exp"
  },
  {
    "path": "pickleglass_web/package.json",
    "chars": 721,
    "preview": "{\n  \"name\": \"pickleglass-frontend\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \""
  },
  {
    "path": "pickleglass_web/postcss.config.js",
    "chars": 82,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n} "
  },
  {
    "path": "pickleglass_web/public/README.md",
    "chars": 447,
    "preview": "# Public Assets\n\nThis folder contains static files.\n\n## Logo Image\n\n**@symbol.svg** - Logo image for the pickleglass app"
  },
  {
    "path": "pickleglass_web/requirements.txt",
    "chars": 271,
    "preview": "fastapi==0.104.1\nuvicorn[standard]==0.24.0\naiosqlite==0.19.0\npydantic==2.5.0\npython-dotenv==1.0.0\npython-multipart==0.0."
  },
  {
    "path": "pickleglass_web/tailwind.config.js",
    "chars": 439,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    './pages/**/*.{js,ts,jsx,tsx,mdx}',\n    "
  },
  {
    "path": "pickleglass_web/tsconfig.json",
    "chars": 591,
    "preview": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"es6\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \""
  },
  {
    "path": "pickleglass_web/utils/api.ts",
    "chars": 17129,
    "preview": "import { auth as firebaseAuth } from './firebase';\nimport { \n  FirestoreUserService, \n  FirestoreSessionService, \n  Fire"
  },
  {
    "path": "pickleglass_web/utils/auth.ts",
    "chars": 2140,
    "preview": "import { useEffect, useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport { UserProfile, setUserInf"
  },
  {
    "path": "pickleglass_web/utils/firebase.ts",
    "chars": 834,
    "preview": "// Import the functions you need from the SDKs you need\nimport { initializeApp, getApp, getApps } from \"firebase/app\";\ni"
  },
  {
    "path": "pickleglass_web/utils/firestore.ts",
    "chars": 8764,
    "preview": "import { \n  doc, \n  collection, \n  addDoc,\n  getDoc, \n  getDocs, \n  setDoc, \n  updateDoc, \n  deleteDoc, \n  query, \n  whe"
  },
  {
    "path": "preload.js",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/bridge/featureBridge.js",
    "chars": 12998,
    "preview": "// src/bridge/featureBridge.js\nconst { ipcMain, app, BrowserWindow } = require('electron');\nconst settingsService = requ"
  },
  {
    "path": "src/bridge/internalBridge.js",
    "chars": 304,
    "preview": "// src/bridge/internalBridge.js\nconst { EventEmitter } = require('events');\n\n// FeatureCore와 WindowCore를 잇는 내부 이벤트 버스\nco"
  },
  {
    "path": "src/bridge/windowBridge.js",
    "chars": 1911,
    "preview": "// src/bridge/windowBridge.js\nconst { ipcMain, shell } = require('electron');\n\n// Bridge는 단순히 IPC 핸들러를 등록하는 역할만 함 (비즈니스 "
  },
  {
    "path": "src/features/ask/askService.js",
    "chars": 16991,
    "preview": "const { BrowserWindow } = require('electron');\nconst { createStreamingLLM } = require('../common/ai/factory');\n// Lazy r"
  },
  {
    "path": "src/features/ask/repositories/firebase.repository.js",
    "chars": 1344,
    "preview": "const { collection, addDoc, query, getDocs, orderBy, Timestamp } = require('firebase/firestore');\nconst { getFirestoreIn"
  },
  {
    "path": "src/features/ask/repositories/index.js",
    "chars": 899,
    "preview": "const sqliteRepository = require('./sqlite.repository');\nconst firebaseRepository = require('./firebase.repository');\nco"
  },
  {
    "path": "src/features/ask/repositories/sqlite.repository.js",
    "chars": 1013,
    "preview": "const sqliteClient = require('../../common/services/sqliteClient');\n\nfunction addAiMessage({ uid, sessionId, role, conte"
  },
  {
    "path": "src/features/common/ai/factory.js",
    "chars": 5429,
    "preview": "// factory.js\n\n/**\n * @typedef {object} ModelOption\n * @property {string} id \n * @property {string} name\n */\n\n/**\n * @ty"
  },
  {
    "path": "src/features/common/ai/providers/anthropic.js",
    "chars": 10784,
    "preview": "const { Anthropic } = require(\"@anthropic-ai/sdk\")\n\nclass AnthropicProvider {\n    static async validateApiKey(key) {\n   "
  },
  {
    "path": "src/features/common/ai/providers/deepgram.js",
    "chars": 3390,
    "preview": "// providers/deepgram.js\n\nconst { createClient, LiveTranscriptionEvents } = require('@deepgram/sdk');\nconst WebSocket = "
  },
  {
    "path": "src/features/common/ai/providers/gemini.js",
    "chars": 10235,
    "preview": "const { GoogleGenerativeAI } = require(\"@google/generative-ai\")\nconst { GoogleGenAI } = require(\"@google/genai\")\n\nclass "
  },
  {
    "path": "src/features/common/ai/providers/ollama.js",
    "chars": 12528,
    "preview": "const http = require('http');\nconst fetch = require('node-fetch');\n\n// Request Queue System for Ollama API (only for non"
  },
  {
    "path": "src/features/common/ai/providers/openai.js",
    "chars": 9725,
    "preview": "const OpenAI = require('openai');\nconst WebSocket = require('ws');\nconst { Portkey } = require('portkey-ai');\nconst { Re"
  },
  {
    "path": "src/features/common/ai/providers/whisper.js",
    "chars": 8184,
    "preview": "let spawn, path, EventEmitter;\n\nif (typeof window === 'undefined') {\n    spawn = require('child_process').spawn;\n    pat"
  },
  {
    "path": "src/features/common/config/checksums.js",
    "chars": 2355,
    "preview": "const DOWNLOAD_CHECKSUMS = {\n    ollama: {\n        dmg: {\n            url: 'https://ollama.com/download/Ollama.dmg',\n   "
  },
  {
    "path": "src/features/common/config/config.js",
    "chars": 4893,
    "preview": "// Configuration management for environment-based settings\nconst os = require('os');\nconst path = require('path');\nconst"
  },
  {
    "path": "src/features/common/config/schema.js",
    "chars": 4656,
    "preview": "const LATEST_SCHEMA = {\n    users: {\n        columns: [\n            { name: 'uid', type: 'TEXT PRIMARY KEY' },\n         "
  },
  {
    "path": "src/features/common/prompts/promptBuilder.js",
    "chars": 792,
    "preview": "const { profilePrompts } = require('./promptTemplates.js');\n\nfunction buildSystemPrompt(promptParts, customPrompt = '', "
  },
  {
    "path": "src/features/common/prompts/promptTemplates.js",
    "chars": 24586,
    "preview": "const profilePrompts = {\n    interview: {\n        intro: `You are the user's live-meeting co-pilot called Pickle, develo"
  },
  {
    "path": "src/features/common/repositories/firestoreConverter.js",
    "chars": 2559,
    "preview": "const encryptionService = require('../services/encryptionService');\nconst { Timestamp } = require('firebase/firestore');"
  },
  {
    "path": "src/features/common/repositories/ollamaModel/index.js",
    "chars": 874,
    "preview": "const sqliteRepository = require('./sqlite.repository');\n\n// For now, we only use SQLite repository\n// In the future, we"
  },
  {
    "path": "src/features/common/repositories/ollamaModel/sqlite.repository.js",
    "chars": 3815,
    "preview": "const sqliteClient = require('../../services/sqliteClient');\n\n/**\n * Get all Ollama models\n */\nfunction getAllModels() {"
  },
  {
    "path": "src/features/common/repositories/permission/index.js",
    "chars": 386,
    "preview": "const sqliteRepository = require('./sqlite.repository');\n\n// This repository is not user-specific, so we always return s"
  },
  {
    "path": "src/features/common/repositories/permission/sqlite.repository.js",
    "chars": 524,
    "preview": "const sqliteClient = require('../../services/sqliteClient');\n\nfunction markKeychainCompleted(uid) {\n    return sqliteCli"
  },
  {
    "path": "src/features/common/repositories/preset/firebase.repository.js",
    "chars": 3413,
    "preview": "const { collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy, Timestamp } = require('fi"
  },
  {
    "path": "src/features/common/repositories/preset/index.js",
    "chars": 1113,
    "preview": "const sqliteRepository = require('./sqlite.repository');\nconst firebaseRepository = require('./firebase.repository');\nco"
  },
  {
    "path": "src/features/common/repositories/preset/sqlite.repository.js",
    "chars": 2332,
    "preview": "const sqliteClient = require('../../services/sqliteClient');\n\nfunction getPresets(uid) {\n    const db = sqliteClient.get"
  },
  {
    "path": "src/features/common/repositories/providerSettings/index.js",
    "chars": 1830,
    "preview": "const sqliteRepository = require('./sqlite.repository');\n\nfunction getBaseRepository() {\n    // For now, we only have sq"
  },
  {
    "path": "src/features/common/repositories/providerSettings/sqlite.repository.js",
    "chars": 5307,
    "preview": "const sqliteClient = require('../../services/sqliteClient');\nconst encryptionService = require('../../services/encryptio"
  },
  {
    "path": "src/features/common/repositories/session/firebase.repository.js",
    "chars": 5206,
    "preview": "const { doc, getDoc, collection, addDoc, query, where, getDocs, writeBatch, orderBy, limit, updateDoc, Timestamp } = req"
  },
  {
    "path": "src/features/common/repositories/session/index.js",
    "chars": 1807,
    "preview": "const sqliteRepository = require('./sqlite.repository');\nconst firebaseRepository = require('./firebase.repository');\n\nl"
  },
  {
    "path": "src/features/common/repositories/session/sqlite.repository.js",
    "chars": 4927,
    "preview": "const sqliteClient = require('../../services/sqliteClient');\n\nfunction getById(id) {\n    const db = sqliteClient.getDb()"
  },
  {
    "path": "src/features/common/repositories/user/firebase.repository.js",
    "chars": 3100,
    "preview": "const { doc, getDoc, setDoc, deleteDoc, writeBatch, query, where, getDocs, collection, Timestamp } = require('firebase/f"
  },
  {
    "path": "src/features/common/repositories/user/index.js",
    "chars": 1342,
    "preview": "const sqliteRepository = require('./sqlite.repository');\nconst firebaseRepository = require('./firebase.repository');\n\nl"
  },
  {
    "path": "src/features/common/repositories/user/sqlite.repository.js",
    "chars": 3152,
    "preview": "const sqliteClient = require('../../services/sqliteClient');\n\nfunction findOrCreate(user) {\n    const db = sqliteClient."
  },
  {
    "path": "src/features/common/repositories/whisperModel/index.js",
    "chars": 1545,
    "preview": "const BaseModelRepository = require('../baseModel');\n\nclass WhisperModelRepository extends BaseModelRepository {\n    con"
  },
  {
    "path": "src/features/common/services/authService.js",
    "chars": 8950,
    "preview": "const { onAuthStateChanged, signInWithCustomToken, signOut } = require('firebase/auth');\nconst { BrowserWindow, shell } "
  },
  {
    "path": "src/features/common/services/databaseInitializer.js",
    "chars": 8240,
    "preview": "const { app } = require('electron');\nconst path = require('path');\nconst fs = require('fs');\nconst sqliteClient = requir"
  },
  {
    "path": "src/features/common/services/encryptionService.js",
    "chars": 6408,
    "preview": "const crypto = require('crypto');\nlet keytar;\n\n// Dynamically import keytar, as it's an optional dependency.\ntry {\n    k"
  },
  {
    "path": "src/features/common/services/firebaseClient.js",
    "chars": 4003,
    "preview": "const { initializeApp } = require('firebase/app');\nconst { initializeAuth } = require('firebase/auth');\nconst Store = re"
  },
  {
    "path": "src/features/common/services/localAIManager.js",
    "chars": 21459,
    "preview": "const { EventEmitter } = require('events');\nconst ollamaService = require('./ollamaService');\nconst whisperService = req"
  },
  {
    "path": "src/features/common/services/migrationService.js",
    "chars": 9208,
    "preview": "const { doc, writeBatch, Timestamp } = require('firebase/firestore');\nconst { getFirestoreInstance } = require('../servi"
  },
  {
    "path": "src/features/common/services/modelStateService.js",
    "chars": 18332,
    "preview": "const { EventEmitter } = require('events');\nconst Store = require('electron-store');\nconst { PROVIDERS, getProviderClass"
  },
  {
    "path": "src/features/common/services/ollamaService.js",
    "chars": 59253,
    "preview": "const { EventEmitter } = require('events');\nconst { spawn, exec } = require('child_process');\nconst { promisify } = requ"
  },
  {
    "path": "src/features/common/services/permissionService.js",
    "chars": 4154,
    "preview": "const { systemPreferences, shell, desktopCapturer } = require('electron');\nconst permissionRepository = require('../repo"
  },
  {
    "path": "src/features/common/services/sqliteClient.js",
    "chars": 12590,
    "preview": "const Database = require('better-sqlite3');\nconst path = require('path');\nconst LATEST_SCHEMA = require('../config/schem"
  },
  {
    "path": "src/features/common/services/whisperService.js",
    "chars": 31027,
    "preview": "const { EventEmitter } = require('events');\nconst { spawn, exec } = require('child_process');\nconst { promisify } = requ"
  },
  {
    "path": "src/features/common/utils/spawnHelper.js",
    "chars": 1067,
    "preview": "const { spawn } = require('child_process');\n\nfunction spawnAsync(command, args = [], options = {}) {\n    return new Prom"
  },
  {
    "path": "src/features/listen/listenService.js",
    "chars": 12265,
    "preview": "const { BrowserWindow } = require('electron');\nconst SttService = require('./stt/sttService');\nconst SummaryService = re"
  },
  {
    "path": "src/features/listen/stt/repositories/firebase.repository.js",
    "chars": 1335,
    "preview": "const { collection, addDoc, query, getDocs, orderBy, Timestamp } = require('firebase/firestore');\nconst { getFirestoreIn"
  },
  {
    "path": "src/features/listen/stt/repositories/index.js",
    "chars": 780,
    "preview": "const sqliteRepository = require('./sqlite.repository');\nconst firebaseRepository = require('./firebase.repository');\nco"
  },
  {
    "path": "src/features/listen/stt/repositories/sqlite.repository.js",
    "chars": 987,
    "preview": "const sqliteClient = require('../../../common/services/sqliteClient');\n\nfunction addTranscript({ uid, sessionId, speaker"
  },
  {
    "path": "src/features/listen/stt/sttService.js",
    "chars": 30669,
    "preview": "const { BrowserWindow } = require('electron');\nconst { spawn } = require('child_process');\nconst { createSTT } = require"
  },
  {
    "path": "src/features/listen/summary/repositories/firebase.repository.js",
    "chars": 1802,
    "preview": "const { collection, doc, setDoc, getDoc, Timestamp } = require('firebase/firestore');\nconst { getFirestoreInstance } = r"
  },
  {
    "path": "src/features/listen/summary/repositories/index.js",
    "chars": 830,
    "preview": "const sqliteRepository = require('./sqlite.repository');\nconst firebaseRepository = require('./firebase.repository');\nco"
  },
  {
    "path": "src/features/listen/summary/repositories/sqlite.repository.js",
    "chars": 1563,
    "preview": "const sqliteClient = require('../../../common/services/sqliteClient');\n\nfunction saveSummary({ uid, sessionId, tldr, tex"
  },
  {
    "path": "src/features/listen/summary/summaryService.js",
    "chars": 13104,
    "preview": "const { BrowserWindow } = require('electron');\nconst { getSystemPrompt } = require('../../common/prompts/promptBuilder.j"
  },
  {
    "path": "src/features/settings/repositories/firebase.repository.js",
    "chars": 4829,
    "preview": "const { collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy } = require('firebase/fire"
  },
  {
    "path": "src/features/settings/repositories/index.js",
    "chars": 1464,
    "preview": "const sqliteRepository = require('./sqlite.repository');\nconst firebaseRepository = require('./firebase.repository');\nco"
  },
  {
    "path": "src/features/settings/repositories/sqlite.repository.js",
    "chars": 4600,
    "preview": "const sqliteClient = require('../../common/services/sqliteClient');\n\nfunction getPresets(uid) {\n    const db = sqliteCli"
  },
  {
    "path": "src/features/settings/settingsService.js",
    "chars": 14522,
    "preview": "const { ipcMain, BrowserWindow } = require('electron');\nconst Store = require('electron-store');\nconst authService = req"
  },
  {
    "path": "src/features/shortcuts/repositories/index.js",
    "chars": 49,
    "preview": "module.exports = require('./sqlite.repository'); "
  },
  {
    "path": "src/features/shortcuts/repositories/sqlite.repository.js",
    "chars": 1300,
    "preview": "const sqliteClient = require('../../common/services/sqliteClient');\nconst crypto = require('crypto');\n\nfunction getAllKe"
  },
  {
    "path": "src/features/shortcuts/shortcutsService.js",
    "chars": 11423,
    "preview": "const { globalShortcut, screen } = require('electron');\nconst shortcutsRepository = require('./repositories');\nconst int"
  },
  {
    "path": "src/index.js",
    "chars": 28027,
    "preview": "// try {\n//     const reloader = require('electron-reloader');\n//     reloader(module, {\n//     });\n// } catch (err) {\n/"
  },
  {
    "path": "src/preload.js",
    "chars": 16214,
    "preview": "// src/preload.js\nconst { contextBridge, ipcRenderer } = require('electron');\n\ncontextBridge.exposeInMainWorld('api', {\n"
  },
  {
    "path": "src/ui/app/ApiKeyHeader.js",
    "chars": 79365,
    "preview": "import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';\n\nexport class ApiKeyHeader extends LitElement {"
  },
  {
    "path": "src/ui/app/HeaderController.js",
    "chars": 11706,
    "preview": "import './MainHeader.js';\nimport './ApiKeyHeader.js';\nimport './PermissionHeader.js';\nimport './WelcomeHeader.js';\n\nclas"
  },
  {
    "path": "src/ui/app/MainHeader.js",
    "chars": 23838,
    "preview": "import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';\n\nexport class MainHeader extends LitElement {\n "
  },
  {
    "path": "src/ui/app/PermissionHeader.js",
    "chars": 23356,
    "preview": "import { LitElement, html, css } from '../assets/lit-core-2.7.4.min.js';\n\nexport class PermissionHeader extends LitEleme"
  },
  {
    "path": "src/ui/app/PickleGlassApp.js",
    "chars": 5808,
    "preview": "import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';\nimport { SettingsView } from '../settings/Setti"
  },
  {
    "path": "src/ui/app/WelcomeHeader.js",
    "chars": 7737,
    "preview": "import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';\n\nexport class WelcomeHeader extends LitElement "
  },
  {
    "path": "src/ui/app/content.html",
    "chars": 4998,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta http-equiv=\"content-security-policy\" content=\"script-src 'self' 'unsafe-"
  },
  {
    "path": "src/ui/app/header.html",
    "chars": 841,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta http-equiv=\"content-security-policy\" content=\"script-src 'self' 'unsafe-"
  },
  {
    "path": "src/ui/ask/AskView.js",
    "chars": 45720,
    "preview": "import { html, css, LitElement } from '../../ui/assets/lit-core-2.7.4.min.js';\nimport { parser, parser_write, parser_end"
  },
  {
    "path": "src/ui/assets/smd.js",
    "chars": 50115,
    "preview": "/*\nStreaming Markdown Parser and Renderer\nMIT License\nCopyright 2024 Damian Tarnawski\nhttps://github.com/thetarnav/strea"
  },
  {
    "path": "src/ui/listen/ListenView.js",
    "chars": 23287,
    "preview": "import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';\nimport './stt/SttView.js';\nimport './summary/Su"
  },
  {
    "path": "src/ui/listen/audioCore/aec.js",
    "chars": 212598,
    "preview": "var createAecModule = (() => {\n  var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefi"
  },
  {
    "path": "src/ui/listen/audioCore/listenCapture.js",
    "chars": 21823,
    "preview": "const createAecModule = require('./aec.js');\n\nlet aecModPromise = null;     // 한 번만 로드\nlet aecMod        = null;\nlet aec"
  },
  {
    "path": "src/ui/listen/audioCore/renderer.js",
    "chars": 995,
    "preview": "// renderer.js\nconst listenCapture = require('./listenCapture.js');\nconst params        = new URLSearchParams(window.loc"
  },
  {
    "path": "src/ui/listen/stt/SttView.js",
    "chars": 6606,
    "preview": "import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';\n\nexport class SttView extends LitElement {\n "
  },
  {
    "path": "src/ui/listen/summary/SummaryView.js",
    "chars": 18147,
    "preview": "import { html, css, LitElement } from '../../assets/lit-core-2.7.4.min.js';\n\nexport class SummaryView extends LitElement"
  },
  {
    "path": "src/ui/settings/SettingsView.js",
    "chars": 57522,
    "preview": "import { html, css, LitElement } from '../assets/lit-core-2.7.4.min.js';\n// import { getOllamaProgressTracker } from '.."
  },
  {
    "path": "src/ui/settings/ShortCutSettingsView.js",
    "chars": 10640,
    "preview": "import { html, css, LitElement } from '../../ui/assets/lit-core-2.7.4.min.js';\n\nconst commonSystemShortcuts = new Set([\n"
  },
  {
    "path": "src/ui/styles/glass-bypass.css",
    "chars": 328,
    "preview": "/*\n  이 파일은 body.has-glass 클래스가 적용되었을 때 모든 애니메이션, 트랜지션,\n  배경, 테두리 등을 비활성화하여 깨끗한 투명 효과(Glass)를 보장합니다.\n*/\nbody.has-glass * "
  },
  {
    "path": "src/window/smoothMovementManager.js",
    "chars": 6320,
    "preview": "const { screen } = require('electron');\n\nclass SmoothMovementManager {\n    constructor(windowPool) {\n        this.window"
  },
  {
    "path": "src/window/windowLayoutManager.js",
    "chars": 13360,
    "preview": "const { screen } = require('electron');\n\n/**\n * \n * @param {BrowserWindow} window \n * @returns {Display}\n */\nfunction ge"
  },
  {
    "path": "src/window/windowManager.js",
    "chars": 29331,
    "preview": "const { BrowserWindow, globalShortcut, screen, app, shell } = require('electron');\nconst WindowLayoutManager = require('"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the pickle-com/glass GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 153 files (1.3 MB), approximately 364.0k tokens, and a symbol index with 1197 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!