# Project export: CLaiM

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 11.0
- Tagline: Automatically file home insurance claims after natural disasters.
- Devpost: https://devpost.com/software/autoclaim-q8who1
- GitHub: https://github.com/SonavAgarwal/calhacks-24
- Video: https://www.youtube.com/embed/2fkQlLvGIrQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Hyperbolic: AI Inference Application Bounty)
- Team: 4 GitHub contributor(s) — Alex Kranias (28 commits), ml5885 (25 commits), Pranav Tadepalli (20 commits), Sonav Agarwal (12 commits)

## Devpost submission (written by the team)

### Inspiration

After the recent hurricanes in Florida, one of our team members who is from Florida had damage to their house. Normally, the process for filing an insurance claim after disaster is extremely cumbersome, requiring proof of all items and their value that were in the house before the disaster. This is very hard for disaster victims to prove in such a tough situation, which dissuades many people from filing insurance claims that cover the full value of all the items they lost.

### What it does

Our app allows homeowners to record a video of their house, while our innovative AI automatically catalogues all items in your house and their approximate value. After a disaster, you an rescan your house and we automatically suggest items that might be lost to add to your insurance claim, with image proof of the before and after.

### How we built it

Our frontend is built with next.js. Our backend uses Flask and sqllite. We also use xrpl in order to put items on the blockchain for proof of their existence before the disaster. Here are the steps our AI uses to process videos and pictures into items: The video is stitched together into a large panorama with OpenCV We use YOLOv8 and Meta's segment anything model to generate areas of interest on the image. We use Hyperbolic's inference (Qwen2 VL7B) to analyze each area and decide if it is an object to record and find its estimated monetary value We place all images in ChromaDB to group/count items of the same type and match images before and after disaster. We originally used ResNet for our embedding for the vector database in order to compare items, but later found all-MiniLM-L6-v2 to work better for vector encoding. The ResNet embeddings were too high dimensional. We generate a pdf claim to submit to the insurance company based on items that didn't appear after the disaster and were added by the user.

### Challenges we ran into

Segmentation problem: We tried a lot of different things in order to segment images well. We tried the segment everything model, SAM+yolo, and tried asking gemini to bounding boxes (it's the only vision language model that advertises this capability). We found SAM+yolo worked the best by far but only after lots of iteration and tuning. System Design: lots of iteration required and thinking about the customer. For simplicity chose to host a single AWS EC2 instance although seriously considered hosting multiple servers for each general task like segmentation. Video Panorama: How do you do segmentation on a video without getting duplicate items across frames? Our solution was to turn the video into a large image and run our machine learning on this image. CORS struck us once again.

### Accomplishments we're proud of

Super fast Segment Everything + Yolo + recognition + grouping pipeline that robustly recognized and catalogued items from video. Well designed and aesthetic front-end.

### What we learned

There are no power outlets in SF. We learned how to deploy an EC2 instance, and also how to use vector databases.

### What's next

We would like to

## README (from the GitHub repository)

Winner of Calhacks 2024 prize! See devpost here: https://devpost.com/software/autoclaim-q8who1


## Detected evidence (automated analysis)

Indexed codebase: 50 recognized source files, 341 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AWS (technology) — claimed on Devpost, not found in the code
- Flask (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (66 of 66)

```
.DS_Store
.gitignore
.gitmodules
agent.py
ch24-client/.eslintrc.json
ch24-client/.gitignore
ch24-client/.prettierrc
ch24-client/components.json
ch24-client/next.config.mjs
ch24-client/package.json
ch24-client/postcss.config.mjs
ch24-client/README.md
ch24-client/src/app/globals.css
ch24-client/src/app/home/page.tsx
ch24-client/src/app/layout.tsx
ch24-client/src/app/page.tsx
ch24-client/src/app/pdf/page.tsx
ch24-client/src/app/upload/layout.tsx
ch24-client/src/app/upload/page.tsx
ch24-client/src/app/uploads/layout.tsx
ch24-client/src/app/uploads/page.tsx
ch24-client/src/components/app-sidebar.tsx
ch24-client/src/components/ImageDialog.tsx
ch24-client/src/components/ItemCard.tsx
ch24-client/src/components/items-table.tsx
ch24-client/src/components/ui/alert-dialog.tsx
ch24-client/src/components/ui/breadcrumb.tsx
ch24-client/src/components/ui/button.tsx
ch24-client/src/components/ui/checkbox.tsx
ch24-client/src/components/ui/input.tsx
ch24-client/src/components/ui/separator.tsx
ch24-client/src/components/ui/sheet.tsx
ch24-client/src/components/ui/sidebar.tsx
ch24-client/src/components/ui/skeleton.tsx
ch24-client/src/components/ui/table.tsx
ch24-client/src/components/ui/tabs.tsx
ch24-client/src/components/ui/tooltip.tsx
ch24-client/src/hooks/use-mobile.tsx
ch24-client/src/lib/data.tsx
ch24-client/src/lib/misc.ts
ch24-client/src/lib/types.tsx
ch24-client/src/lib/utils.ts
ch24-client/tailwind.config.ts
ch24-client/tsconfig.json
gemini.py
items_and_images.db
private_keys.json
README.md
server/.gitignore
server/aws.py
server/BLOCKCHAIN.py
server/chroma.py
server/db.py
server/hyperbolic.py
server/image_embedding.py
server/items_and_images.db
server/mint_nfts.py
server/ml.py
server/predict.py
server/private_keys.json
server/server.py
server/stitcher.py
server/test_hyperbolic.py
server/yolov8n.pt
stitch.py
vector.py
```

### Dependencies

- ch24-client/package.json: @radix-ui/react-alert-dialog@^1.1.2, @radix-ui/react-checkbox@^1.1.2, @radix-ui/react-dialog@^1.1.2, @radix-ui/react-separator@^1.1.0, @radix-ui/react-slot@^1.1.0, @radix-ui/react-tabs@^1.1.1, @radix-ui/react-tooltip@^1.1.3, @react-pdf/renderer@^4.0.0, @tanstack/react-table@^8.20.5, @types/node@^20, @types/react@^18, @types/react-dom@^18, class-variance-authority@^0.7.0, clsx@^2.1.1, eslint@^8, eslint-config-next@14.2.15, lucide-react@^0.453.0, next@14.2.15, postcss@^8, prettier@^3.3.3, prettier-plugin-tailwindcss@^0.6.8, react@^18, react-dom@^18, react-dropzone@^14.2.10, react-hook-form@^7.53.1, tailwind-merge@^2.5.4, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5, zustand@^5.0.0

### Recent commits (newest first)

- Create README.md
- fix filtering etc
- progress
- Merge branch 'main' of https://github.com/SonavAgarwal/calhacks-24
- blockchained
- refactor: claim -> confirm matches
- frontend matches backend
- Merge branch 'main' of https://github.com/SonavAgarwal/calhacks-24
- block
- fix: claim doesn't delete just sets new status
- Merge branch 'main' of https://github.com/SonavAgarwal/calhacks-24
- refactor+feat: before and after consolidated to images + make claim feature
- Merge branch 'main' of https://github.com/SonavAgarwal/calhacks-24
- added delete endpoint
- Merge branch 'main' of https://github.com/SonavAgarwal/calhacks-24
- rip
- feat: before and after count + set pending to done + new get inventory data
- Merge branch 'main' of https://github.com/SonavAgarwal/calhacks-24
- debugged pipeline
- idk

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

### ch24-client/package.json

```
{
  "name": "ch24-client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-alert-dialog": "^1.1.2",
    "@radix-ui/react-checkbox": "^1.1.2",
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-separator": "^1.1.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.1",
    "@radix-ui/react-tooltip": "^1.1.3",
    "@react-pdf/renderer": "^4.0.0",
    "@tanstack/react-table": "^8.20.5",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "lucide-react": "^0.453.0",
    "next": "14.2.15",
    "react": "^18",
    "react-dom": "^18",
    "react-dropzone": "^14.2.10",
    "react-hook-form": "^7.53.1",
    "tailwind-merge": "^2.5.4",
    "tailwindcss-animate": "^1.0.7",
    "zustand": "^5.0.0"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.15",
    "postcss": "^8",
    "prettier": "^3.3.3",
    "prettier-plugin-tailwindcss": "^0.6.8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### server/server.py

```python
import json
# import env
import os
import uuid
from datetime import datetime

import boto3
from aws import open_s3_client, upload_image_to_s3
from chroma import *
from dotenv import load_dotenv
from flask import Flask, jsonify, request
from flask_cors import CORS
from ml import process_image, process_video
from werkzeug.utils import secure_filename
from db import *

load_dotenv()

app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "http://localhost:3000"}})
app.config['CORS_HEADERS'] = 'Content-Type'


@app.route('/')
def hello_world():
    return 'Hello, World!'


###################
# Upload media


@app.route('/upload_media', methods=['POST'])
def upload_media():
    before = request.args.get('before')

    # with each new upload session set prev pending to done
    set_pending_to_done()

    print('upload_media')

    # Create a new S3 client
    s3 = open_s3_client()

    if s3 is None:
        return jsonify({'error': 'Could not initialize S3 client'}), 500

    # Get the uploaded file from the request
    if 'files[]' not in request.files:
        return jsonify({'error': 'No files provided'}), 400

    files = request.files.getlist('files[]')
    if len(files) == 0:
        return jsonify({'error': 'No files provided'}), 400

    uploaded_images = []

    for file in files:

        # ensure the file is an image or video
        if file.content_type not in ['image/jpeg', 'image/png', 'image/gif', 'video/mp4', 'video/quicktime']:
            return jsonify({'error': 'Invalid file type'}), 400

        # TODO: send the files through the pipeline (call process_video or process_image)
        if file.content_type == 'video/mp4' or file.content_type == 'video/quicktime':
            print('processing video', file)
            uploads = process_video(file, s3, before)
        else:
            print('processing image', file)
            uploads = process_image(file, s3, before)
        
        uploaded_images += uploads

    # # Return the image URLs
    # return jsonify({'urls': uploaded_images}), 200
    return jsonify({'message': 'Media uploaded successfully'}), 200


###################
# Get the inventory

@app.route('/inventory', methods=['GET'])
def get_items():
    # do something to get inventory

    # metadata to filter by
    item_id = None
    url_path = None
    before = request.args.get('before')
    status = request.args.get('status')

    filtered_images = filter_images_by_metadata(
        item_id, url_path, before, status)
    
    # print("Filtered images", filtered_images)

    ids = filtered_images['ids'][0]
    metadatas = filtered_images['metadatas'][0]

    results = list(zip(ids, metadatas))

    print("looping through results")

    items = {}
    for id, metadata in results:
        print(f"Item ID: {id}, Metadata: {metadata}")
        # Get the item details from the image
        item_id = metadata.get('item_id')
        if not item_id:
            continue
        item_details = get_item(item_id)
        if item_details and item_details['id'] not in items:
            item_details['images'] = []
            items[item_details['id']] = item_details

    print("BEFORE IMAGES ADDED TO ITEMS")
    # Print out the items
    for item in items.values():
        print(f"Item: {item}")

    # Now add the images to the items
    for id, metadata in results:
        print(f"Adding image to item {id}")
        item_obj = items.get(metadata.get('item_id', None), None)
        if item_obj:
            item_obj['images'].append(metadata)

    print("AFTER IMAGES ADDED TO ITEMS")
    # print out the items
    for item in items.values():
        print(f"Item: {item}")

    # convert the items dict to a list
    items = list(items.values())

    # returns all the items in the inventory, joined with their images
    return jsonify({"items": items}), 200

@app.route('/confirm_matches', methods=['POST'])
def confirm_matches():
    data = request.json
    item_ids = data.get('item_ids', [])

    if not item_ids:
        return jsonify({"error": "No item IDs provided"}), 400

    matched_images = []

    # For each item to claim
    for item_id in item_ids:
        # Find and remove associated images from ChromaDB
        images = filter_images_by_metadata(item_id=item_id)
        if images and 'ids' in images:
            image_ids = images['ids'][0]
            for image_id in image_ids:
                update_image_status(image_id, new_status='matched')        
                matched_images.append(image_id)

    return jsonify({
        "message": "Claim processed successfully. Image statuses updated to 'claimed'",
        "claimed_items": item_ids,
        "matched_images": matched_images
    }), 200


###################
# Get pending uploads

@app.route('/pending_uploads', methods=['GET'])
def get_pending_uploads():
    # do something to get pending uploads
    filtered_images = filter_images_by_metadata(status='pending')

    pending_items = set()
    for image in filtered_images:
        image['']

    # TODO: return all the images that are pending upload (not yet in inventory)
    return jsonify({"message": "Pending uploads fetched successfully"}), 200


###################
# Set pending images to done
def set_status_to_status(old_status, new_status):
    # data = request.json

    print(f"Setting {old_status} images to {new_status} status")

    result = filter_images_by_metadata(status=old_status)
    image_ids = result['ids'][0]
    for id in image_ids:
        update_image_status(id, new_status=new_status)

    print(f"Images {image_ids} updated from {old_status} to {new_status} status")

    return jsonify({"message": f"Images {image_ids} updated from {old_status} to {new_status} status"}), 200


def set_pending_to_done():
    return set_status_to_status(old_status='pending', new_status='done')



###################
# Accept images to inventory

@app.route('/accept_to_inventory', methods=['POST'])
def accept_to_inventory():
    data = request.json
    
[truncated — 708 more characters]
```

### ch24-client/src/app/layout.tsx

```typescript
import type { Metadata } from "next"
import localFont from "next/font/local"
import "./globals.css"

const geistSans = localFont({
    src: "./fonts/GeistVF.woff",
    variable: "--font-geist-sans",
    weight: "100 900",
})
const geistMono = localFont({
    src: "./fonts/GeistMonoVF.woff",
    variable: "--font-geist-mono",
    weight: "100 900",
})

export const metadata: Metadata = {
    title: "Create Next App",
    description: "Generated by create next app",
}

export default function RootLayout({
    children,
}: Readonly<{
    children: React.ReactNode
}>) {
    return (
        <html lang="en">
            <head>
                <link
                    rel="preconnect"
                    href="https://fonts.googleapis.com"
                ></link>
                <link
                    rel="preconnect"
                    href="https://fonts.gstatic.com"
                    crossOrigin="anonymous"
                ></link>
                <link
                    href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
                    rel="stylesheet"
                ></link>
            </head>
            <body
                className={`${geistSans.variable} ${geistMono.variable} antialiased`}
            >
                {children}
            </body>
        </html>
    )
}

```

### ch24-client/src/app/page.tsx

```typescript
"use client"

import ImageDialog from "@/components/ImageDialog"
import { ItemsTable } from "@/components/items-table"
import { Checkbox } from "@/components/ui/checkbox"
import { Image, Item, UploadSessionResponse } from "@/lib/types"
import {
    CellContext,
    ColumnDef,
    getCoreRowModel,
    useReactTable,
} from "@tanstack/react-table"
import { match } from "assert"
import { useRouter, useSearchParams } from "next/navigation"
import { useEffect, useMemo, useRef, useState } from "react"
import { twMerge } from "tailwind-merge"

interface Props {}

const page = (props: Props) => {
    const before = useSearchParams().get("before") === "true"
    const router = useRouter()

    const [loading, setLoading] = useState(true)
    const processing = useRef(true)

    const [response, setResponse] = useState<UploadSessionResponse | null>(null)

    const [enlargedImage, setEnlargedImage] = useState<Image | null>(null)
    const [enlargedRow, setEnlargedRow] = useState<Item | null>(null)

    const itemColumns = useMemo(() => {
        const itemsColumns: ColumnDef<Item>[] = [
            {
                accessorKey: "name",
                header: "Name",
            },
            {
                accessorKey: "description",
                header: "Description",
                cell: (info: CellContext<Item, unknown>) => (
                    <div className="text-sm">
                        {info.row.original.description}
                    </div>
                ),
            },
            {
                accessorKey: "price",
                header: "Price",
                accessorFn: (originalRow) =>
                    `$${parseFloat(originalRow.price).toFixed(2)}`,
            },
            {
                accessorKey: "before_count",
                header: "Count",
            },
            {
                accessorKey: "images",
                header: "Images",
                cell: (info: CellContext<Item, unknown>) => (
                    <div className="flex items-center justify-center">
                        {(info.getValue() as Image[])
                            .slice(0, 3)
                            .map((image, index) => (
                                <img
                                    onClick={() => setEnlargedImage(image)}
                                    key={image.image_id}
                                    src={image.url_path}
                                    alt={info.row.original.name}
                                    className={twMerge(
                                        "h-10 w-10 rounded-md border-2 border-white object-cover",
                                    )}
                                    style={{
                                        marginLeft: index === 0 ? 0 : "-1.5rem",
                                    }}
                                />
                            ))}
                    </div>
                ),
            },
            {
                accessorKey: "category",
                header: "Category",
            },
            {
                accessorKey: "id",
                header: "",
                cell: ({ row }: CellContext<Item, unknown>) => (
                    <Checkbox
                        onClick={(e) => e.stopPropagation()}
                        className="mr-4 h-6 w-6"
                        checked={row.getIsSelected()}
                        onCheckedChange={row.getToggleSelectedHandler()}
                    />
                ),
            },
        ]
        return itemsColumns
    }, [response])

    const items = useMemo<Item[]>(() => {
        // filter items based on before
        let data = response?.items ?? []
        data = JSON.parse(JSON.stringify(data))
        data = data.filter((item) =>
            item.images.some((image) => image.status === "inventory"),
        )

        // only show images that are status inventory
        data = data.map((item) => {
            return {
                ...item,
                images: item.images.filter(
                    (image) => image.status === "inventory",
                ),
            }
        })

        return data
    }, [before, response])

    const matchedItems = useMemo<Item[]>(() => {
        // filter items based on before
        let data = response?.items ?? []
        data = JSON.parse(JSON.stringify(data))
        data = data.filter((item) =>
            item.images.some((image) => image.status === "matched"),
        )
        return data
    }, [before, response])

    const table = useReactTable({
        data: items,
        columns: itemColumns,
        getCoreRowModel: getCoreRowModel(),
        getRowId: (row) => row.id,
        enableRowSelection: true,
        enableMultiRowSelection: true,
    })

    const matchedTable = useReactTable({
        data: matchedItems,
        columns: itemColumns,
        getCoreRowModel: getCoreRowModel(),
        getRowId: (row) => row.id,
        enableRowSelection: true,
        enableMultiRowSelection: true,
    })

    useEffect(() => {
        function fetchData() {
            fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/inventory`, {
                headers: {
                    "ngrok-skip-browser-warning": "true",
                },
            })
                .then((response) => response.json())
                .then((data) => {
                    setLoading(false)
                    processing.current = data.processing
                    setResponse(data)
                    console.log("data", data)
                })
                .catch((error) => {
                    console.error("Error fetching data", error)
                })

            setLoading(false)
        }

        fetchData()

        // return () => clearInterval(interval)
    }, [])

    const claimItems = () => {
        const selectedRows = table.getSelectedRowModel()?.rows ?? []
        // claim items
        console.log("Claimin
[truncated — 7011 more characters]
```

### ch24-client/src/app/upload/layout.tsx

```typescript
import React from "react";

const layout = (props: { children: React.ReactNode }) => {
	return (
		<div className="w-screen min-h-screen flex flex-col items-center">
			<div className="w-[48rem] min-h-screen flex flex-col">
				{props.children}
			</div>
		</div>
	);
};

export default layout;

```

### ch24-client/src/app/uploads/layout.tsx

```typescript
import React from "react";

const layout = (props: { children: React.ReactNode }) => {
	return (
		<div className="w-screen min-h-screen flex flex-col items-center">
			<div className="w-[48rem] min-h-screen flex flex-col">
				{props.children}
			</div>
		</div>
	);
};

export default layout;

```

### ch24-client/src/app/pdf/page.tsx

```typescript
"use client"

import React, { useEffect, useState } from "react"
import ReactPDF, {
    Page,
    Text,
    View,
    Document,
    StyleSheet,
    PDFViewer,
    Image,
} from "@react-pdf/renderer"
import { Item } from "@/lib/types"

// Create styles
const styles = StyleSheet.create({
    page: {
        padding: 20,
    },
    table: {
        // display: "table",
        width: "auto",
    },
    tableRow: {
        flexDirection: "row",
    },
    tableCell: {
        flex: 1,
        padding: 5,
        fontSize: 10,
        borderBottomWidth: 1,
        borderColor: "#000",
    },
})

const items = [
    {
        category: "Electronics",
        item: "Laptop",
        description: "MacBook Pro",
        yearBought: "2020",
        qty: 1,
        replacementCost: "$1500",
        totalCost: "$1500",
        link: "https://example.com/laptop",
        picture: "https://picsum.photos/200/300",
    },
    {
        category: "Electronics",
        item: "Laptop",
        description: "MacBook Pro",
        yearBought: "2020",
        qty: 1,
        replacementCost: "$1500",
        totalCost: "$1500",
        link: "https://example.com/laptop",
        picture: "https://picsum.photos/200/300",
    },
    {
        category: "Electronics",
        item: "Laptop",
        description: "MacBook Pro",
        yearBought: "2020",
        qty: 1,
        replacementCost: "$1500",
        totalCost: "$1500",
        link: "https://example.com/laptop",
        picture: "https://picsum.photos/200/300",
    },
    // More items...
]

// Create Document Component
const MyDocument = () => {
    // const data = items

    const [data, setData] = useState<Item[]>([])
    useEffect(() => {
        // load "claimedItems" from localStorage
        const items = JSON.parse(localStorage.getItem("claimedItems") || "[]")
        setData(items)
    }, [])

    console.log(data)
    return (
        <Document>
            <Page size="A4" orientation="landscape" style={styles.page}>
                <View style={styles.table}>
                    {/* Table Headers */}
                    <View style={styles.tableRow}>
                        <Text style={styles.tableCell}>Category</Text>
                        <Text style={styles.tableCell}>Item</Text>
                        <Text style={{ ...styles.tableCell, flexGrow: 2 }}>
                            Description
                        </Text>
                        <Text style={styles.tableCell}>Year Bought</Text>
                        <Text style={styles.tableCell}>Qty</Text>
                        <Text style={styles.tableCell}>Replacement Cost</Text>
                        <Text style={styles.tableCell}>Total Cost</Text>
                        <Text
                            style={{
                                ...styles.tableCell,
                                flexGrow: 3,
                            }}
                        >
                            Link
                        </Text>
                        {/* <Text style={styles.tableCell}>Picture</Text> */}
                    </View>

                    {/* Table Rows - Loop through data */}
                    {data.map((item, index) => (
                        <View key={index} style={styles.tableRow}>
                            <Text style={styles.tableCell}>
                                {item.category}
                            </Text>
                            <Text style={styles.tableCell}>{item.name}</Text>
                            <Text style={{ ...styles.tableCell, flexGrow: 2 }}>
                                {item.description}
                            </Text>
                            <Text style={styles.tableCell}>{2024}</Text>
                            <Text style={styles.tableCell}>
                                {item.before_count}
                            </Text>
                            <Text style={styles.tableCell}>{item.price}</Text>
                            <Text style={styles.tableCell}>{item.price}</Text>
                            <Text
                                style={{
                                    ...styles.tableCell,
                                    flexGrow: 3,
                                }}
                            >
                                https://example.com/{item.name}
                            </Text>
                            {/* <Image
                                style={styles.tableCell}
                                src={item.picture}
                            /> */}
                        </View>
                    ))}
                </View>
            </Page>
        </Document>
    )
}

const page = () => {
    const [isClient, setIsClient] = useState(false)

    useEffect(() => {
        setIsClient(true)
    }, [])

    if (!isClient) {
        return null
    }
    return (
        <PDFViewer
            style={{
                width: "100%",
                height: "100vh",
                border: "none",
            }}
        >
            <MyDocument />
        </PDFViewer>
    )
}

export default page

```

### ch24-client/src/app/upload/page.tsx

```typescript
"use client"

import { FileUp } from "lucide-react"
import React, { useState } from "react"
import Image from "next/image"
import Dropzone from "react-dropzone"
import { twMerge } from "tailwind-merge"
import { useRouter } from "next/navigation"

interface Props {}

const page = (props: Props) => {
    const router = useRouter()

    const [uploadType, setUploadType] = useState<"inventory" | "claims">(
        "inventory",
    )

    const [files, setFiles] = useState<File[]>([])

    const handleDrop = (acceptedFiles: File[]) => {
        setFiles((prev) => [...prev, ...acceptedFiles])
    }

    const [loading, setLoading] = useState(false)

    const handleUpload = async () => {
        // fetch localhost 5000/upload_image with files, query param after is true if claims, false if inventory
        // if successful, navigate to uploads/response.uploadId
        // if not, show error
        setLoading(true)
        // setTimeout(() => {
        //     router.push(`/uploads/meow`)
        // }, 1000)
        // return

        const formData = new FormData()
        files.forEach((file) => {
            formData.append("files[]", file)
        })

        const response = await fetch(
            `${process.env.NEXT_PUBLIC_BASE_URL}/upload_media?before=${uploadType === "inventory"}`,
            {
                method: "POST",
                body: formData,
                headers: {
                    "ngrok-skip-browser-warning": "true",
                },
            },
        )

        if (response.ok) {
            const data = await response.json()
            // navigate to uploads/response.uploadId
            console.log(data)
            setLoading(false)

            router.push(`/uploads?before=${uploadType === "inventory"}`)
        } else {
            console.error("Error uploading files")
        }
    }

    return (
        <div className="flex min-h-screen w-full flex-col items-center justify-center gap-4 p-4">
            {/* <h1 className="text-4xl font-bold w-full">Upload Media</h1> */}

            {/* two tabs called "inventory" and "for claims" */}
            {/* the selected one is black background white text */}
            {/* the background should slide between the two options, like a pill */}

            <div className="relative flex w-64 rounded-full border border-border bg-background">
                {/* Sliding background */}
                <div
                    className={twMerge(
                        "absolute bottom-0 left-1 top-0 my-1 rounded-full bg-black transition-all duration-300",
                        uploadType === "inventory"
                            ? "w-1/2"
                            : "-left-1 w-1/2 translate-x-full",
                        "outline-none",
                    )}
                ></div>

                {/* Inventory Button */}
                <button
                    className={twMerge(
                        "relative z-10 flex-1 rounded-full py-2 text-center",
                        uploadType === "inventory"
                            ? "text-white"
                            : "text-black",
                        "outline-none transition-colors",
                    )}
                    onClick={() => setUploadType("inventory")}
                >
                    Inventory
                </button>

                {/* For Claims Button */}
                <button
                    className={twMerge(
                        "relative z-10 flex-1 rounded-full py-2 text-center",
                        uploadType === "claims" ? "text-white" : "text-black",
                        "outline-none transition-colors",
                    )}
                    onClick={() => setUploadType("claims")}
                >
                    For Claims
                </button>
            </div>

            <Dropzone
                onDrop={(acceptedFiles) => handleDrop(acceptedFiles)}
                accept={{
                    "image/png": [".png"],
                    "image/jpeg": [".jpg"],
                    "image/heic": [".heic"],
                    "video/mp4": [".mp4"],
                    "video/quicktime": [".mov"],
                }}
            >
                {({ getRootProps, getInputProps }) => (
                    <div
                        className={twMerge(
                            "flex h-96 w-96 flex-col items-center justify-center rounded-xl border border-border bg-background p-4 transition-opacity",
                            loading && "opacity-35",
                        )}
                        {...getRootProps()}
                    >
                        <input {...getInputProps()} />
                        {files.length === 0 && (
                            <>
                                <Image
                                    width={100}
                                    height={100}
                                    src="/assets/upload-cards.png"
                                    alt="upload cards"
                                    className="mb-4"
                                />
                                <p className="text-center text-xl font-bold">
                                    Upload photos and videos
                                </p>
                                <p className="text-center italic">
                                    .png, .jpg, .heic, .mp4, .mov
                                </p>
                            </>
                        )}
                        {files.length > 0 && (
                            <div className="grid h-full w-full grid-cols-2 gap-4 overflow-auto rounded-xl bg-background">
                                {files.map((file, i) => (
                                    <div
                                        key={i}
                                        className="relative rounded-xl"
                                    >
                         
[truncated — 1316 more characters]
```

### ch24-client/src/app/home/page.tsx

```typescript
"use client"

import ImageDialog from "@/components/ImageDialog"
import { ItemsTable } from "@/components/items-table"
import { Checkbox } from "@/components/ui/checkbox"
import { fixBoolean } from "@/lib/misc"
import { Image, Item, UploadSessionResponse } from "@/lib/types"
import {
    CellContext,
    ColumnDef,
    getCoreRowModel,
    useReactTable,
} from "@tanstack/react-table"
import { useSearchParams } from "next/navigation"
import { useEffect, useMemo, useRef, useState } from "react"
import { twMerge } from "tailwind-merge"

interface Props {}

const page = (props: Props) => {
    const before = useSearchParams().get("before") === "true"

    const [loading, setLoading] = useState(true)
    const processing = useRef(true)

    const [response, setResponse] = useState<UploadSessionResponse | null>(null)

    const [enlargedImage, setEnlargedImage] = useState<Image | null>(null)
    const [enlargedRow, setEnlargedRow] = useState<Item | null>(null)

    const itemColumns = useMemo(() => {
        const itemsColumns: ColumnDef<Item>[] = [
            {
                accessorKey: "name",
                header: "Name",
            },
            {
                accessorKey: "description",
                header: "Description",
                cell: (info: CellContext<Item, unknown>) => (
                    <div className="text-sm">
                        {info.row.original.description}
                    </div>
                ),
            },
            {
                accessorKey: "price",
                header: "Price",
                accessorFn: (originalRow) =>
                    `$${parseFloat(originalRow.price).toFixed(2)}`,
            },
            {
                accessorKey: "before_count",
                header: "Count",
            },
            {
                accessorKey: "images",
                header: "Images",
                cell: (info: CellContext<Item, unknown>) => (
                    <div className="flex items-center justify-center">
                        {(info.getValue() as Image[])
                            .slice(0, 3)
                            .map((image, index) => (
                                <img
                                    onClick={() => setEnlargedImage(image)}
                                    key={image.image_id}
                                    src={image.url_path}
                                    alt={info.row.original.name}
                                    className={twMerge(
                                        "h-10 w-10 rounded-md border-2 border-white object-cover",
                                    )}
                                    style={{
                                        marginLeft: index === 0 ? 0 : "-1.5rem",
                                    }}
                                />
                            ))}
                    </div>
                ),
            },
            {
                accessorKey: "category",
                header: "Category",
            },
            {
                accessorKey: "id",
                header: "",
                cell: ({ row }: CellContext<Item, unknown>) => (
                    <Checkbox
                        onClick={(e) => e.stopPropagation()}
                        className="mr-4 h-6 w-6"
                        checked={row.getIsSelected()}
                        onCheckedChange={row.getToggleSelectedHandler()}
                    />
                ),
            },
        ]
        return itemsColumns
    }, [response])

    const items = useMemo<Item[]>(() => {
        // filter items based on before
        let data = response?.items ?? []

        console.log("data", data)

        if (before) {
            console.log("filtering")
            // only show rows that have an image with status pending
            data = data.filter((item) =>
                item.images.some((image) => image.status === "pending"),
            )

            // only show images with status pending
            data.forEach((item) => {
                item.images = item.images.filter(
                    (image) => image.status === "pending",
                )
            })
        } else {
            // only show rows that have an image with status pending and before is false
            data = data.filter((item) =>
                item.images.some(
                    (image) =>
                        image.status === "pending" && !fixBoolean(image.before),
                ),
            )

            // // only show images with status pending and before is false
            // data.forEach((item) => {
            //     item.images = item.images.filter(
            //         (image) => image.status === "pending" && !image.before,
            //     )
            // })
        }
        console.log("data", data)
        return data
    }, [before, response])

    const table = useReactTable({
        data: items,
        columns: itemColumns,
        getCoreRowModel: getCoreRowModel(),
        getRowId: (row) => row.id,
        enableRowSelection: true,
        enableMultiRowSelection: true,
    })

    useEffect(() => {
        // fetch example.com/upload/${uploadId}
        // if successful, set loading to false

        // if response.processing is true, set processing to true
        // if not, set processing to false

        // if not, show error

        function fetchData() {
            fetch(
                `${process.env.NEXT_PUBLIC_BASE_URL}/inventory?status=inventory&before=true`,
                {
                    headers: {
                        "ngrok-skip-browser-warning": "true",
                    },
                },
            )
                .then((response) => response.json())
                .then((data) => {
                    setLoading(false)
                    processing.current = data.processing
                    setResponse(data)
                    console.log("da
[truncated — 3464 more characters]
```

### agent.py

```python
import time
from typing import Any, Dict
 
from uagents import Agent, Context, Model

 
class Response(Model):
    timestamp: int
    text: str
    agent_address: str
 
 
# You can also use empty models to represent empty request/response bodies
class EmptyMessage(Model):
    pass
 
 
agent = Agent(name="Rest API")
 
 
@agent.on_rest_get("/rest/get", Response)
async def handle_get(ctx: Context) -> Dict[str, Any]:
    ctx.logger.info("Received GET request")
    return {
        "timestamp": int(time.time()),
        "text": "Hello from the GET handler!",
        "agent_address": ctx.agent.address,
    }
 
 
if __name__ == "__main__":
    agent.run()
```

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