| /** |
| * @license |
| * Copyright 2025 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '@material/web/button/filled-tonal-button.js'; |
| import '@material/web/chips/chip-set.js'; |
| import '@material/web/icon/icon.js'; |
| import '@material/web/iconbutton/icon-button.js'; |
| import '@material/web/progress/circular-progress.js'; |
| import './gemini-message'; |
| import './splash-page-action'; |
| |
| import {css, html, LitElement} from 'lit'; |
| import {customElement, property, state} from 'lit/decorators.js'; |
| import {classMap} from 'lit/directives/class-map.js'; |
| import {when} from 'lit/directives/when.js'; |
| |
| import {Action} from '../../api/ai-code-review'; |
| import {chatModelToken, Turn} from '../../models/chat/chat-model'; |
| import {resolve} from '../../models/dependency'; |
| import {userModelToken} from '../../models/user/user-model'; |
| import {AccountDetailInfo, ServerInfo} from '../../types/common'; |
| import {subscribe} from '../lit/subscription-controller'; |
| import {getDisplayName} from '../../utils/display-name-util'; |
| |
| /** |
| * A component for displaying a splash page when there are no chat messages. |
| */ |
| @customElement('splash-page') |
| export class SplashPage extends LitElement { |
| @state() account?: AccountDetailInfo; |
| |
| @state() isBackgroundRequestExpanded = false; |
| |
| @state() turns: readonly Turn[] = []; |
| |
| @state() actions: readonly Action[] = []; |
| |
| @property({type: Boolean}) isChangePrivate = false; |
| |
| private readonly getChatModel = resolve(this, chatModelToken); |
| |
| private readonly getUserModel = resolve(this, userModelToken); |
| |
| constructor() { |
| super(); |
| subscribe( |
| this, |
| () => this.getChatModel().turns$, |
| x => (this.turns = x ?? []) |
| ); |
| subscribe( |
| this, |
| () => this.getChatModel().actions$, |
| x => |
| (this.actions = (x ?? []).filter( |
| action => !!action.enable_splash_page_card |
| )) |
| ); |
| subscribe( |
| this, |
| () => this.getUserModel().account$, |
| x => (this.account = x) |
| ); |
| } |
| |
| private get currentTurn(): Turn | undefined { |
| if (this.turns.length === 0) return undefined; |
| const turn = this.turns[this.turns.length - 1]; |
| return turn; |
| } |
| |
| get backgroundRequest(): Turn | undefined { |
| const turn = this.currentTurn; |
| return turn?.userMessage.isBackgroundRequest ? turn : undefined; |
| } |
| |
| static override styles = css` |
| :host { |
| overflow: auto; |
| padding-left: 20px; |
| padding-right: 20px; |
| padding-top: 20px; |
| } |
| .splash-container { |
| display: flex; |
| justify-content: center; |
| flex-flow: column nowrap; |
| } |
| .splash-greeting { |
| background: linear-gradient(135deg, #217bfe 0, #078efb 33%, #ac87eb 100%); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| -webkit-box-orient: vertical; |
| color: transparent; |
| display: -webkit-inline-box; |
| font-size: 24px; |
| font-weight: 400; |
| margin-block-end: var(--spacing-s); |
| } |
| .material-icon { |
| color: #5f6368; |
| } |
| .splash-question { |
| color: var(--chat-splash-page-question-color); |
| margin-bottom: 16px; |
| margin-top: 0px; |
| font-family: var(--header-font-family); |
| font-size: var(--font-size-h1); |
| font-weight: var(--font-weight-h1); |
| line-height: var(--line-height-h1); |
| } |
| .background-request-container { |
| background-color: var(--chat-splash-page-info-panel-bg-color); |
| padding: 15px; |
| border-radius: 15px; |
| margin-bottom: 12px; |
| display: flex; |
| flex-direction: column; |
| } |
| .background-request-container-inner { |
| position: relative; |
| max-height: 10em; |
| min-height: 10em; |
| overflow: hidden; |
| } |
| .background-request-container-inner.expanded { |
| max-height: none; |
| overflow: auto; |
| } |
| .background-request-container-overlay { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: linear-gradient( |
| to top, |
| var(--chat-splash-page-info-panel-bg-color), |
| transparent 50% |
| ); |
| } |
| .user-background-question { |
| font-family: var(--header-font-family); |
| font-size: var(--font-size-normal); |
| font-weight: var(--font-weight-normal); |
| line-height: var(--line-height-normal); |
| } |
| .expansion-button-container { |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| } |
| .info-panel-expansion-button { |
| top: 10px; |
| font-size: 1.5em; |
| border: none; |
| background-color: transparent; |
| color: var(--primary-default); |
| cursor: pointer; |
| } |
| .background-request-header { |
| display: flex; |
| align-items: center; |
| gap: 5px; |
| margin-bottom: 12px; |
| font-weight: bold; |
| } |
| .background-request-header .thinking-spinner { |
| margin-left: auto; |
| } |
| .action-container { |
| background-color: transparent; |
| margin-bottom: 20px; |
| display: flex; |
| justify-content: center; |
| flex-wrap: wrap; |
| width: 100%; |
| } |
| .action-container-title { |
| height: 28px; |
| display: flex; |
| align-items: center; |
| vertical-align: middle; |
| font-size: 0.9em; |
| font-weight: 500; |
| color: var(--chat-splash-page-action-set-title-color); |
| } |
| .action-container-title .autoreview-run-all-button { |
| margin-left: auto; |
| margin-right: 8px; |
| } |
| .small-icon { |
| /* TODO: find small-icon styles equivalent */ |
| } |
| `; |
| |
| override render() { |
| const config = {user: {anonymous_coward_name: ''}} as ServerInfo; |
| const displayName = getDisplayName(config, this.account); |
| return html` |
| <div class="splash-container"> |
| <h1 class="splash-greeting">Hello, ${displayName}</h1> |
| <p class="splash-question">How can I help you today?</p> |
| |
| ${this.renderContent()} |
| </div> |
| `; |
| } |
| |
| private renderContent() { |
| if (this.isChangePrivate) { |
| return html` |
| <div class="background-request-container"> |
| <div class="background-request-header"> |
| <md-icon class="material-icon">info</md-icon> |
| <span class="user-background-question" |
| >Review Agent is disabled on private changes.</span |
| > |
| </div> |
| </div> |
| `; |
| } |
| return html` |
| ${this.renderBackgroundRequest()} |
| |
| <div class="action-container-title suggested-actions-title"> |
| Capabilities |
| </div> |
| |
| ${this.renderActionChipSet()} |
| `; |
| } |
| |
| private renderBackgroundRequest() { |
| const request = this.backgroundRequest; |
| if (!request) return; |
| return html` |
| <div class="background-request-container"> |
| <div |
| class="background-request-container-inner ${classMap({ |
| expanded: this.isBackgroundRequestExpanded, |
| })}" |
| > |
| <div class="background-request-header"> |
| <md-icon class="material-icon">lightbulb_tips</md-icon> |
| <span class="user-background-question" |
| >${request.userMessage.content}</span |
| > |
| ${when( |
| !request.geminiMessage.responseComplete, |
| () => html`<md-circular-progress |
| class="thinking-spinner" |
| indeterminate |
| size="17" |
| ></md-circular-progress>` |
| )} |
| </div> |
| <gemini-message |
| .isBackgroundRequest=${true} |
| .isLatest=${true} |
| .turnIndex=${0} |
| ></gemini-message> |
| ${when( |
| !this.isBackgroundRequestExpanded, |
| () => html`<div class="background-request-container-overlay"></div>` |
| )} |
| </div> |
| <div class="expansion-button-container"> |
| <button |
| class="info-panel-expansion-button" |
| title=${this.isBackgroundRequestExpanded ? 'Hide' : 'Show more'} |
| aria-label=${this.isBackgroundRequestExpanded |
| ? 'Hide' |
| : 'Show more'} |
| @click=${this.toggleBackgroundRequestExpansion} |
| > |
| ${this.isBackgroundRequestExpanded ? '–' : '...'} |
| </button> |
| </div> |
| </div> |
| `; |
| } |
| |
| private renderActionChipSet() { |
| return html` |
| <md-chip-set class="action-container"> |
| ${this.actions.map( |
| (action, index, array) => html` |
| <splash-page-action |
| .action=${action} |
| .isFirst=${index === 0} |
| .isLast=${index === array.length - 1} |
| ></splash-page-action> |
| ` |
| )} |
| </md-chip-set> |
| `; |
| } |
| |
| protected toggleBackgroundRequestExpansion() { |
| this.isBackgroundRequestExpanded = !this.isBackgroundRequestExpanded; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'splash-page': SplashPage; |
| } |
| } |