Skip to content
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
15 changes: 14 additions & 1 deletion frontend/ai.client/src/app/artifacts/artifact-library.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ <h2 class="mt-3 text-sm/6 font-medium text-gray-900 dark:text-white">No artifact
[attr.role]="sharedAvailable() ? 'tabpanel' : null"
[attr.aria-labelledby]="sharedAvailable() ? 'artifact-tab-' + tab() : null"
>
<!-- Empty: the filters excluded everything -->
<!-- Empty: the filters excluded everything in this tab -->
@if (isFilteredEmpty()) {
<div
class="mt-6 rounded-2xl border border-dashed border-gray-300 bg-white p-10 text-center dark:border-gray-700 dark:bg-gray-800"
Expand All @@ -240,6 +240,19 @@ <h2 class="mt-3 text-sm/6 font-medium text-gray-900 dark:text-white">No artifact
</div>
}

<!-- Empty: this tab holds nothing, but the library is not empty. A
distinct state from both of the above — saying "no match" to
someone who never searched is a wrong answer. -->
@if (isTabEmpty()) {
<div
class="mt-6 rounded-2xl border border-dashed border-gray-300 bg-white p-10 text-center dark:border-gray-700 dark:bg-gray-800"
>
<p class="mx-auto max-w-md text-sm/6 text-gray-600 dark:text-gray-400">
{{ emptyTabMessage() }}
</p>
</div>
}

<!-- List view -->
@if (!loading() && filtered().length > 0 && viewMode() === 'list') {
<ul
Expand Down
51 changes: 51 additions & 0 deletions frontend/ai.client/src/app/artifacts/artifact-library.page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ describe('ArtifactLibraryPage', () => {
typeOptions: () => Array<{ value: string; label: string }>;
isEmpty: () => boolean;
isFilteredEmpty: () => boolean;
isTabEmpty: () => boolean;
emptyTabMessage: () => string;
error: () => string | null;
loading: () => boolean;
search: { set: (v: string) => void };
Expand Down Expand Up @@ -521,6 +523,55 @@ describe('ArtifactLibraryPage', () => {
expect(thumbnail.closest('[aria-hidden="true"]')).not.toBeNull();
});

it('does not blame a search the user never made', async () => {
// Regression: opening "Shared with you" with nothing shared used to
// show "No artifacts match your search" because the empty-state
// gate read the LIBRARY total rather than the tab's. Found on dev.
mockHttp.listLibrary.mockResolvedValue([stubArtifact()]);
mockShares.listSharedWithMe.mockResolvedValue({
artifacts: [],
nextCursor: null,
});
const c = api(await createComponent());
c.setTab('shared');

expect(c.isFilteredEmpty()).toBe(false);
expect(c.isTabEmpty()).toBe(true);
expect(c.emptyTabMessage()).toContain('shared with you');
});

it('still blames the search when there really was one', async () => {
mockHttp.listLibrary.mockResolvedValue([
stubArtifact({ title: 'Budget model' }),
]);
mockShares.listSharedWithMe.mockResolvedValue({
artifacts: [],
nextCursor: null,
});
const c = api(await createComponent());
c.setTab('yours');
c.search.set('nothing matches this');

expect(c.isTabEmpty()).toBe(false);
expect(c.isFilteredEmpty()).toBe(true);
});

it('reports an empty library ahead of an empty tab', async () => {
// With nothing anywhere, "No artifacts yet" is the true statement;
// a per-tab message would bury the one that matters.
mockHttp.listLibrary.mockResolvedValue([]);
mockShares.listSharedWithMe.mockResolvedValue({
artifacts: [],
nextCursor: null,
});
const c = api(await createComponent());
c.setTab('shared');

expect(c.isEmpty()).toBe(true);
expect(c.isTabEmpty()).toBe(false);
expect(c.isFilteredEmpty()).toBe(false);
});

it('falls back to a placeholder title and an undated label', async () => {
mockHttp.listLibrary.mockResolvedValue([
stubArtifact({ title: '', updatedAt: '' }),
Expand Down
45 changes: 42 additions & 3 deletions frontend/ai.client/src/app/artifacts/artifact-library.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,16 +383,55 @@ export class ArtifactLibraryPage {
);

/**
* "Nothing matches" is a different message from "you have nothing", and
* conflating them tells a user with a full library that it is empty.
* "Nothing matches" is a different message from "you have nothing" —
* and once tabs exist there is a third: "nothing *here*".
*
* This gates on the count of the SELECTED TAB, not the library. It
* used to gate on the library total, which meant opening "Shared with
* you" with nothing shared told the user "No artifacts match your
* search" when they had not searched for anything — a wrong answer,
* and the exact conflation the previous version of this comment was
* written to prevent, reintroduced one level up. Found on dev, not by
* a test: the specs asserted which rows rendered, never which sentence
* appeared when none did.
*/
protected readonly isFilteredEmpty = computed(
() =>
!this.loading() &&
this.totalCount() > 0 &&
this.tabCount() > 0 &&
this.filtered().length === 0,
);

/**
* The selected tab holds nothing, but the library is not empty — so
* neither "No artifacts yet" nor a search failure is true.
*/
protected readonly isTabEmpty = computed(
() =>
!this.loading() &&
!this.error() &&
this.totalCount() > 0 &&
this.tabCount() === 0,
);

/**
* Copy for that state, which has to name the tab: "nothing here" is
* only useful if it says where "here" is.
*/
protected readonly emptyTabMessage = computed(() => {
switch (this.tab()) {
case 'shared':
return 'Nothing has been shared with you yet. Artifacts other ' +
'people share with you directly will appear here.';
case 'yours':
return "You haven't made any artifacts yet.";
default:
// Unreachable while "All" is the union of the other two, but a
// silent blank panel is the worst way to find out that changed.
return 'Nothing to show here.';
}
});

constructor() {
void this.load();
}
Expand Down