diff --git a/packages/web/src/index.css b/packages/web/src/index.css index 58a4a5c4..db484f8b 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -546,4 +546,150 @@ dialog { } } -} \ No newline at end of file +} + +/* Provider Filter */ +.provider-filter-button { + background-color: var(--color-background); + color: var(--color-text); + border: 1px solid var(--color-border); + display: flex; + align-items: center; + gap: 0.375rem; + + &:hover { + background-color: var(--color-surface); + } + + #provider-count { + color: var(--color-text-tertiary); + } +} + +.provider-popover { + position: fixed; + z-index: 100; + background-color: var(--color-background); + border: 1px solid var(--color-border); + border-radius: 0.375rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05), 0 4px 8px rgba(0, 0, 0, 0.05), + 0 8px 16px rgba(0, 0, 0, 0.07), 0 16px 32px rgba(0, 0, 0, 0.07); + width: 20rem; + max-height: 28rem; + overflow: hidden; +} + +.provider-popover-content { + display: flex; + flex-direction: column; + max-height: 28rem; +} + +.provider-search-container { + padding: 0.75rem; + border-bottom: 1px solid var(--color-border); + flex: 0 0 auto; + display: flex; + gap: 0.5rem; + align-items: center; + + input { + flex: 1 1 auto; + font-size: 0.8125rem; + line-height: 1.1; + padding: 0.5rem 0.625rem; + border-radius: 0.25rem; + border: 1px solid var(--color-border); + height: 2rem; + background: none; + color: var(--color-text); + + &:focus { + border-color: var(--color-brand); + outline: none; + } + } +} + +.provider-reset-button { + flex: 0 0 auto; + cursor: pointer; + border: 1px solid var(--color-border); + background-color: var(--color-background); + color: var(--color-text); + font-size: 0.8125rem; + line-height: 1.1; + height: 2rem; + padding: 0.5rem 0.75rem; + border-radius: 0.25rem; + white-space: nowrap; + + &:hover:not(:disabled) { + background-color: var(--color-surface); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + color: var(--color-text-tertiary); + } +} + +.provider-list { + flex: 1; + overflow-y: auto; + padding: 0.375rem; + overscroll-behavior: contain; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--color-border); + border-radius: 4px; + } +} + +.provider-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.625rem; + cursor: pointer; + border-radius: 0.25rem; + user-select: none; + + &:hover { + background-color: var(--color-surface); + } +} + +.provider-checkbox { + flex-shrink: 0; + width: 1rem; + height: 1rem; + cursor: pointer; + accent-color: var(--color-brand); + pointer-events: none; +} + +.provider-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 1rem; + height: 1rem; + + svg { + width: 100%; + height: 100%; + color: var(--color-text-secondary); + } +} + +.provider-name { + flex: 1; + font-size: 0.875rem; +} diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index 393ee535..338fced1 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -143,21 +143,60 @@ document.querySelectorAll("th.sortable").forEach((header) => { }); }); +/////////////////////////////// +// Handle Provider Filter +/////////////////////////////// +const providerFilterButton = document.getElementById("provider-filter")!; +const providerPopover = document.getElementById("provider-popover")!; +const providerSearch = document.getElementById( + "provider-search" +)! as HTMLInputElement; +const providerResetButton = document.getElementById( + "provider-reset" +)! as HTMLButtonElement; +const providerCheckboxes = document.querySelectorAll( + ".provider-checkbox" +) as NodeListOf; +const providerItems = document.querySelectorAll( + ".provider-item" +) as NodeListOf; +const providerCountSpan = document.getElementById("provider-count")!; + +const allProviderValues = Array.from(providerCheckboxes).map((cb) => cb.value); +let selectedProviders = new Set(allProviderValues); + /////////////////// // Handle Search /////////////////// function filterTable(value: string) { - const lowerCaseValues = value.toLowerCase().split(",").filter(str => str.trim() !== ""); + const lowerCaseValues = value + .toLowerCase() + .split(",") + .filter((str) => str.trim() !== ""); const rows = document.querySelectorAll( "table tbody tr" ) as NodeListOf; rows.forEach((row) => { + const providerId = row.cells[2].textContent?.trim() || ""; + const isProviderSelected = selectedProviders.has(providerId); + + if (!isProviderSelected) { + row.style.display = "none"; + return; + } + + if (lowerCaseValues.length === 0) { + row.style.display = ""; + return; + } + const cellTexts = Array.from(row.cells).map((cell) => cell.textContent!.toLowerCase() ); - const isVisible = lowerCaseValues.length === 0 || - lowerCaseValues.some((lowerCaseValue) => cellTexts.some((text) => text.includes(lowerCaseValue))); + const isVisible = lowerCaseValues.some((lowerCaseValue) => + cellTexts.some((text) => text.includes(lowerCaseValue)) + ); row.style.display = isVisible ? "" : "none"; }); @@ -182,6 +221,124 @@ search.addEventListener("keydown", (e) => { } }); +function updateProviderCount() { + const totalProviders = providerCheckboxes.length; + const selectedCount = selectedProviders.size; + providerCountSpan.textContent = `${selectedCount}/${totalProviders}`; + providerResetButton.disabled = selectedCount === totalProviders; +} + +function filterByProviders() { + filterTable(search.value); + updateProviderCount(); + updateQueryParams({ + providers: + selectedProviders.size === providerCheckboxes.length + ? null + : Array.from(selectedProviders).sort().join(","), + }); +} + +function togglePopover() { + const isVisible = providerPopover.style.display !== "none"; + providerPopover.style.display = isVisible ? "none" : "block"; + + if (!isVisible) { + const buttonRect = providerFilterButton.getBoundingClientRect(); + providerPopover.style.top = `${buttonRect.bottom + 4}px`; + providerPopover.style.left = `${ + buttonRect.right - providerPopover.offsetWidth + }px`; + } +} + +function closePopover() { + providerPopover.style.display = "none"; + + providerSearch.value = ""; + filterProviderList(""); +} + +function filterProviderList(searchValue: string) { + const searchLower = searchValue.toLowerCase(); + providerItems.forEach((item) => { + const providerName = item.getAttribute("data-provider-name") || ""; + if (providerName.includes(searchLower)) { + item.style.display = ""; + } else { + item.style.display = "none"; + } + }); +} + +providerFilterButton.addEventListener("click", (e) => { + e.stopPropagation(); + togglePopover(); +}); + +document.addEventListener("click", (e) => { + if ( + !providerPopover.contains(e.target as Node) && + !providerFilterButton.contains(e.target as Node) + ) { + closePopover(); + } +}); + +providerSearch.addEventListener("input", () => { + filterProviderList(providerSearch.value); +}); + +providerResetButton.addEventListener("click", (e) => { + e.stopPropagation(); + + selectedProviders = new Set(allProviderValues); + providerCheckboxes.forEach((cb) => { + cb.checked = true; + }); + + providerSearch.value = ""; + filterProviderList(""); + + filterByProviders(); +}); + +providerItems.forEach((item) => { + item.addEventListener("click", (e) => { + e.preventDefault(); + + const checkbox = item.querySelector( + ".provider-checkbox" + ) as HTMLInputElement; + const providerId = checkbox.value; + const wasChecked = checkbox.checked; + const allSelected = selectedProviders.size === providerCheckboxes.length; + + if (allSelected) { + selectedProviders.clear(); + selectedProviders.add(providerId); + providerCheckboxes.forEach((cb) => { + cb.checked = cb.value === providerId; + }); + } else if (wasChecked) { + if (selectedProviders.size === 1) { + selectedProviders = new Set(allProviderValues); + providerCheckboxes.forEach((cb) => { + cb.checked = true; + }); + } else { + selectedProviders.delete(providerId); + checkbox.checked = false; + } + } else { + selectedProviders.add(providerId); + checkbox.checked = true; + } + + filterByProviders(); + }); +}); + /////////////////////////////////// // Handle Copy model ID function /////////////////////////////////// @@ -217,6 +374,19 @@ search.addEventListener("keydown", (e) => { function initializeFromURL() { const params = getQueryParams(); + (() => { + const providersParam = params.get("providers"); + if (providersParam) { + const providerIds = providersParam.split(","); + selectedProviders = new Set(providerIds); + + providerCheckboxes.forEach((cb) => { + cb.checked = selectedProviders.has(cb.value); + }); + } + updateProviderCount(); + })(); + (() => { const searchQuery = params.get("search"); if (!searchQuery) return; @@ -234,6 +404,10 @@ function initializeFromURL() { const direction = (params.get("order") as "asc" | "desc") || "asc"; sortTable(columnIndex, direction); })(); + + if (selectedProviders.size < providerCheckboxes.length) { + filterByProviders(); + } } document.addEventListener("DOMContentLoaded", initializeFromURL); diff --git a/packages/web/src/render.tsx b/packages/web/src/render.tsx index 41e1d461..defa629a 100644 --- a/packages/web/src/render.tsx +++ b/packages/web/src/render.tsx @@ -210,9 +210,50 @@ export const Rendered = renderToString( ⌘K + +