From bc0097617351be4a5323dd40f86c26cc432a3916 Mon Sep 17 00:00:00 2001 From: Ellen Jiang Date: Tue, 3 Feb 2026 17:24:53 -0500 Subject: [PATCH 1/3] Add build-time environment variable to specify application mode (public vs internal) for experimental features --- frontend/.storybook/main.ts | 11 +++++ frontend/declaration.d.ts | 3 ++ frontend/package.json | 23 ++++++--- frontend/src/service_provider.ts | 4 ++ frontend/src/services/app_config.service.ts | 55 +++++++++++++++++++++ frontend/webpack.config.ts | 3 +- 6 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 frontend/src/services/app_config.service.ts diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts index 5fb27ef8..016b38cb 100644 --- a/frontend/.storybook/main.ts +++ b/frontend/.storybook/main.ts @@ -16,6 +16,7 @@ */ import type { Options } from "@swc/core"; import type { StorybookConfig } from "@storybook/web-components-webpack5"; +import * as webpack from "webpack"; const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], @@ -104,5 +105,15 @@ const config: StorybookConfig = { return newConfig; }, + webpackFinal: async (config) => { + config.plugins = config.plugins || []; + config.plugins.push( + new webpack.DefinePlugin({ + APP_MODE: JSON.stringify("public"), + }), + ); + return config; + }, }; export default config; + diff --git a/frontend/declaration.d.ts b/frontend/declaration.d.ts index b1a3514b..c751fdd6 100644 --- a/frontend/declaration.d.ts +++ b/frontend/declaration.d.ts @@ -26,3 +26,6 @@ declare const GIT_VERSION: string; declare const GIT_COMMIT_HASH: string; declare const GIT_BRANCH: string; declare const GIT_LAST_COMMIT_DATETIME: string; + +/** App deployment mode: 'public' or 'internal' */ +declare const APP_MODE: "public" | "internal"; diff --git a/frontend/package.json b/frontend/package.json index 0d938a79..1ee3f6f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,13 +9,20 @@ }, "private": true, "scripts": { - "build": "webpack --mode development", - "build:prod": "webpack --mode production", - "serve": "webpack serve --mode development", - "serve:prod": "webpack serve --mode production", - "start": "npm run build && npm run serve", - "start:prod": "npm run build:prod && npm run serve:prod", - "deploy:prod": "npm run build:prod && gcloud app deploy", + "build:public": "APP_MODE=public webpack --mode development", + "build:public:prod": "APP_MODE=public webpack --mode production", + "build:internal": "APP_MODE=internal webpack --mode development", + "build:internal:prod": "APP_MODE=internal webpack --mode production", + "serve:public": "APP_MODE=public webpack serve --mode development", + "serve:public:prod": "APP_MODE=public webpack serve --mode production", + "serve:internal": "APP_MODE=internal webpack serve --mode development", + "serve:internal:prod": "APP_MODE=internal webpack serve --mode production", + "start:public": "npm run build:public && npm run serve:public", + "start:public:prod": "npm run build:public:prod && npm run serve:public:prod", + "start:internal": "npm run build:internal && npm run serve:internal", + "start:internal:prod": "npm run build:internal:prod && npm run serve:internal:prod", + "deploy:public": "npm run build:public:prod && gcloud app deploy", + "deploy:internal": "npm run build:internal:prod && gcloud app deploy", "lint": "eslint src", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", @@ -97,4 +104,4 @@ "plugin:storybook/recommended" ] } -} +} \ No newline at end of file diff --git a/frontend/src/service_provider.ts b/frontend/src/service_provider.ts index c69f73fd..20e6a92a 100644 --- a/frontend/src/service_provider.ts +++ b/frontend/src/service_provider.ts @@ -17,6 +17,7 @@ import { Core } from "./core/core"; import { AnalyticsService } from "./services/analytics.service"; +import { AppConfigService } from "./services/app_config.service"; import { BannerService } from "./services/banner.service"; import { DialogService } from "./services/dialog.service"; import { DocumentStateService } from "./services/document_state.service"; @@ -38,6 +39,9 @@ export function makeServiceProvider(self: Core) { get analyticsService() { return self.getService(AnalyticsService); }, + get appConfigService() { + return self.getService(AppConfigService); + }, get bannerService() { return self.getService(BannerService); }, diff --git a/frontend/src/services/app_config.service.ts b/frontend/src/services/app_config.service.ts new file mode 100644 index 00000000..a9ce4b1b --- /dev/null +++ b/frontend/src/services/app_config.service.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { computed, makeObservable } from "mobx"; + +import { Service } from "./service"; + +export type AppMode = "public" | "internal"; + +/** + * Service for accessing app configuration based on deployment mode. + * The mode is set at build time via the APP_MODE environment variable. + */ +export class AppConfigService extends Service { + constructor() { + super(); + makeObservable(this); + } + + /** Current app deployment mode. */ + @computed get mode(): AppMode { + return APP_MODE; + } + + /** Whether the app is running in internal (authenticated) mode. */ + @computed get isInternalMode(): boolean { + return this.mode === "internal"; + } + + /** Whether the app is running in public (open access) mode. */ + @computed get isPublicMode(): boolean { + return this.mode === "public"; + } + + /** Feature flags based on deployment mode. */ + @computed get features() { + return { + authentication: this.isInternalMode, + }; + } +} diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index ab304e63..db236c96 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -68,8 +68,9 @@ const config: webpack.Configuration = { GIT_COMMIT_HASH: JSON.stringify(gitRevisionPlugin.commithash()), GIT_BRANCH: JSON.stringify(gitRevisionPlugin.branch()), GIT_LAST_COMMIT_DATETIME: JSON.stringify( - gitRevisionPlugin.lastcommitdatetime() + gitRevisionPlugin.lastcommitdatetime(), ), + APP_MODE: JSON.stringify(process.env.APP_MODE || "public"), }), new webpack.ProvidePlugin({ process: "process/browser", From 7416c61683b863da16f11d2611f3177899bb0c74 Mon Sep 17 00:00:00 2001 From: Ellen Jiang Date: Wed, 4 Feb 2026 11:47:07 -0500 Subject: [PATCH 2/3] Add auth service for managing firebase authentication --- frontend/src/service_provider.ts | 4 + frontend/src/services/auth.service.ts | 146 ++++++++++++++++++++++ frontend/src/services/firebase.service.ts | 20 ++- 3 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 frontend/src/services/auth.service.ts diff --git a/frontend/src/service_provider.ts b/frontend/src/service_provider.ts index 20e6a92a..63e2740c 100644 --- a/frontend/src/service_provider.ts +++ b/frontend/src/service_provider.ts @@ -18,6 +18,7 @@ import { Core } from "./core/core"; import { AnalyticsService } from "./services/analytics.service"; import { AppConfigService } from "./services/app_config.service"; +import { AuthService } from "./services/auth.service"; import { BannerService } from "./services/banner.service"; import { DialogService } from "./services/dialog.service"; import { DocumentStateService } from "./services/document_state.service"; @@ -42,6 +43,9 @@ export function makeServiceProvider(self: Core) { get appConfigService() { return self.getService(AppConfigService); }, + get authService() { + return self.getService(AuthService); + }, get bannerService() { return self.getService(BannerService); }, diff --git a/frontend/src/services/auth.service.ts b/frontend/src/services/auth.service.ts new file mode 100644 index 00000000..41ed9097 --- /dev/null +++ b/frontend/src/services/auth.service.ts @@ -0,0 +1,146 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GoogleAuthProvider, + User, + onAuthStateChanged, + signInWithPopup, + signOut, +} from "firebase/auth"; +import { + action, + computed, + makeObservable, + observable, + runInAction, +} from "mobx"; + +import { Service } from "./service"; +import { FirebaseService } from "./firebase.service"; + +interface ServiceProvider { + firebaseService: FirebaseService; +} + +/** + * Service for managing Firebase Authentication. + * Only active in internal mode (when APP_MODE === "internal"). + */ +export class AuthService extends Service { + constructor(private readonly sp: ServiceProvider) { + super(); + makeObservable(this); + + // Only initialize auth in internal mode + if (APP_MODE === "internal") { + this.initializeAuth(); + } else { + // In public mode, auth is not used + this.isLoading = false; + } + } + + /** The currently authenticated user, or null if not signed in. */ + @observable user: User | null = null; + + /** Whether auth state is still being determined. */ + @observable isLoading = true; + + /** Error message from the last auth operation, if any. */ + @observable error: string | null = null; + + /** Whether the user is currently authenticated. */ + @computed get isAuthenticated(): boolean { + return this.user !== null; + } + + /** The current user's UID, or null if not authenticated. */ + @computed get userId(): string | null { + return this.user?.uid ?? null; + } + + /** The current user's display name. */ + @computed get displayName(): string | null { + return this.user?.displayName ?? null; + } + + /** The current user's email. */ + @computed get email(): string | null { + return this.user?.email ?? null; + } + + /** The current user's photo URL. */ + @computed get photoURL(): string | null { + return this.user?.photoURL ?? null; + } + + private initializeAuth() { + const auth = this.sp.firebaseService.auth; + if (!auth) { + console.warn("AuthService: Firebase Auth not initialized"); + this.isLoading = false; + return; + } + + onAuthStateChanged(auth, (user) => { + runInAction(() => { + this.user = user; + this.isLoading = false; + this.error = null; + }); + }); + } + + /** Sign in with Google using a popup. */ + @action + async signInWithGoogle(): Promise { + const auth = this.sp.firebaseService.auth; + if (!auth) { + this.error = "Authentication not available"; + return; + } + + try { + this.error = null; + const provider = new GoogleAuthProvider(); + await signInWithPopup(auth, provider); + } catch (e) { + runInAction(() => { + this.error = e instanceof Error ? e.message : "Sign in failed"; + }); + console.error("Sign in error:", e); + } + } + + /** Sign out the current user. */ + @action + async signOut(): Promise { + const auth = this.sp.firebaseService.auth; + if (!auth) return; + + try { + this.error = null; + await signOut(auth); + } catch (e) { + runInAction(() => { + this.error = e instanceof Error ? e.message : "Sign out failed"; + }); + console.error("Sign out error:", e); + } + } +} diff --git a/frontend/src/services/firebase.service.ts b/frontend/src/services/firebase.service.ts index 16066bad..ab48f345 100644 --- a/frontend/src/services/firebase.service.ts +++ b/frontend/src/services/firebase.service.ts @@ -16,6 +16,7 @@ */ import { FirebaseApp, initializeApp } from "firebase/app"; +import { Auth, connectAuthEmulator, getAuth } from "firebase/auth"; import { type Firestore, Unsubscribe, @@ -57,6 +58,11 @@ export class FirebaseService extends Service { this.functions = getFunctions(this.app); this.storage = getStorage(this.app); + // Initialize Auth only in internal mode + if (APP_MODE === "internal") { + this.auth = getAuth(this.app); + } + // Only register emulators if in dev mode if (process.env.NODE_ENV === "development") { this.registerEmulators(); @@ -67,24 +73,32 @@ export class FirebaseService extends Service { firestore: Firestore; functions: Functions; storage: FirebaseStorage; + auth: Auth | null = null; unsubscribe: Unsubscribe[] = []; registerEmulators() { connectFirestoreEmulator( this.firestore, "localhost", - FIREBASE_LOCAL_HOST_PORT_FIRESTORE + FIREBASE_LOCAL_HOST_PORT_FIRESTORE, ); connectStorageEmulator( this.storage, "localhost", - FIREBASE_LOCAL_HOST_PORT_STORAGE + FIREBASE_LOCAL_HOST_PORT_STORAGE, ); connectFunctionsEmulator( this.functions, "localhost", - FIREBASE_LOCAL_HOST_PORT_FUNCTIONS + FIREBASE_LOCAL_HOST_PORT_FUNCTIONS, ); + // Connect Auth emulator if auth is initialized + if (this.auth) { + connectAuthEmulator( + this.auth, + `http://localhost:${FIREBASE_LOCAL_HOST_PORT_AUTH}`, + ); + } } // Returns the download URL of the given storage file. From c7d8f84e3bf0a1f2c7d93e461f9a4abd0518a88b Mon Sep 17 00:00:00 2001 From: Ellen Jiang Date: Wed, 4 Feb 2026 12:13:56 -0500 Subject: [PATCH 3/3] Add authentication user flow with login page and profile button, gated on internal flag --- frontend/src/app.ts | 27 ++++- frontend/src/components/auth/login_page.scss | 75 +++++++++++++ frontend/src/components/auth/login_page.ts | 76 +++++++++++++ .../src/components/auth/profile_button.scss | 17 +++ .../src/components/auth/profile_button.ts | 106 ++++++++++++++++++ frontend/src/components/header/header.scss | 4 + frontend/src/components/header/header.ts | 2 + .../src/components/lumi_reader/lumi_reader.ts | 43 ++++--- .../components/overflow_menu/overflow_menu.ts | 3 +- frontend/src/sass/_common.scss | 1 + 10 files changed, 330 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/auth/login_page.scss create mode 100644 frontend/src/components/auth/login_page.ts create mode 100644 frontend/src/components/auth/profile_button.scss create mode 100644 frontend/src/components/auth/profile_button.ts diff --git a/frontend/src/app.ts b/frontend/src/app.ts index 2ba0be3a..21d54368 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -24,6 +24,7 @@ import "./components/floating_panel_host/floating_panel_host"; import "./components/smart_highlight_menu/smart_highlight_menu"; import "./components/dialogs/dialogs"; import "./components/banner/banner"; +import "./components/auth/login_page"; import "lit-toast/lit-toast.js"; import { MobxLitElement } from "@adobe/lit-mobx"; @@ -33,6 +34,8 @@ import { createRef, ref, Ref } from "lit/directives/ref.js"; import { styleMap } from "lit/directives/style-map.js"; import { core } from "./core/core"; +import { AppConfigService } from "./services/app_config.service"; +import { AuthService } from "./services/auth.service"; import { Pages, RouterService } from "./services/router.service"; import { SettingsService } from "./services/settings.service"; import { SnackbarService } from "./services/snackbar.service"; @@ -43,16 +46,20 @@ import { styles } from "./app.scss"; import { LightMobxLitElement } from "./components/light_mobx_lit_element/light_mobx_lit_element"; import { GalleryView } from "./shared/types"; +import { FloatingPanelService } from "./services/floating_panel_service"; /** App main component. */ @customElement("lumi-app") export class App extends LightMobxLitElement { static override styles: CSSResultGroup = [styles]; + private readonly appConfigService = core.getService(AppConfigService); + private readonly authService = core.getService(AuthService); private readonly bannerService = core.getService(BannerService); private readonly routerService = core.getService(RouterService); private readonly settingsService = core.getService(SettingsService); private readonly snackbarService = core.getService(SnackbarService); + private readonly floatingPanelService = core.getService(FloatingPanelService); private readonly toastRef: Ref = createRef(); @@ -121,6 +128,21 @@ export class App extends LightMobxLitElement { } override render() { + // In internal mode, require authentication before showing app content + if ( + this.appConfigService.features.authentication && + !this.authService.isAuthenticated + ) { + return html` + +
+ +
+ `; + } + const mainStyles = styleMap({ height: this.bannerService.isBannerOpen ? `calc(100% - ${BANNER_HEIGHT}px)` @@ -131,7 +153,10 @@ export class App extends LightMobxLitElement { -
+
this.floatingPanelService.hide()} + > ${this.renderBanner()}
${this.renderPageContent()}
diff --git a/frontend/src/components/auth/login_page.scss b/frontend/src/components/auth/login_page.scss new file mode 100644 index 00000000..2bacad70 --- /dev/null +++ b/frontend/src/components/auth/login_page.scss @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@use "../../sass/common"; +@use "../../sass/typescale"; + +.login-page { + @include common.flex-row-align-center; + justify-content: center; + height: 100vh; + width: 100vw; + background: linear-gradient( + 135deg, + var(--md-sys-color-primary-container) 0%, + var(--md-sys-color-surface) 100% + ); +} + +.login-card { + @include common.flex-column; + align-items: center; + gap: common.$spacing-xxl; + padding: 36px; + + background: var(--md-sys-color-surface); + border: 3px solid var(--md-sys-color-primary-container-high); + border-radius: common.$border-radius-large; + + min-width: 320px; +} + +.header { + @include common.flex-column; + align-items: center; + gap: common.$spacing-medium; +} + +h1 { + @include typescale.headline-large; + margin: 0; + color: var(--md-sys-color-on-surface); +} + +.subtitle { + @include typescale.body-small; + margin: 0; + color: var(--md-sys-color-on-surface-variant); + font-style: italic; +} + +.loading { + @include typescale.body-small; + color: var(--md-sys-color-on-surface-variant); + font-style: italic; +} + +.error { + @include typescale.body-small; + color: var(--md-sys-color-error); + margin: 0; +} diff --git a/frontend/src/components/auth/login_page.ts b/frontend/src/components/auth/login_page.ts new file mode 100644 index 00000000..9381631c --- /dev/null +++ b/frontend/src/components/auth/login_page.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import "../../pair-components/button"; +import "../../pair-components/icon"; + +import { MobxLitElement } from "@adobe/lit-mobx"; +import { CSSResultGroup, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +import { core } from "../../core/core"; +import { AuthService } from "../../services/auth.service"; +import { APP_NAME } from "../../shared/constants"; + +import { styles } from "./login_page.scss"; + +/** + * Full-screen login page shown when user is not authenticated in internal mode. + */ +@customElement("login-page") +export class LoginPage extends MobxLitElement { + static override styles: CSSResultGroup = [styles]; + + private readonly authService = core.getService(AuthService); + + private handleLogin() { + this.authService.signInWithGoogle(); + } + + override render() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "login-page": LoginPage; + } +} diff --git a/frontend/src/components/auth/profile_button.scss b/frontend/src/components/auth/profile_button.scss new file mode 100644 index 00000000..036942b4 --- /dev/null +++ b/frontend/src/components/auth/profile_button.scss @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use "../../sass/common"; diff --git a/frontend/src/components/auth/profile_button.ts b/frontend/src/components/auth/profile_button.ts new file mode 100644 index 00000000..20e2201e --- /dev/null +++ b/frontend/src/components/auth/profile_button.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import "../../pair-components/icon_button"; +import "../../pair-components/tooltip"; + +import { MobxLitElement } from "@adobe/lit-mobx"; +import { CSSResultGroup, html, nothing } from "lit"; +import { customElement } from "lit/decorators.js"; + +import { core } from "../../core/core"; +import { AppConfigService } from "../../services/app_config.service"; +import { AuthService } from "../../services/auth.service"; +import { + FloatingPanelService, + OverflowMenuProps, +} from "../../services/floating_panel_service"; + +import { styles } from "./profile_button.scss"; + +/** + * A user account button that shows authentication state. + * When authenticated, shows an icon button that opens an overflow menu. + * Only renders in internal mode when authentication is enabled. + */ +@customElement("profile-button") +export class ProfileButton extends MobxLitElement { + static override styles: CSSResultGroup = [styles]; + + private readonly appConfigService = core.getService(AppConfigService); + private readonly authService = core.getService(AuthService); + private readonly floatingPanelService = core.getService(FloatingPanelService); + + private handleSignOut() { + this.floatingPanelService.hide(); + this.authService.signOut(); + } + + private handleAccountClick(e: Event) { + const target = e.currentTarget as HTMLElement; + const menuProps = new OverflowMenuProps([ + { + icon: "logout", + label: "Sign out", + onClick: () => this.handleSignOut(), + }, + ]); + this.floatingPanelService.show(menuProps, target); + } + + private getTooltipText(): string { + const name = this.authService.displayName || this.authService.email; + return name ? `Signed in as ${name}` : "Signed in"; + } + + override render() { + // Don't render anything if authentication is not enabled + if (!this.appConfigService.features.authentication) { + return nothing; + } + + // Show loading state + if (this.authService.isLoading) { + return nothing; + } + + // Show logged-in state with user icon button + if (this.authService.isAuthenticated) { + return html` + + + + `; + } + + // Not authenticated - don't render (login page handles this) + return nothing; + } +} + +declare global { + interface HTMLElementTagNameMap { + "profile-button": ProfileButton; + } +} diff --git a/frontend/src/components/header/header.scss b/frontend/src/components/header/header.scss index 31e0e409..e190f8dc 100644 --- a/frontend/src/components/header/header.scss +++ b/frontend/src/components/header/header.scss @@ -64,6 +64,10 @@ @include common.flex-row-align-center; gap: common.$spacing-small; } + + profile-button { + margin-left: common.$spacing-medium; + } } h1 { diff --git a/frontend/src/components/header/header.ts b/frontend/src/components/header/header.ts index c220c7c0..ae95d199 100644 --- a/frontend/src/components/header/header.ts +++ b/frontend/src/components/header/header.ts @@ -18,6 +18,7 @@ import "../../pair-components/button"; import "../../pair-components/icon_button"; import "../../pair-components/tooltip"; +import "../auth/profile_button"; import { MobxLitElement } from "@adobe/lit-mobx"; import { CSSResultGroup, html, nothing } from "lit"; import { customElement } from "lit/decorators.js"; @@ -83,6 +84,7 @@ export class Header extends MobxLitElement { return html` ${this.renderFeedbackButton()} ${this.renderSettingsButton()} ${this.renderImportButton()} + `; } diff --git a/frontend/src/components/lumi_reader/lumi_reader.ts b/frontend/src/components/lumi_reader/lumi_reader.ts index b498f18d..8d56efc4 100644 --- a/frontend/src/components/lumi_reader/lumi_reader.ts +++ b/frontend/src/components/lumi_reader/lumi_reader.ts @@ -209,7 +209,7 @@ export class LumiReader extends LightMobxLitElement { try { metadata = await getArxivMetadata( this.firebaseService.functions, - this.documentId + this.documentId, ); } catch (error) { console.error("Warning: Document metadata or version not found.", error); @@ -255,11 +255,11 @@ export class LumiReader extends LightMobxLitElement { if ( LOADING_STATUS_ERROR_STATES.includes( - data.loadingStatus as LoadingStatus + data.loadingStatus as LoadingStatus, ) ) { this.snackbarService.show( - `Error loading document: ${this.documentId}` + `Error loading document: ${this.documentId}`, ); } } else { @@ -269,7 +269,7 @@ export class LumiReader extends LightMobxLitElement { (error) => { this.snackbarService.show(`Error loading document: ${error.message}`); console.error(error); - } + }, ); } @@ -289,13 +289,13 @@ export class LumiReader extends LightMobxLitElement { .filter( (paper) => paper.metadata.paperId !== this.documentId && - paper.status === "complete" + paper.status === "complete", ); const summaryAnswer = await getPersonalSummaryCallable( this.firebaseService.functions, currentDoc, pastPapers, - this.settingsService.apiKey.value + this.settingsService.apiKey.value, ); this.historyService.addPersonalSummary(this.documentId, summaryAnswer); @@ -327,7 +327,7 @@ export class LumiReader extends LightMobxLitElement { private readonly handleDefine = async ( text: string, highlightedSpans: HighlightSelection[], - imageInfo?: ImageInfo + imageInfo?: ImageInfo, ) => { if (!this.documentStateService.lumiDocManager) return; @@ -348,7 +348,7 @@ export class LumiReader extends LightMobxLitElement { this.firebaseService.functions, this.documentStateService.lumiDocManager.lumiDoc, request, - this.settingsService.apiKey.value + this.settingsService.apiKey.value, ); this.historyService.addAnswer(this.documentId, response); } catch (e) { @@ -374,7 +374,7 @@ export class LumiReader extends LightMobxLitElement { highlightedText: string, query: string, highlightedSpans: HighlightSelection[], - imageInfo?: ImageInfo + imageInfo?: ImageInfo, ) => { const currentDoc = this.documentStateService.lumiDocManager?.lumiDoc; if (!currentDoc) return; @@ -396,7 +396,7 @@ export class LumiReader extends LightMobxLitElement { this.firebaseService.functions, currentDoc, request, - this.settingsService.apiKey.value + this.settingsService.apiKey.value, ); this.historyService.addAnswer(this.documentId, response); } catch (e) { @@ -433,14 +433,14 @@ export class LumiReader extends LightMobxLitElement { selectionInfo.selectedText, selectionInfo.highlightSelection, this.handleDefine.bind(this), - this.handleAsk.bind(this) + this.handleAsk.bind(this), ); if (isViewportSmall()) { if (this.mobileSmartHighlightContainerRef.value) { this.floatingPanelService.show( props, - this.mobileSmartHighlightContainerRef.value + this.mobileSmartHighlightContainerRef.value, ); } } else { @@ -450,7 +450,7 @@ export class LumiReader extends LightMobxLitElement { private readonly handleImageClick = ( info: ImageInfo, - target: HTMLElement + target: HTMLElement, ) => { this.analyticsService.trackAction(AnalyticsAction.READER_IMAGE_CLICK); @@ -459,17 +459,17 @@ export class LumiReader extends LightMobxLitElement { [], this.handleDefine.bind(this), this.handleAsk.bind(this), - info + info, ); this.floatingPanelService.show(props, target); this.documentStateService.highlightManager?.addImageHighlight( - info.imageStoragePath + info.imageStoragePath, ); }; private readonly handlePaperReferenceClick = ( reference: LumiReference, - target: HTMLElement + target: HTMLElement, ) => { const props = new ReferenceTooltipProps(reference); this.floatingPanelService.show(props, target); @@ -477,7 +477,7 @@ export class LumiReader extends LightMobxLitElement { private readonly handleFootnoteClick = ( footnote: LumiFootnote, - target: HTMLElement + target: HTMLElement, ) => { const props = new FootnoteTooltipProps(footnote); this.floatingPanelService.show(props, target); @@ -485,7 +485,7 @@ export class LumiReader extends LightMobxLitElement { private readonly handleAnswerHighlightClick = ( answer: LumiAnswer, - target: HTMLElement + target: HTMLElement, ) => { const props = new AnswerHighlightTooltipProps(answer); this.floatingPanelService.show(props, target); @@ -586,8 +586,7 @@ export class LumiReader extends LightMobxLitElement { `; } - private clearHighlightsAndMenus() { - this.floatingPanelService.hide(); + private clearHighlights() { this.documentStateService.highlightManager?.clearHighlights(); } @@ -602,7 +601,7 @@ export class LumiReader extends LightMobxLitElement { if (this.loadingStatus === LoadingStatus.WAITING) { return this.renderWithStyles( - this.renderImportingDocumentLoadingState(this.metadata) + this.renderImportingDocumentLoadingState(this.metadata), ); } @@ -629,7 +628,7 @@ export class LumiReader extends LightMobxLitElement {
{ - this.clearHighlightsAndMenus(); + this.clearHighlights(); }} > html` - ` + `, )} `; } diff --git a/frontend/src/sass/_common.scss b/frontend/src/sass/_common.scss index 31291bf9..d65fa8cc 100644 --- a/frontend/src/sass/_common.scss +++ b/frontend/src/sass/_common.scss @@ -387,6 +387,7 @@ $spacing-xxl: 24px; $border: 1px solid var(--md-sys-color-outline-variant); $border-radius-small: 8px; $border-radius-medium: 12px; +$border-radius-large: 16px; $header-height: 48px; $footer-height: 48px;