Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Franknoirot/limit model loading #139

Merged
merged 6 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This repository is an open-source example of how to quickly get up and running w
## Developing

1. Generate a dev API token from https://dev.zoo.dev
2. Set the `VITE_TOKEN` environment variable in `./.env.development` to the generated dev API token
2. Set the `VITE_TOKEN` environment variable in `./.env.development.local` to the generated dev API token
3. Install yarn
4. Install dependencies with `yarn global add vite` and `yarn install`
5. Run the dev server with `yarn dev -- --open`
Expand Down
107 changes: 75 additions & 32 deletions src/components/GenerationList.svelte
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
<script lang="ts">
import type { Models } from '@kittycad/lib'
import type { UIEventHandler } from 'svelte/elements'
import GenerationListItem from './GenerationListItem.svelte'
import { endpoints } from '$lib/endpoints'
import { page } from '$app/stores'
import { fetchedGenerations, generations, nextPageToken } from '$lib/stores'
import { fetchedGenerations, generations, nextPageTokens } from '$lib/stores'
import { sortTimeBuckets } from '$lib/time'
import { browser } from '$app/environment'
import Spinner from 'components/Icons/Spinner.svelte'
import { PAGES_AHEAD_TO_FETCH } from '$lib/consts'
import { onMount } from 'svelte'

let isFetching = false
let error: string | null = null
let pagesToFetch = PAGES_AHEAD_TO_FETCH
let scrolledPercentage = 0
let fetchPromise: Promise<void>

$: if (browser && $nextPageToken !== null) {
fetchData()
onMount(() => {
fetchPromise =
$nextPageTokens[$nextPageTokens.length - 1] === null ? Promise.resolve() : fetchData()
})

// Reset the pages to fetch counter if the user scrolls
// to the bottom of the generation list
const handleScroll: UIEventHandler<HTMLElement> = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target as HTMLElement
scrolledPercentage = (scrollTop / (scrollHeight - clientHeight)) * 100
if (scrolledPercentage > 90) {
pagesToFetch = PAGES_AHEAD_TO_FETCH
fetchPromise = fetchData()
}
}

async function fetchData() {
if ($nextPageToken === null) return
isFetching = true
// If we're at the end of the list, don't fetch more
if ($nextPageTokens[$nextPageTokens.length - 1] === null) return

const response = await fetch(endpoints.list({ page_token: $nextPageToken }), {
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + $page.data.token
const response = await fetch(
endpoints.list({ page_token: $nextPageTokens[$nextPageTokens.length - 1] }),
{
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + $page.data.token
}
}
})

)
if (!response.ok) {
isFetching = false
error = 'Failed to fetch your creations'
return
}

const nextBatchPayload = (await response.json()) as Models['TextToCadResultsPage_type']

// If we see that one of fetched generations has an id that matches one of the
// generations in the store, we know we can stop fetching
const shouldKeepFetching = updateFetchedGenerations(nextBatchPayload)
$nextPageToken = shouldKeepFetching ? nextBatchPayload?.next_page ?? null : null
isFetching = false
const hasNoDuplicateGenerations = updateFetchedGenerations(nextBatchPayload)

if (nextBatchPayload?.next_page === null) {
$nextPageTokens = [...$nextPageTokens, null]
pagesToFetch = 0
} else if (hasNoDuplicateGenerations) {
$nextPageTokens = [...$nextPageTokens, nextBatchPayload?.next_page ?? null]
pagesToFetch = pagesToFetch - 1
} else {
pagesToFetch = 0
}

// If we have more pages to fetch, fetch them
// and "keep the promise alive"
if (pagesToFetch > 0) {
return fetchData()
}
}

function updateFetchedGenerations(payload: Models['TextToCadResultsPage_type']): boolean {
Expand All @@ -60,8 +92,14 @@
}
</script>

<section class="overflow-y-auto max-h-full px-2 lg:pr-4 pt-6">
{#if Object.keys($generations).length > 0}
<section
on:scroll={handleScroll}
class="overflow-y-auto max-h-full px-2 lg:pr-4 pt-6"
data-testid="generation-list"
>
{#if error}
<p class="text-red mt-2">{error}</p>
{:else if Object.keys($generations).length > 0}
{#each Object.entries($generations).toSorted(sortTimeBuckets) as [category, items]}
<div class="first-of-type:mt-0 mt-12">
<h2 class="pl-2 lg:pl-4 text-xl">{category}</h2>
Expand All @@ -74,19 +112,24 @@
</ul>
</div>
{/each}
{/if}
{#if isFetching}
<p
class={'flex gap-4 m-2 text-sm tracking-wide text-chalkboard-100 dark:text-chalkboard-30' +
(Object.keys($generations).length > 0 ? ' pt-8 border-t' : '')}
>
<span class="flex-grow">Fetching your creations</span>
<Spinner class="w-5 h-5 animate-spin inline-block mr-2" />
</p>
{:else if Object.keys($generations).length === 0}
{#await fetchPromise}
<p
class={'flex gap-4 m-2 text-sm tracking-wide text-chalkboard-100 dark:text-chalkboard-30' +
(Object.keys($generations).length > 0 ? ' pt-8 border-t' : '')}
>
<span class="flex-grow">Fetching your creations</span>
<Spinner class="w-5 h-5 animate-spin inline-block mr-2" />
</p>
{:then}
{#if $nextPageTokens[$nextPageTokens.length - 1] === null}
<p
class="text-chalkboard-100 dark:text-chalkboard-30 text-sm tracking-wide m-2 py-6 border-t"
>
You've reached the end of your creations 🎉
</p>
{/if}
{/await}
{:else}
<p>You'll see your creations here once you submit your first prompt</p>
{/if}
{#if error}
<p class="text-red mt-2">{error}</p>
{/if}
</section>
11 changes: 10 additions & 1 deletion src/lib/consts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { msSinceStartOfDay, msSinceStartOfMonth, msSinceAWeekAgo, msSinceStartOfYear } from './time'

export const PERSIST_KEY_VERSION = '2023-01-09'
export const PERSIST_KEY_VERSION = '2024-06-21'
export const PERSIST_KEY_GENERATIONS = 'TEXT_TO_CAD_GENERATIONS'
export const PERSIST_KEY_UNREAD = 'TEXT_TO_CAD_UNREAD'

Expand Down Expand Up @@ -41,3 +41,12 @@ export const TIME_BUCKETS = [
test: (then: Date, now: Date) => now.getTime() - then.getTime() < msSinceStartOfYear(now)
}
] as const

/**
* The number of pages to fetch ahead of the current page
*/
export const PAGES_AHEAD_TO_FETCH = 5
/**
* The number of items to fetch per request
*/
export const ITEMS_PER_PAGE = 5
3 changes: 2 additions & 1 deletion src/lib/endpoints.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Models } from '@kittycad/lib'
import { ITEMS_PER_PAGE } from './consts'

export type CADFormat = Models['FileExportFormat_type']

Expand All @@ -23,7 +24,7 @@ export const endpoints = {
`${import.meta.env.VITE_API_BASE_URL}/file/conversion/gltf/${output_format}`,
feedback: (id: string, feedback: Models['TextToCad_type']['feedback']) =>
`${import.meta.env.VITE_API_BASE_URL}/user/text-to-cad/${id}?feedback=${feedback}`,
list: ({ limit = 5, page_token }: ListParams) =>
list: ({ limit = ITEMS_PER_PAGE, page_token }: ListParams) =>
`${import.meta.env.VITE_API_BASE_URL}/user/text-to-cad?no_models=true&limit=${limit}${
page_token ? `&page_token=${page_token}` : ''
}`,
Expand Down
5 changes: 4 additions & 1 deletion src/lib/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export const generations = derived([combinedGenerations], ([$combinedGenerations
return groupBy($combinedGenerations, ({ created_at }) => bucketByTime(created_at))
})

export const nextPageToken = writable<string | null | undefined>(undefined)
const NEXT_PAGE_TOKENS_KEY = 'nextPageTokens'
export const nextPageTokensInitial = fromLocalStorage<string[]>(NEXT_PAGE_TOKENS_KEY, [])
export const nextPageTokens = writable(nextPageTokensInitial)
toLocalStorage(nextPageTokens, NEXT_PAGE_TOKENS_KEY)

type UserSettings = {
autoRotateModels: boolean
Expand Down
14 changes: 14 additions & 0 deletions tests/e2e.playwright.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ITEMS_PER_PAGE, PAGES_AHEAD_TO_FETCH } from '../src/lib/consts'
import { expect, test } from '@playwright/test'

test('Redirects to the dashboard from home when logged-in', async ({ page }) => {
Expand All @@ -24,3 +25,16 @@ test('Prompt input is visible and usable on mobile', async ({ page }) => {
await expect(page.locator('textarea')).toBeInViewport()
await expect(page.locator('textarea')).toBeFocused()
})

test('Sidebar only loads set number of pages of results initially', async ({ page }) => {
// Go to the home page
await page.goto('https://localhost:3000')

// Assert that we are now on the dashboard
await page.waitForURL('**/dashboard', { waitUntil: 'networkidle' })

// Assert that only 5 pages of results are loaded initially
await expect(page.getByTestId('generation-list').getByRole('link')).toHaveCount(
PAGES_AHEAD_TO_FETCH * ITEMS_PER_PAGE
)
})