Skip to content

Commit

Permalink
Franknoirot/limit model loading (#139)
Browse files Browse the repository at this point in the history
* Update guidance on local token in README

* Make items fetched per page a named const, add one for fetch ahead page count

* Refactor GenerationList to fetch only a few pages ahead and fetch more when scroll is near the bottom

* Add a Playwright test to verify this behavior

* Follow Svelte warning and put the initial fetch in an onMount handler

* Update persistence key so we reset localStorage for people
  • Loading branch information
franknoirot authored Jun 21, 2024
1 parent d909bb4 commit 5a5f7cb
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 36 deletions.
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
)
})

0 comments on commit 5a5f7cb

Please sign in to comment.