# Project export: Mosaic

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: Group trip planning app. Everyone swipes on local spots, AI creates itineraries matching your crew's vibe. Consensus made easy.
- Devpost: https://devpost.com/software/mosaic-09vqiu
- GitHub: https://github.com/purelyKai/Mosaic
- Video: https://www.youtube.com/embed/Aq5WXmQQooo?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Kevin R (29 commits), Joy (16 commits), Kai Black (14 commits), Seba (8 commits)

## Devpost submission (written by the team)

### Inspiration

Mosaic transforms group travel planning from chaos to consensus. Friends create a trip, invite their crew with a code, and everyone swipes through local restaurants, activities, and attractions, like Tinder for places. Our AI analyzes the group's collective preferences to generate personalized, day-by-day itineraries that satisfy everyone's tastes. No more endless group chats debating where to eat or what to do. Whether you're vegan, adventurous, or need the perfect coffee spot, Mosaic finds the hidden gems your entire group will love. Stop compromising. Start exploring together.

## README (from the GitHub repository)



# Mosaic

Mosaic is a mobile group travel planner app that allows group of friends to plan out their next trip. Mosaic utilizies a custom built recommendation system along with a friendly mobile app UI to allow users to plan their next trip with a few clicks.

Mosaic is built with React Native on the frontend, with tools such as Elastic Search, Supabase, and Flask powering the backend.

## 🚀 Features 🚀

- Custom feed of locations/places to visit based off user preferences
- Authentication via Google to allow for easy sign up
- Easy trip planning via Google Maps API to allow groups to easily input their travel destination
- Everything bundled in one easy to use mobile app 🔥

## ⚡️ Tech Stack ⚡️

- React Native
- TypeScript
- Flask
- Python
- ElasticSearch (Vector DB)
- Supabase
- Google APIS (Google places, Google Auth)

## 🏃 Setup and Run Locally 🏃

To run the project locally, follow these steps:

### Prerequisites

- Ensure you have the following installed:
  - [Node.js](https://nodejs.org/) (v16 or later)
  - [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/)
  - [Python](https://www.python.org/) (v3.9 or later)
  - [pip](https://pip.pypa.io/en/stable/)
  - [Expo CLI](https://docs.expo.dev/get-started/installation/)

### 1. Clone the Repository

```bash
git clone https://github.com/purelyKai/Mosaic.git
cd Mosaic
```

### 2. Set Up Environment Variables

#### Backend

- Copy the `.env.example` file to create a `.env` file in the `backend` directory:

```bash
cp backend/.env.example backend/.env
```

- Fill in the required values in the `backend/.env` file:
  - `ELASTIC_CLOUD_ID`
  - `ELASTIC_API_KEY`
  - `PLACES_API_KEY`
  - `OPENAI_API_KEY`
  - `HOST_IP`
  - `HOST_PORT`

#### Frontend

- Copy the `.env.example` file to create a `.env` file in the `mobile` directory:

```bash
cp mobile/.env.example mobile/.env
```

- Fill in the required values in the `mobile/.env` file:
  - `EXPO_PUBLIC_SUPABASE_URL`
  - `EXPO_PUBLIC_SUPABASE_ANON_KEY`
  - `EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID`
  - `EXPO_PUBLIC_API_BASE_URL`

### 3. Install Dependencies

#### Backend

```bash
cd backend
pip install -r requirements.txt
```

#### Mobile

```bash
cd ../mobile
npm install
```

### 4. Configure Elasticsearch Properties

- Ensure the Elasticsearch properties are set up correctly. Refer to the `elasticsearch_properties_example.txt` file in the `backend` directory for the required mappings:

```plaintext
{
  "mappings": {
    "properties": {
      "doc_type": {
        "type": "keyword"
      },
      "location": {
        "type": "geo_point"
      },
      "place_id": {
        "type": "keyword"
      },
      "place_name": {
        "type": "text"
      },
      "place_summary_text": {
        "type": "text"
      },
      "place_vec": {
        "type": "dense_vector",
        "dims": 512,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "bbq_hnsw",
          "m": 16,
          "ef_construction": 100,
          "rescore_vector": {
            "oversample": 3
          }
        }
      },
      "user_uuid": {
        "type": "keyword"
      },
      "user_vec": {
        "type": "dense_vector",
        "dims": 512,
        "index": true,
        "similarity": "cosine",
        "index_options": {
          "type": "bbq_hnsw",
          "m": 16,
          "ef_construction": 100,
          "rescore_vector": {
            "oversample": 3
          }
        }
      },
      "view_count": {
        "type": "integer"
      }
    }
  }
}
```

### 5. Run the Applications

#### Backend

- Start the Flask server:

```bash
cd backend
python app.py
```

#### Mobile

- Start the Expo development server:

```bash
cd ../mobile
npx expo start
```

### 6. Access the Applications

- Backend: The Flask server will run at `http://127.0.0.1:5000` by default.
- Mobile: Use the Expo Go app or an emulator to preview the app.

### Required APIs

The following APIs are required to run the project:

- **Elasticsearch**: For storing vectors containing information about users and locations.
- **Google Places API**: For fetching place details.
- **OpenAI API**: For generating vectors to store user preferences and other AI-driven features.
- **Supabase**: For authentication and database management.

## 📸 Project Images 📸

Here are some images showcasing the Mosaic project:

![Image 2](images/image2.png)
![Image 3](images/image3.png)
![Image 4](images/image4.png)
![Image 5](images/image5.png)


## 📄 License 📄 ##
This project is supported by the [MIT License](https://opensource.org/license/MIT).

## Detected evidence (automated analysis)

Indexed codebase: 34 recognized source files, 92 KB.
- Flask (technology) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (50 of 50)

```
.metals/metals.lock.db
.metals/metals.log
.metals/metals.mv.db
.vscode/settings.json
backend/.env.example
backend/.gitignore
backend/app.py
backend/elasticsearch_properties_example.txt
backend/find_places.py
backend/requirements.txt
LICENSE.txt
mobile/.env.example
mobile/.gitignore
mobile/.vscode/extensions.json
mobile/.vscode/settings.json
mobile/app.json
mobile/eslint.config.js
mobile/index.js
mobile/lib/supabase.ts
mobile/package.json
mobile/README.md
mobile/src/API/Elastic.tsx
mobile/src/App.tsx
mobile/src/components/GoogleLoginButton.tsx
mobile/src/components/GoogleLogoutButton.tsx
mobile/src/components/ImageBoard.tsx
mobile/src/components/LocationPicker.tsx
mobile/src/components/MasonryList.tsx
mobile/src/components/PlaceCard.tsx
mobile/src/components/PreferencesForm.tsx
mobile/src/components/SplashScreenController.tsx
mobile/src/constants/categories.ts
mobile/src/constants/theme.ts
mobile/src/context/AuthContext.tsx
mobile/src/context/CategoryContext.tsx
mobile/src/navigation/AppNavigator.tsx
mobile/src/navigation/types.ts
mobile/src/providers/AuthProvider.tsx
mobile/src/providers/CategoryProvider.tsx
mobile/src/screens/AuthScreen.tsx
mobile/src/screens/GroupsScreen.tsx
mobile/src/screens/MainScreen.tsx
mobile/src/screens/PreferencesScreen.tsx
mobile/src/services/tripService.tsx
mobile/src/types/group.types.ts
mobile/src/types/index.ts
mobile/src/types/place.types.ts
mobile/src/types/user.types.ts
mobile/tsconfig.json
README.md
```

### Dependencies

- backend/requirements.txt: elastic-transport@==9.2.0, elasticsearch@==9.1.1, Flask@==3.1.2, flask-cors@==6.0.1, google-api-core@==2.27.0, google-auth@==2.41.1, google-geo-type@==0.4.0, google-maps-places@==0.4.0, numpy@==2.3.4, openai@==2.6.1, python-dotenv@==1.1.1, requests@==2.32.5
- mobile/package.json: @expo/vector-icons@^15.0.3, @react-native-async-storage/async-storage@2.2.0, @react-native-google-signin/google-signin@^16.0.0, @react-navigation/bottom-tabs@^7.4.0, @react-navigation/elements@^2.6.3, @react-navigation/native@^7.1.8, @react-navigation/stack@^7.5.0, @supabase/supabase-js@^2.76.1, @types/react@~19.1.0, eslint@^9.25.0, eslint-config-expo@~10.0.0, expo@~54.0.20, expo-constants@~18.0.10, expo-dev-client@~6.0.16, expo-font@~14.0.9, expo-haptics@~15.0.7, expo-image@~3.0.10, expo-linking@~8.0.8, expo-location@^19.0.7, expo-router@~6.0.13, expo-secure-store@~15.0.7, expo-splash-screen@~31.0.10, expo-status-bar@~3.0.8, expo-symbols@~1.0.7, expo-system-ui@~6.0.8, expo-web-browser@~15.0.8, react@19.1.0, react-dom@19.1.0, react-native@0.81.5, react-native-gesture-handler@~2.28.0, react-native-google-places-autocomplete@^2.5.7, react-native-maps@^1.20.1, react-native-reanimated@~4.1.1, react-native-safe-area-context@~5.6.0, react-native-screens@~4.16.0, react-native-web@~0.21.0, react-native-worklets@0.5.1, typescript@~5.9.2

### Recent commits (newest first)

- updated readme lol
- liking on frontend
- Feed with real data
- Update Place Retrun Type
- fix: google photos images properly fetching in dihh
- consolidating hooks
- Update trip.tsx
- get -> post req
- app.py logging, frontend hooks
- WARNING: the image url doesn't actually work, but the API endpoint response is formatted correctly
- gl seb
- NEW API KEYS FREE FREE FREE
- fixxxx
- Merge pull request #12 from purelyKai/fixes/group_call
- added radius to find_location
- wip: group trip recommendation fetching
- Merge pull request #11 from purelyKai/features/agent
- fixed increasing view counts when a user likes stuff, also agent stuff had hands, I give up
- Merge pull request #10 from purelyKai/features/user_stuffz
- i just wanna sleep

## Key source files (fetched from GitHub, selected and truncated for size)

### backend/requirements.txt

```
elastic-transport==9.2.0
elasticsearch==9.1.1
Flask==3.1.2
flask-cors==6.0.1
google-api-core==2.27.0
google-auth==2.41.1
google-geo-type==0.4.0
google-maps-places==0.4.0
numpy==2.3.4
openai==2.6.1
python-dotenv==1.1.1
requests==2.32.5

```

### mobile/package.json

```
{
  "name": "mobile",
  "main": "index.js",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web",
    "lint": "expo lint"
  },
  "dependencies": {
    "@expo/vector-icons": "^15.0.3",
    "@react-native-async-storage/async-storage": "2.2.0",
    "@react-native-google-signin/google-signin": "^16.0.0",
    "@react-navigation/bottom-tabs": "^7.4.0",
    "@react-navigation/elements": "^2.6.3",
    "@react-navigation/native": "^7.1.8",
    "@react-navigation/stack": "^7.5.0",
    "@supabase/supabase-js": "^2.76.1",
    "expo": "~54.0.20",
    "expo-constants": "~18.0.10",
    "expo-dev-client": "~6.0.16",
    "expo-font": "~14.0.9",
    "expo-haptics": "~15.0.7",
    "expo-image": "~3.0.10",
    "expo-linking": "~8.0.8",
    "expo-location": "^19.0.7",
    "expo-router": "~6.0.13",
    "expo-secure-store": "~15.0.7",
    "expo-splash-screen": "~31.0.10",
    "expo-status-bar": "~3.0.8",
    "expo-symbols": "~1.0.7",
    "expo-system-ui": "~6.0.8",
    "expo-web-browser": "~15.0.8",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-native": "0.81.5",
    "react-native-gesture-handler": "~2.28.0",
    "react-native-google-places-autocomplete": "^2.5.7",
    "react-native-maps": "^1.20.1",
    "react-native-reanimated": "~4.1.1",
    "react-native-safe-area-context": "~5.6.0",
    "react-native-screens": "~4.16.0",
    "react-native-web": "~0.21.0",
    "react-native-worklets": "0.5.1"
  },
  "devDependencies": {
    "@types/react": "~19.1.0",
    "eslint": "^9.25.0",
    "eslint-config-expo": "~10.0.0",
    "typescript": "~5.9.2"
  },
  "private": true
}

```

### mobile/index.js

```javascript
import { registerRootComponent } from 'expo';
import App from './src/App';

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

```

### backend/app.py

```python
import os
from dotenv import load_dotenv
from elasticsearch import Elasticsearch
from flask import Flask, request, jsonify
import numpy as np
import openai
from find_places import find_places, get_place_information
from flask_cors import CORS

load_dotenv()

CLOUD_ID = os.getenv("ELASTIC_CLOUD_ID")
ELASTIC_API_KEY = os.getenv("ELASTIC_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
HOST_IP = os.getenv("HOST_IP")
HOST_PORT = int(os.getenv("HOST_PORT", 5000))
INDEX_NAME = "mosaic-index"


print(HOST_IP)
print(HOST_PORT)
client = Elasticsearch(
    CLOUD_ID,
    api_key=ELASTIC_API_KEY,
)
app = Flask(__name__)
CORS(app)

openai.api_key = OPENAI_API_KEY  # someones gotta make this

def generate_response_dict(recs): 
    dih = {}  # this will be a dict that gets returns to the frontend man, idek anymore im so tired
    for place_id in recs:
        name, image_url = get_place_information(place_id) 
        dih[str(place_id)] = {
            "image_url": image_url,
            "name": name,
        } 
    return dih 

def get_embedding(text):
    """Generate embedding for text using OpenAI."""
    response = openai.embeddings.create(
        model="text-embedding-3-small",
        input=text,
        dimensions=512,  # 512 cuz we broke
    )
    return response.data[0].embedding


def normalize(vec):  # kai
    """Normalize a vector to unit length."""
    v = np.array(vec)
    return (v / np.linalg.norm(v)).tolist()


def get_user_vectors(user_ids):
    docs = client.mget(index=INDEX_NAME, ids=user_ids)
    vecs = []
    for id in user_ids:
        user_doc = client.search(
            index=INDEX_NAME,
            query={
                "bool": {
                    "must": [
                        {"term": {"doc_type": "user"}},
                        {"term": {"user_uuid": id}},
                    ],
                }
            },
            _source=["user_vec"],
        )
        user_vec = user_doc["hits"]["hits"][0]["_source"][
            "user_vec"
        ]  # ik im so good at coding, what company want me tho?
        vecs.append(user_vec)
    if not vecs:
        return None
    print(
        "succesfully got the dense vectors for the following users: " + str(user_ids)
    )  # lol
    return np.array(vecs)


def update_user_vector(user_id, place_id, alpha=0.2):
    """Update user embedding after liking a place."""
    user_doc = client.search(
        index=INDEX_NAME,
        query={  # should have linked this sooner but here it is: https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl
            "bool": {
                "must": [
                    {"term": {"user_uuid": user_id}},
                    {"term": {"doc_type": "user"}},
                ]
            }
        },
        _source=["user_vec"],
    )
    place_doc = client.search(
        index=INDEX_NAME,
        query={
            "bool": {
                "must": [
                    {"term": {"place_id": place_id}},
                    {"term": {"doc_type": "place"}},
                ]
            }
        },
        _source=["place_vec"],
    )

    # return type of client.get is either self or None
    if not user_doc or not place_doc:
        return jsonify({"message": "Error fetching related docs"}), 404

    r = update_view_count(place_doc["hits"]["hits"][0]["_id"])  # update view count
    user_source = user_doc["hits"]["hits"][0]["_source"]
    place_source = place_doc["hits"]["hits"][0]["_source"]

    # these are the vectors
    place_vec = np.array(place_source["place_vec"])
    user_vec = np.array(user_source["user_vec"])
    new_vec = normalize((1 - alpha) * user_vec + alpha * place_vec)  # kai
    try:
        client.update(
            index=INDEX_NAME,
            id=user_id,
            body={"doc": {"user_vec": new_vec}},
        )

        return jsonify({"message": f"User {user_id} vector updated"}), 200
    except Exception as e:
        print(f"ran into error when trying to update user vector: {e}")
        return jsonify({f"message": "ran into an exception"}), 400


def update_view_count(doc_id):
    """
    Atomically increments the 'view_count' field for the specified document.
    If the field does not exist, it initializes it to 1.
    """
    try:
        response = client.update(
            index=INDEX_NAME,
            id=doc_id,
            script={
                "source": "ctx._source.view_count = ctx._source.view_count + params.inc",
                "params": {"inc": 1},
            },
            upsert={"view_count": 1},
        )
        return

    except Exception as e:
        print(f"Update error: {e}")
        return jsonify({"message": "Failed to update view count."}), 500

@app.before_request
def log_request_info():
    print(f"Incoming request: {request.method} {request.path}")
    print(f"Headers: {dict(request.headers)}")
    print(f"Body: {request.get_data(as_text=True)}")

# finds nearby places to a user and indexes them
@app.route("/api/find_places", methods=["POST"])
async def find_nearby_places():
    if request.is_json:
        data = request.get_json()

        lat = float(data.get("lat"))
        lon = float(data.get("lon"))
        radius = data.get("radius")
        if not all([lat, lon]):
            return jsonify({"message": "Missing required fields: lat, lon"}), 400

        # run async function synchronously
        response = find_places(lat, lon,radius_meters=int(radius))

        for place in response.places:
            place_id = place.id

            # skip if we already processed this place
            doc_exists = client.exists(id=place_id, index=INDEX_NAME)
            if doc_exists:
                continue

            place_name = str(getattr(place, "display_name", "No title found"))

            summary = ""
            if hasattr(
                place, "generative_summary"
            ):  # sometimes generative summary is unavailable
                summary = place.generative_summary.overview.text

[truncated — 6774 more characters]
```

### mobile/src/App.tsx

```typescript
import React from 'react';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { StatusBar } from 'expo-status-bar';
import AuthProvider from './providers/AuthProvider';
import { AppNavigator } from './navigation/AppNavigator';
import { SplashScreenController } from './components/SplashScreenController';
import CategoryProvider from './providers/CategoryProvider';

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <AuthProvider>
        <CategoryProvider>
          <SplashScreenController />
          <AppNavigator />
          <StatusBar style="auto" />
        </CategoryProvider>
      </AuthProvider>
    </GestureHandlerRootView>
  );
}

```

### mobile/src/types/index.ts

```typescript
export * from './user.types';
export * from './group.types';
export * from './place.types';

```

### mobile/eslint.config.js

```javascript
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');

module.exports = defineConfig([
  expoConfig,
  {
    ignores: ['dist/*'],
  },
]);

```

### backend/find_places.py

```python
import os
import requests
from google.maps import places_v1
from google.type import latlng_pb2
from dotenv import load_dotenv
import requests
from bs4 import BeautifulSoup


load_dotenv()

PLACES_API_KEY = os.getenv("PLACES_API_KEY")
URL = f"https://places.googleapis.com/v1/places/" # need to append PLACE_ID to end of this

client = places_v1.PlacesClient(
  # Instantiates the Places client, passing the API key
  client_options={"api_key": PLACES_API_KEY}
)

def get_place_information(place_id):
    """
    Fetches Place Details (photos, displayName) and constructs the direct image URL.
    Returns: A tuple (img_url, display_name) or an error string.
    """
    full_url = URL + place_id
    fieldMask = "photos,displayName"
    headers = {
        "Content-Type": "application/json",
        "X-Goog-Api-Key": PLACES_API_KEY,
        "X-Goog-FieldMask": fieldMask
    }
    
    res = requests.get(full_url, headers=headers)
    
    if res.status_code == 200:
        data = res.json()
        
        # Get display name first
        place_name = data['displayName']['text']
        
        # Check if photos exist
        if 'photos' not in data or not data['photos']:
            print(f"No photos found for place: {place_name}")
            # Return name with a None URL if no photo is available
            return (None, place_name)
        
        photo_resource_name = data['photos'][0]['name']
        
        print("Place Name:", place_name)
        print("Photo Resource Name (Token):", photo_resource_name)
        
        # Construct the image report url
        img_url = data['photos'][0]['flagContentUri']

        response = requests.get(img_url)
        soup = BeautifulSoup(response.text, 'html.parser')

        img = soup.find('img', id='preview-image')
        if img:
            print("Final Image URL:", img['src'])
            return (place_name,str(img['src'])) 

        else:
            print("Image not found")
            return (place_name,"") 
        
        
        
    else:
        print(f"Error fetching details (Status {res.status_code}): {res.text}")
        return ("something went wrong bro", None) # Return error string and None for name
    
def find_places(lat, lng, radius_meters=20000, types=["restaurant"]):
    center_point = latlng_pb2.LatLng(latitude=lat, longitude=lng)
    circle_area = places_v1.types.Circle(
        center=center_point,
        radius=radius_meters
    )
    # Add the circle to the location restriction
    location_restriction = places_v1.SearchNearbyRequest.LocationRestriction(
        circle=circle_area
    )

    # Build the request
    request = places_v1.SearchNearbyRequest(
        location_restriction=location_restriction,
        max_result_count=10, # restricting max results so we dont get too many entries in our db right now
        included_types=types
    )

    # Set the field mask
    # https://developers.google.com/maps/documentation/places/web-service/place-details#fieldmask
    # format: places.<field_name> 
    # if you want more fields, make them comma separated
    fieldMask = "places.id,places.displayName,places.generativeSummary,places.photos"

    # Make the request
    response = client.search_nearby(request=request, metadata=[("x-goog-fieldmask",fieldMask)])
    return response

# alternatively we could create a CRON job that fetches and populates places every few X hours 
# and goes across common tourist destinations across the world. Then we wouldn't need to fetch the places
# and insert them into the vector db every single time a user opens the app.
# Or we can do a mix of both
# It limits our availability to common tourist destinations but we would be able to expand in the future and
# it makes it quicker to load.

# also one more thought for the vector embeddings, we could make it so we also have a CRON job updating a user embedding.
# or every ~5 likes they do we update their embedding.
# because i have a hunch that the actual modification of the user embedding will take a good amount of time. rengenerating an embedding every 
# like will be annoying if they are saving often.


```

### mobile/lib/supabase.ts

```typescript
import { createClient } from '@supabase/supabase-js';
import { deleteItemAsync, getItemAsync, setItemAsync } from 'expo-secure-store';
const ExpoSecureStoreAdapter = {
  getItem: (key: string) => {
    console.debug("getItem", { key, getItemAsync })
    return getItemAsync(key)
  },
  setItem: (key: string, value: string) => {
    if (value.length > 2048) {
      console.warn('Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.')
    }
    return setItemAsync(key, value)
  },
  removeItem: (key: string) => {
    return deleteItemAsync(key)
  },
};
export const supabase = createClient(
  process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
  process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '',
  {
    auth: {
      storage: ExpoSecureStoreAdapter as any,
      autoRefreshToken: true,
      persistSession: true,
      detectSessionInUrl: false,
    },
  },
);
```

### mobile/src/navigation/types.ts

```typescript
export type RootStackParamList = {
  Auth: undefined;
  Groups: undefined;
  Preferences: undefined;
  Main: { groupId: string };
};

```

[24 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]