Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cd23490
feat(embeddings): add typed Admin API data-contract layer
kkopanidis Sep 8, 2026
a58c888
feat(embeddings): register module shell and readiness overview
kkopanidis Sep 8, 2026
ebcad93
feat(embeddings): add config CRUD and index views
kkopanidis Sep 8, 2026
ee5e432
feat(embeddings): add provider settings and worker toggle
kkopanidis Sep 8, 2026
19c69de
feat(embeddings): add backfill list and detail operations
kkopanidis Sep 9, 2026
4646884
feat(embeddings): add operator test search page
kkopanidis Sep 9, 2026
4be90cd
test(embeddings): add Vitest unit foundation
kkopanidis Sep 9, 2026
5db0eec
test(embeddings): add mocked Admin API e2e suite
kkopanidis Sep 9, 2026
6035e17
fix(embeddings): address operator review findings for loading, search…
kkopanidis Sep 9, 2026
9293ed8
refactor(embeddings): share page helpers and drop extra search wrapping
kkopanidis Sep 9, 2026
2273b1d
fix(embeddings): align UI contracts with backend search and enablement
kkopanidis Sep 9, 2026
9b5928b
fix(embeddings): keep provider keys and search documents off the client
kkopanidis Sep 9, 2026
8c294ea
fix(embeddings): slim RSC payloads and fail closed on schema eligibility
kkopanidis Sep 9, 2026
276c176
fix(embeddings): reject disabled schemas in eligibility paths
kkopanidis Sep 9, 2026
005363f
chore(embeddings): trim generic operator copy and share backfill defa…
kkopanidis Sep 9, 2026
09c87ad
fix(embeddings): keep node:test schema-field suite out of Vitest
kkopanidis Sep 9, 2026
b01ce4f
feat(embeddings): redesign settings around a model catalogue
kkopanidis Sep 9, 2026
d37bca8
feat(embeddings): choose configs from catalogue and extendable schemas
kkopanidis Sep 9, 2026
53f4417
fix(embeddings): align catalogue and schema contracts with backend
kkopanidis Sep 9, 2026
12b1b56
chore(embeddings): trim catalogue copy and drop settings type dodges
kkopanidis Sep 9, 2026
a8d3a9f
fix(embeddings): reset config form from persisted upsert result
kkopanidis Sep 9, 2026
53444e5
fix(embeddings): deny platform and database system schemas
kkopanidis Sep 9, 2026
470a5f1
chore(embeddings): collapse schema fixtures and trim catalogue copy
kkopanidis Sep 9, 2026
1a60612
test(embeddings): use exact toast locator and widen CI paths
kkopanidis Sep 9, 2026
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
49 changes: 49 additions & 0 deletions .github/workflows/embeddings-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Embeddings UI

on:
pull_request:
types:
- opened
- synchronize
- reopened
paths:
- 'src/app/(dashboard)/(modules)/embeddings/**'
- 'src/app/(dashboard)/template.tsx'
- 'src/components/embeddings/**'
- 'src/components/dashboard/ActiveModuleGrid.tsx'
- 'src/components/dashboard/ModuleDashboard.tsx'
- 'src/components/navigation/navList.config.ts'
- 'src/lib/models/embeddings/**'
- 'src/lib/api/embeddings/**'
- 'src/lib/api/database/**'
- 'src/lib/utils/module-utils.ts'
- 'src/lib/utils/module-utils.test.ts'
- 'src/lib/utils/breadcrumbs.ts'
- 'src/lib/utils/breadcrumbs.test.ts'
- 'src/test/**'
- 'e2e/**'
- 'playwright.config.ts'
- 'package.json'
- 'pnpm-lock.yaml'
- 'vitest.config.mts'
- '.github/workflows/embeddings-ui.yml'

jobs:
unit-lint-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable Corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm test
- run: pnpm lint
- run: pnpm exec tsc --noEmit
- run: CI=false pnpm build
- name: Install Playwright Chromium
run: pnpm exec playwright install --with-deps chromium
- run: pnpm test:e2e
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,10 @@ dist

# typescript
next-env.d.ts

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/e2e/.auth/
11 changes: 11 additions & 0 deletions e2e/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { test as setup } from '@playwright/test';
import { loginThroughUi } from './helpers/ui.ts';
import { resetMock } from './helpers/mock.ts';

const storagePath = 'e2e/.auth/user.json';

setup('authenticate through login', async ({ page }) => {
await resetMock('ready');
await loginThroughUi(page);
await page.context().storageState({ path: storagePath });
});
96 changes: 96 additions & 0 deletions e2e/backfills.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { expect, test } from '@playwright/test';
import { resetMock } from './helpers/mock.ts';
import { exactText } from './helpers/ui.ts';

test.describe('embedding backfills', () => {
test('starts a run and advances queued to running to completed', async ({
page,
}) => {
await resetMock('ready');
await page.goto('/embeddings/backfills');
await page.getByRole('button', { name: 'Start backfill' }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await page.locator('#start-schema').click();
await page.getByRole('option', { name: 'Product' }).click();
await page
.getByRole('dialog')
.getByRole('button', { name: 'Start backfill' })
.click();
await expect(exactText(page, 'Backfill queued')).toBeVisible();
await expect(page.getByRole('dialog')).toBeHidden();
await page.getByRole('link', { name: 'Product' }).first().click();
await expect(exactText(page, 'Queued')).toBeVisible();
await expect(exactText(page, 'Running')).toBeVisible({ timeout: 15_000 });
await expect(exactText(page, 'Completed')).toBeVisible({
timeout: 15_000,
});
});

test('cancels a run and resumes it', async ({ page }) => {
await resetMock('ready');
await page.goto('/embeddings/backfills');
await page.getByRole('button', { name: 'Start backfill' }).click();
await page.locator('#start-schema').click();
await page.getByRole('option', { name: 'Product' }).click();
await page
.getByRole('dialog')
.getByRole('button', { name: 'Start backfill' })
.click();
await expect(exactText(page, 'Backfill queued')).toBeVisible();
await expect(page.getByRole('dialog')).toBeHidden();
await page.getByRole('link', { name: 'Product' }).first().click();
await page.getByRole('button', { name: 'Cancel run' }).click();
await expect(page.getByRole('alertdialog')).toBeVisible();
await page
.getByRole('alertdialog')
.getByRole('button', { name: 'Cancel run' })
.click();
await expect(exactText(page, 'Backfill canceled')).toBeVisible();
await expect(exactText(page, 'Canceled').first()).toBeVisible();
await page.getByRole('button', { name: 'Resume run' }).click();
await expect(page.getByRole('alertdialog')).toBeVisible();
await page
.getByRole('alertdialog')
.getByRole('button', { name: 'Resume run' })
.click();
await expect(exactText(page, 'Backfill resumed')).toBeVisible();
await expect(exactText(page, 'Queued')).toBeVisible();
});

test('cancels and resumes from the list row', async ({ page }) => {
await resetMock('ready');
await page.goto('/embeddings/backfills');
await page.getByRole('button', { name: 'Start backfill' }).click();
await page.locator('#start-schema').click();
await page.getByRole('option', { name: 'Product' }).click();
await page
.getByRole('dialog')
.getByRole('button', { name: 'Start backfill' })
.click();
await expect(exactText(page, 'Backfill queued')).toBeVisible();
await expect(page.getByRole('dialog')).toBeHidden();
await page.getByRole('button', { name: 'Cancel Product run' }).click();
await expect(page.getByRole('alertdialog')).toBeVisible();
await page
.getByRole('alertdialog')
.getByRole('button', { name: 'Cancel run' })
.click();
await expect(exactText(page, 'Backfill canceled')).toBeVisible();
await page.getByRole('button', { name: 'Resume Product run' }).click();
await expect(page.getByRole('alertdialog')).toBeVisible();
await page
.getByRole('alertdialog')
.getByRole('button', { name: 'Resume run' })
.click();
await expect(exactText(page, 'Backfill resumed')).toBeVisible();
await expect(exactText(page, 'Queued')).toBeVisible();
});

test('disables next page when the list is empty', async ({ page }) => {
await resetMock('blank');
await page.goto('/embeddings/backfills');
await expect(
page.getByRole('button', { name: 'Go to next page' })
).toBeDisabled();
});
});
220 changes: 220 additions & 0 deletions e2e/configs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { expect, test } from '@playwright/test';
import { resetMock } from './helpers/mock.ts';

test.describe('embedding configs', () => {
test('creates a config and shows a pending matching index', async ({
page,
}) => {
await resetMock('blank');
await page.goto('/embeddings/configs/new');
await expect(
page.getByRole('heading', { name: 'New config' })
).toBeVisible();
await page.getByRole('combobox', { name: 'Schema' }).click();
await page.getByRole('option', { name: 'Product' }).click();
await page.getByRole('checkbox', { name: 'title' }).click();
await page.getByLabel('Target field').fill('embedding');
await page.getByRole('button', { name: 'Create config' }).click();
await expect(page).toHaveURL(/\/embeddings\/configs\/cfg_/);
await expect(
page.getByRole('heading', { name: 'Matching index' })
).toBeVisible();
await expect(page.getByText('Pending', { exact: true })).toBeVisible();
await expect(
page.getByRole('heading', { name: 'Index is not queryable yet' })
).toBeVisible();
await expect(
page.getByText('Not queryable', { exact: true })
).toBeVisible();
});

test('omits disabled, non-extendable, and internal schemas from create choices', async ({
page,
}) => {
await resetMock('blank');
await page.goto('/embeddings/configs/new');
await page.getByRole('combobox', { name: 'Schema' }).click();
await expect(page.getByRole('option', { name: 'Product' })).toBeVisible();
await expect(page.getByRole('option', { name: 'User' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Team' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Note' })).toBeVisible();
await expect(
page.getByRole('option', { name: 'ArchivedProduct' })
).toHaveCount(0);
await expect(page.getByRole('option', { name: 'CmsOnly' })).toHaveCount(0);
await expect(
page.getByRole('option', { name: 'Admin', exact: true })
).toHaveCount(0);
await expect(
page.getByRole('option', { name: 'AdminMiddleware' })
).toHaveCount(0);
await expect(
page.getByRole('option', { name: 'AppMiddleware' })
).toHaveCount(0);
await expect(page.getByRole('option', { name: 'Client' })).toHaveCount(0);
await expect(page.getByRole('option', { name: 'Config' })).toHaveCount(0);
await expect(page.getByRole('option', { name: 'Views' })).toHaveCount(0);
await page.getByPlaceholder('Search schemas').fill('cms');
await expect(page.getByText('No matching schemas')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByPlaceholder('Search schemas')).toHaveCount(0);
});

test('asks for a schema before listing source fields', async ({ page }) => {
await resetMock('blank');
await page.goto('/embeddings/configs/new');
await expect(
page.getByText('Select a schema to see eligible fields.')
).toBeVisible();
});

test('uses route labels instead of raw config ids in breadcrumbs', async ({
page,
}) => {
await resetMock('ready');
await page.goto('/embeddings/configs/cfg_product');
const crumbs = page.getByRole('navigation', { name: 'breadcrumb' });
await expect(crumbs.getByText('Configs', { exact: true })).toBeVisible();
await expect(crumbs.getByText('Config', { exact: true })).toBeVisible();
await expect(crumbs.getByText('Cfg_product')).toHaveCount(0);
});

test('shows stacked config cards on a narrow viewport', async ({ page }) => {
await resetMock('ready');
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/embeddings/configs');
await expect(page.getByRole('heading', { name: 'Configs' })).toBeVisible();
await expect(
page.getByRole('term').filter({ hasText: 'Index' }).first()
).toBeVisible();
await expect(
page
.getByRole('definition')
.filter({ hasText: 'openai-compatible/text-embedding-3-small' })
.first()
).toBeVisible();
await expect(
page.getByRole('definition').filter({ hasText: 'Ready' }).first()
).toBeVisible();
});

test('selects the highest ready generation over a pending v1', async ({
page,
}) => {
await resetMock('ready');
await page.goto('/embeddings/configs/cfg_product');
await expect(page.getByText('Product_embedding_v2')).toBeVisible();
await expect(page.getByText('Product_embedding_v1')).toHaveCount(0);
const generation = page.getByRole('term').filter({ hasText: 'Generation' });
await expect(generation).toBeVisible();
await expect(
generation.locator('xpath=following-sibling::dd[1]')
).toHaveText('2');
});

test('searches schemas with arrows and enter', async ({ page }) => {
await resetMock('blank');
await page.goto('/embeddings/configs/new');
const schema = page.getByRole('combobox', { name: 'Schema' });
await schema.click();
await page.getByPlaceholder('Search schemas').fill('pro');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect(schema).toContainText('Product');
});

test('derives dimensions from the selected model and clears on provider change', async ({
page,
}) => {
await resetMock('blank');
await page.goto('/embeddings/configs/new');
await expect(page.getByLabel('Dimensions')).toHaveValue('1536');
await expect(page.getByLabel('Dimensions')).toHaveAttribute('readonly', '');
await expect(page.getByText(/Recommended for text/)).toBeVisible();
await page.getByRole('combobox', { name: 'Provider' }).click();
await page.getByRole('option', { name: 'voyage' }).click();
await expect(page.getByRole('combobox', { name: 'Model' })).toContainText(
'Select a model'
);
await page.getByRole('combobox', { name: 'Model' }).click();
await page.getByRole('option', { name: 'voyage-3' }).click();
await expect(page.getByLabel('Dimensions')).toHaveValue('1024');
});

test('keeps the only provider visible and disabled on an existing config', async ({
page,
}) => {
await resetMock('ready');
await page.goto('/embeddings/configs/cfg_product');
const provider = page.getByRole('combobox', { name: 'Provider' });
await expect(provider).toBeVisible();
await expect(provider).toBeDisabled();
await expect(provider).toContainText('openai-compatible');
});

test('rejects an incompatible target field collision', async ({ page }) => {
await resetMock('blank');
await page.goto('/embeddings/configs/new');
await page.getByRole('combobox', { name: 'Schema' }).click();
await page.getByRole('option', { name: 'Note' }).click();
await page.getByRole('checkbox', { name: 'title' }).click();
await page.getByLabel('Target field').fill('embedding');
await page.getByRole('button', { name: 'Create config' }).click();
await expect(
page
.getByRole('region', { name: 'Notifications (F8)' })
.getByText(
"Field 'embedding' already exists on schema 'Note' and is not a compatible embeddings extension"
)
).toBeVisible();
});

test('resets enabled after a material edit that stays pending', async ({
page,
}) => {
await resetMock('ready');
await page.goto('/embeddings/configs/cfg_product');
const enabled = page.getByRole('switch', { name: 'Enabled' });
await expect(enabled).toBeChecked();
await page.getByRole('combobox', { name: 'Similarity' }).click();
await page.getByRole('option', { name: 'Euclidean' }).click();
await page.getByRole('button', { name: /Save changes/ }).click();
const dialog = page.getByRole('alertdialog');
await expect(dialog).toBeVisible();
await expect(
dialog.getByRole('heading', { name: 'Save material changes?' })
).toBeVisible();
await dialog.getByRole('button', { name: 'Save changes' }).click();
await expect(
page
.getByRole('region', { name: 'Notifications (F8)' })
.getByText(
'Config stayed disabled until the matching index is queryable.'
)
).toBeVisible();
await expect(enabled).not.toBeChecked();
await expect(
page.getByRole('button', { name: 'Save changes', exact: true })
).toBeDisabled();
await expect(page.getByText('Pending', { exact: true })).toBeVisible();
});

test('blocks a config whose model is absent from the catalogue', async ({
page,
}) => {
await resetMock('ready');
await page.goto('/embeddings/configs/cfg_legacy');
await expect(
page.getByText(
'This model is not in the provider catalogue. Add it in Settings before changing this config.'
)
).toBeVisible();
await expect(
page.getByRole('link', { name: 'Open settings' }).first()
).toHaveAttribute('href', '/embeddings/settings');
await expect(page.getByRole('combobox', { name: 'Model' })).toContainText(
'text-embedding-ada-002'
);
await expect(page.getByRole('combobox', { name: 'Model' })).toBeDisabled();
});
});
Loading
Loading