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
1 change: 0 additions & 1 deletion frontend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export class App extends MobxLitElement {
return html`
<page-header></page-header>
<quick-start-gallery></quick-start-gallery>
<home-gallery-tabs></home-gallery-tabs>
<home-gallery></home-gallery>
`;
case Pages.ADMIN:
Expand Down
92 changes: 92 additions & 0 deletions frontend/src/components/gallery/home_gallery.scss
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ gallery-card {
}

.banner {
@include typescale.body-small;
background: var(--md-sys-color-tertiary-container);
border-radius: common.$spacing-medium;
color: var(--md-sys-color-on-tertiary-container);
Expand Down Expand Up @@ -134,3 +135,94 @@ h1 {
border-color: var(--md-sys-color-secondary);
}
}

.gallery-header-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: $gallery-width;
padding: 0 common.$spacing-xxl;
box-sizing: border-box;
}

.controls {
margin-top: common.$spacing-large;
display: flex;
align-items: center;
gap: common.$spacing-medium;
}

/* Smooth fade when refreshing */
.gallery-wrapper {
opacity: 1;
visibility: visible;
transition:
opacity 150ms ease,
visibility 150ms ease;
}

.gallery-wrapper.hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}

// Sort
.menu-wrapper {
@include typescale.label-small;
@include common.flex-column;
gap: common.$spacing-small;
overflow: auto;
}

.menu-item {
@include common.nav-item;
display: flex;
min-width: 140px;
justify-content: space-between; /* pushes checkmark to right */
align-items: center;
gap: common.$spacing-small;
}

.menu-item .checkmark {
opacity: 0.9;
}

// Search
.search-container {
position: relative;
display: flex;
align-items: center;
height: 32px;
border: 1px solid var(--md-sys-color-outline-variant);
border-radius: common.$spacing-medium;
padding: 0 common.$spacing-medium 0 28px; /* space for icon */
background: var(--md-sys-color-surface);
cursor: text;
transition:
background 120ms ease,
border-color 120ms ease;

&:hover {
background: var(--md-sys-color-surface-variant);
}

&:focus-within {
border-color: var(--md-sys-color-secondary);
background: var(--md-sys-color-surface);
}
}

/* This is needed to render the search icon as part of the
* text area wrapper. */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the way we usually do this is flex on the search-container with gap, then set flex-shrink: 0 on the pr-icon.

.search-container pr-icon {
position: absolute;
left: common.$spacing-medium;
pointer-events: none;
}

.search-container pr-textarea {
width: 140px;
margin: 0;
}
173 changes: 118 additions & 55 deletions frontend/src/components/gallery/home_gallery.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import '../../pair-components/icon';
import '../../pair-components/menu';
import '../../pair-components/textarea';

import './gallery_card';

import {MobxLitElement} from '@adobe/lit-mobx';
import {CSSResultGroup, html, nothing} from 'lit';
import {customElement} from 'lit/decorators.js';

import {customElement, state} from 'lit/decorators.js';
import {repeat} from 'lit/directives/repeat.js';
import {core} from '../../core/core';
import {AuthService} from '../../services/auth.service';
import {ExperimentEditor} from '../../services/experiment.editor';
import {HomeService} from '../../services/home.service';
import {Pages, RouterService} from '../../services/router.service';

import {Experiment, Visibility} from '@deliberation-lab/utils';
import {
Experiment,
SortMode,
sortLabel,
sortExperiments,
} from '@deliberation-lab/utils';
import {convertExperimentToGalleryItem} from '../../shared/experiment.utils';
import {
getQuickstartAgentGroupChatTemplate,
Expand All @@ -21,7 +29,6 @@ import {getQuickstartPrivateChatTemplate} from '../../shared/templates/quickstar

import {styles} from './home_gallery.scss';

/** Gallery for home/landing page */
@customElement('home-gallery')
export class HomeGallery extends MobxLitElement {
static override styles: CSSResultGroup = [styles];
Expand All @@ -30,27 +37,76 @@ export class HomeGallery extends MobxLitElement {
private readonly homeService = core.getService(HomeService);
private readonly routerService = core.getService(RouterService);

@state() private searchQuery = '';
@state() private sortMode: SortMode = SortMode.NEWEST;
@state() private refreshing = false;

private async setSort(mode: SortMode) {
this.sortMode = mode;
this.refreshing = true;
await new Promise((r) => setTimeout(r, 120));
this.refreshing = false;
}

private renderControls() {
const renderSortItem = (mode: SortMode, label: string) => {
const selected = this.sortMode === mode;
return html`
<div class="menu-item" @click=${() => this.setSort(mode)}>
${label}
${this.sortMode === mode
? html`<span class="checkmark">✔</span>`
: nothing}
</div>
`;
};
return html`
<div class="controls">
<div class="search-container">
<pr-icon icon="search" size="small"></pr-icon>
<pr-textarea
placeholder="Search"
.value=${this.searchQuery}
@input=${(e: InputEvent) =>
(this.searchQuery = (e.target as HTMLTextAreaElement).value)}
></pr-textarea>
</div>

<pr-menu name=${sortLabel(this.sortMode)} icon="sort" color="neutral">
<div class="menu-wrapper">
${renderSortItem(SortMode.NEWEST, 'Newest first')}
${renderSortItem(SortMode.OLDEST, 'Oldest first')}
${renderSortItem(SortMode.ALPHA_ASC, 'Alphabetical (A–Z)')}
${renderSortItem(SortMode.ALPHA_DESC, 'Alphabetical (Z–A)')}
</div>
</pr-menu>
</div>
`;
}

override render() {
const renderExperiment = (experiment: Experiment) => {
const item = convertExperimentToGalleryItem(experiment);

const navigate = () => {
const navigate = () =>
this.routerService.navigate(Pages.EXPERIMENT, {
experiment: experiment.id,
});
};

return html`
<gallery-card .item=${item} @click=${navigate}></gallery-card>
`;
return html`<gallery-card
.item=${item}
@click=${navigate}
></gallery-card>`;
};

const experiments = this.homeService.experiments
.slice()
.sort(
(a, b) =>
b.metadata.dateCreated.seconds - a.metadata.dateCreated.seconds,
let experiments = [...this.homeService.experiments];

if (this.searchQuery.trim()) {
const q = this.searchQuery.toLowerCase();
experiments = experiments.filter((e) =>
e.metadata.name?.toLowerCase().includes(q),
);
}

experiments = sortExperiments(experiments, this.sortMode);

const yourExperiments = experiments.filter(
(e) => e.metadata.creator === this.authService.userEmail,
Expand All @@ -61,26 +117,34 @@ export class HomeGallery extends MobxLitElement {
this.authService.isViewedExperiment(e.id),
);

if (this.homeService.showMyExperiments) {
return html`
<div class="gallery-wrapper">
${this.renderEmptyMessage(yourExperiments)}
${yourExperiments.map((e) => renderExperiment(e))}
</div>
`;
}
const list = this.homeService.showMyExperiments
? yourExperiments
: otherExperiments;

const banner = this.homeService.showMyExperiments
? nothing
: html`
<div class="banner">
Experiments by others will only be shown in this tab if they are
shared publicly and have been viewed by you before. Ask the creator
to share the link with you.
</div>
`;

return html`
<div class="gallery-wrapper">
<div class="banner">
Experiments by others will only be shown in this tab if they are
shared publicly and have been viewed by you before. To view an
experiment, ask the creator to make the experiment public and share
the link with you.
</div>
${this.renderEmptyMessage(otherExperiments)}
${otherExperiments.map((e) => renderExperiment(e))}
<home-gallery-tabs>
<div slot="gallery-controls">${this.renderControls()}</div>
</home-gallery-tabs>
<div class="gallery-wrapper ${this.refreshing ? 'hidden' : ''}">
${banner} ${this.renderEmptyMessage(list)}
${repeat(
list,
(e) => e.id,
(e) => renderExperiment(e),
)}
</div>

${this.refreshing ? html`<div class="placeholder"></div>` : nothing}
`;
}

Expand All @@ -90,41 +154,40 @@ export class HomeGallery extends MobxLitElement {
}
}

/** Tabs for home/landing page */
@customElement('home-gallery-tabs')
export class HomeGalleryTabs extends MobxLitElement {
static override styles: CSSResultGroup = [styles];
private readonly homeService = core.getService(HomeService);

override render() {
return html`
<div class="gallery-tabs">
<div
class="gallery-tab ${this.homeService.showMyExperiments
? 'active'
: ''}"
@click=${() => {
this.homeService.setShowMyExperiments(true);
}}
>
My experiments
</div>
<div
class="gallery-tab ${!this.homeService.showMyExperiments
? 'active'
: ''}"
@click=${() => {
this.homeService.setShowMyExperiments(false);
}}
>
Shared with me
<div class="gallery-header-row">
<div class="gallery-tabs">
<div
class="gallery-tab ${this.homeService.showMyExperiments
? 'active'
: ''}"
@click=${() => this.homeService.setShowMyExperiments(true)}
>
My experiments
</div>
<div
class="gallery-tab ${!this.homeService.showMyExperiments
? 'active'
: ''}"
@click=${() => this.homeService.setShowMyExperiments(false)}
>
Shared with me
</div>
</div>

<slot name="gallery-controls"></slot>
</div>
`;
}
}

/** Quick start cards for home/landing page */
/* Quick start cards for home/landing page */
@customElement('quick-start-gallery')
export class QuickStartGallery extends MobxLitElement {
static override styles: CSSResultGroup = [styles];
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/components/header/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {ExperimentManager} from '../../services/experiment.manager';
import {ParticipantService} from '../../services/participant.service';
import {Pages, RouterService} from '../../services/router.service';

import {DOCUMENTATION_URL} from '../../shared/constants';
import {BUG_REPORT_URL, DOCUMENTATION_URL} from '../../shared/constants';
import {
getParticipantInlineDisplay,
getParticipantStatusDetailText,
Expand Down Expand Up @@ -169,7 +169,7 @@ export class Header extends MobxLitElement {

switch (activePage) {
case Pages.HOME:
return 'Deliberate Lab';
return '🕊️ Deliberate Lab';
case Pages.ADMIN:
return 'Admin dashboard';
case Pages.SETTINGS:
Expand Down Expand Up @@ -224,6 +224,18 @@ export class Header extends MobxLitElement {
>
</pr-icon-button>
</pr-tooltip>
<pr-tooltip text="Report a bug" position="BOTTOM_END">
<pr-icon-button
icon="bug_report"
color="secondary"
variant="default"
@click=${() => {
// TODO: Add Analytics tracking for bug report click
window.open(BUG_REPORT_URL, '_blank');
}}
>
</pr-icon-button>
</pr-tooltip>
<pr-tooltip text="View experimenter settings" position="BOTTOM_END">
<pr-icon-button
icon="settings"
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/shared/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/** Documentation URL. */
/** Documentation URLs. */
export const BUG_REPORT_URL =
'https://github.com/PAIR-code/deliberate-lab/issues/';
export const DOCUMENTATION_URL = 'https://pair-code.github.io/deliberate-lab/';

/** Prolific URL prefix. */
Expand Down
Loading