blob: 019888a014b5790ce51fad4a947032774e3a6673 [file] [log] [blame]
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '@material/web/iconbutton/icon-button.js';
import '@material/web/progress/circular-progress.js';
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 '../shared/gr-icon/gr-icon';
import '../shared/gr-button/gr-button';
import '../shared/gr-tooltip-content/gr-tooltip-content';
import {Action, ContextItemType} from '../../api/ai-code-review';
import {chatModelToken} from '../../models/chat/chat-model';
import {parseLink} from '../../models/chat/context-item-util';
import {resolve} from '../../models/dependency';
import {isDefined} from '../../types/types';
import {fireAlert} from '../../utils/event-util';
import {subscribe} from '../lit/subscription-controller';
/**
* A component that renders a single action as a clickable chip on the chat
* splash page. Clicking the chip initiates a chat or action based on the
* provided `Action` object.
*/
@customElement('splash-page-action')
export class SplashPageAction extends LitElement {
@property({type: Object}) action?: Action;
@property({type: Boolean}) isFirst = false;
@property({type: Boolean}) isLast = false;
@state() contextItemTypes: readonly ContextItemType[] = [];
private readonly getChatModel = resolve(this, chatModelToken);
constructor() {
super();
subscribe(
this,
() => this.getChatModel().contextItemTypes$,
types => (this.contextItemTypes = types)
);
}
static override styles = css`
:host {
display: flex;
justify-content: center;
flex-wrap: wrap;
width: 100%;
position: relative;
}
.action-chip {
display: flex;
background-color: var(--background-color-tertiary);
color: var(--primary-default);
height: 60px;
align-items: center;
border-radius: 4px;
margin: 0;
width: 100%;
overflow: hidden;
cursor: pointer;
--md-assist-chip-outline-width: 0;
}
.action-chip[disabled] {
opacity: 0.6;
cursor: default;
}
.action-chip.first-action-chip {
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.action-chip.last-action-chip {
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
.action-chip.custom-action-chip {
background-color: var(--custom-action-chip-bg-color);
}
.action-icon {
padding: 4px;
border-radius: 8px;
background-color: var(--background-color-primary);
flex-shrink: 0;
}
.action-text-container {
display: flex;
flex-direction: column;
height: 100%;
overflow-x: hidden;
}
.main-action-text-container {
margin-left: 20px;
font-weight: 400;
display: flex;
align-items: center;
}
.main-action-text-container.has-subtext {
margin-top: 12px;
margin-bottom: -2px;
}
.action-text {
font-family: var(--font-family);
font-size: var(--font-size-normal);
font-weight: var(--font-weight-normal);
line-height: var(--line-height-normal);
color: var(--primary-text-color);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-subtext {
vertical-align: super;
margin-left: 5px;
padding: 0px 15px 8px;
font-size: 0.8em;
color: var(--chat-splash-page-question-color);
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-subtext.is-passed {
color: var(--file-reviewed-color);
}
.action-subtext.is-actionable {
color: var(--tonal-red);
}
.info-button {
margin-left: 10px;
margin-right: 10px;
font-size: 24px;
--button-background-color: transparent;
}
.info-button gr-icon {
color: inherit;
}
.chip-content {
display: flex;
align-items: center;
}
`;
override render() {
if (!this.action) return;
const chipClasses = {
'action-chip': true,
'first-action-chip': this.isFirst,
'last-action-chip': this.isLast,
};
return html`
<md-assist-chip
class=${classMap(chipClasses)}
title=${this.action.hover_text ?? ''}
@click=${this.handleAction}
>
<div class="chip-content">
<gr-icon
class="action-icon"
icon=${this.action.icon ?? 'warning'}
></gr-icon>
<div class="action-text-container">
<div
class=${classMap({
'main-action-text-container': true,
'has-subtext': !!this.action.subtext,
})}
>
<span class="action-text">${this.action.display_text}</span>
<gr-tooltip-content has-tooltip title="Capability details">
<gr-button
flatten
class="info-button"
@click=${this.displayDetailsCard}
>
<gr-icon icon="info"></gr-icon>
</gr-button>
</gr-tooltip-content>
</div>
${when(
this.action.subtext,
() => html` <span
class=${classMap({
'action-subtext': true,
})}
>${this.action?.subtext}</span
>`
)}
</div>
</div>
</md-assist-chip>
`;
}
private handleAction() {
const action = this.action;
if (!action) return;
const contextItems = (action.context_item_links ?? []).map(link =>
parseLink(link, this.contextItemTypes)
);
if (contextItems.some(item => !item)) {
fireAlert(this, 'Failed to parse one or more context item links.');
}
if (action.enable_send_without_input) {
this.getChatModel().startNewChatWithPredefinedPrompt(
action.id,
contextItems.filter(isDefined)
);
} else {
this.getChatModel().startNewChatWithUserInput(
action.initial_user_prompt ?? '',
action.id,
contextItems.filter(isDefined)
);
}
}
private displayDetailsCard(event: MouseEvent) {
event.stopPropagation();
// TODO: Implement this.
}
}
declare global {
interface HTMLElementTagNameMap {
'splash-page-action': SplashPageAction;
}
}