blob: 6f071e87cb888045a0e64708d5847e94a7eafb32 [file] [log] [blame]
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '@material/web/iconbutton/icon-button.js';
import '@material/web/button/text-button.js';
import '@material/web/icon/icon.js';
import '@material/web/menu/menu.js';
import '@material/web/menu/menu-item.js';
import '../shared/gr-icon/gr-icon';
import {MdMenu} from '@material/web/menu/menu';
import {css, html, LitElement} from 'lit';
import {customElement, query, state} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';
import {fire} from '../../utils/event-util';
import {subscribe} from '../lit/subscription-controller';
import {ModelInfo} from '../../api/ai-code-review';
import {chatModelToken, ChatPanelMode} from '../../models/chat/chat-model';
import {resolve} from '../../models/dependency';
@customElement('chat-header')
export class ChatHeader extends LitElement {
static override styles = css`
:host {
display: flex;
padding: 0 var(--spacing-xxl) 0 var(--spacing-xl);
align-items: center;
color: var(--primary-text-color);
}
.title {
color: var(--primary-text-color);
font-family: var(--header-font-family);
font-size: var(--font-size-h2);
font-weight: var(--font-weight-h2);
line-height: var(--line-height-h2);
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
md-text-button.select-model-trigger {
height: auto;
min-width: 50px;
}
.title-group {
display: flex;
flex-direction: column;
align-items: start;
}
.subtitle {
font-size: 12px;
font-weight: 500;
color: var(--deemphasized-text-color);
display: flex;
flex-direction: row;
align-items: center;
max-width: 100%;
}
.subtitle-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-drop-down {
height: 16px;
width: 16px;
font-size: 18px;
margin-top: -2px;
}
:host > md-icon-button,
:host > gr-icon {
flex-shrink: 0;
}
md-icon-button {
height: 40px;
width: 40px;
font-size: 20px;
font-weight: 500;
}
md-icon-button.back-arrow {
height: 32px;
width: 32px;
padding-right: 0px;
}
md-icon {
vertical-align: middle;
}
md-icon-button:disabled md-icon {
color: var(--deemphasized-text-color);
}
.gemini-icon {
color: var(--deemphasized-text-color);
font-size: 24px;
margin-right: 3px;
}
.first-right-button {
margin-left: auto;
}
.more-actions-menu md-menu-item md-icon {
color: var(--deemphasized-text-color);
}
.select-model-menu {
max-width: 500px;
}
md-text-button.select-model-trigger > span {
min-width: 0;
}
md-icon-button {
color: var(--primary-text-color);
--md-icon-button-icon-color: var(--primary-text-color);
--md-icon-button-hover-icon-color: var(--primary-text-color);
}
`;
@state() availableModels: ModelInfo[] = [];
@state() selectedModel?: ModelInfo;
@state() documentationUrl?: string;
@state() mode: ChatPanelMode = ChatPanelMode.CONVERSATION;
@query('#selectModelMenu') private selectModelMenu?: MdMenu;
@query('#moreActionsMenu') private moreActionsMenu?: MdMenu;
private readonly getChatModel = resolve(this, chatModelToken);
constructor() {
super();
subscribe(
this,
() => this.getChatModel().availableModelsMap$,
x => (this.availableModels = [...x.values()])
);
subscribe(
this,
() => this.getChatModel().selectedModel$,
x => (this.selectedModel = x)
);
subscribe(
this,
() => this.getChatModel().models$,
x => (this.documentationUrl = x?.documentation_url)
);
subscribe(
this,
() => this.getChatModel().mode$,
x => (this.mode = x ?? ChatPanelMode.CONVERSATION)
);
}
override render() {
return html`
${this.renderLeftSectionChat()} ${this.renderLeftSectionHistory()}
${this.renderRightButtons()} ${this.renderMenus()}
`;
}
private renderLeftSectionHistory() {
if (this.mode !== ChatPanelMode.HISTORY) return;
return html`
<md-icon-button
class="back-arrow"
aria-label="Back to chat"
title="Back to chat"
@click=${this.backToChat}
>
<md-icon>arrow_back_ios</md-icon>
</md-icon-button>
<span class="title">History</span>
`;
}
private renderLeftSectionChat() {
if (this.mode !== ChatPanelMode.CONVERSATION) return;
return html`
<gr-icon class="gemini-icon" icon="robot_2"></gr-icon>
<md-text-button
id="selectModelTrigger"
class="select-model-trigger"
@click=${() =>
this.selectModelMenu && (this.selectModelMenu.open = true)}
?disabled=${!this.selectedModel}
>
<div class="title-group">
<span class="title">Review Agent</span>
${this.selectedModel
? html`
<div class="subtitle">
<span class="subtitle-text"
>${this.selectedModel?.short_text}</span
>
<md-icon class="arrow-drop-down">arrow_drop_down</md-icon>
</div>
`
: ''}
</div>
</md-text-button>
`;
}
private renderRightButtons() {
return html`
<md-icon-button
class="history-button first-right-button"
aria-label="Show history"
title="Show history"
@click=${this.showHistory}
>
<md-icon>history</md-icon>
</md-icon-button>
<md-icon-button
id="moreActionsTrigger"
class="more-actions-trigger"
aria-label="More actions"
title="More"
@click=${() =>
this.moreActionsMenu && (this.moreActionsMenu.open = true)}
>
<md-icon>more_vert</md-icon>
</md-icon-button>
<md-icon-button
class="clear-history-button"
@click=${this.startNewConversation}
title="Start a new conversation"
aria-label="Start a new conversation"
>
<md-icon>add</md-icon>
</md-icon-button>
<md-icon-button
class="close-button"
@click=${this.closePanel}
title="Close Review Agent panel"
aria-label="Close Review Agent panel"
>
<md-icon>clear</md-icon>
</md-icon-button>
`;
}
private renderMenus() {
return html`
<md-menu
id="selectModelMenu"
anchor="selectModelTrigger"
class="select-model-menu"
>
${this.availableModels.map(
option => html`
<md-menu-item @click=${() => this.onSwitchModel(option)}>
<md-icon
slot="start"
style=${styleMap({
visibility:
this.selectedModel?.model_id === option.model_id
? 'visible'
: 'hidden',
})}
>done</md-icon
>
${option.full_display_text}
</md-menu-item>
`
)}
</md-menu>
${this.renderDocumentationMenu()}
`;
}
private renderDocumentationMenu() {
if (!this.documentationUrl) return;
return html`
<md-menu
id="moreActionsMenu"
anchor="moreActionsTrigger"
class="more-actions-menu"
menu-corner="start-end"
anchor-corner="end-end"
>
<a
href=${this.documentationUrl}
target="_blank"
rel="noopener noreferrer"
style="text-decoration: none;"
>
<md-menu-item>
<md-icon slot="start">help_outline</md-icon>
Documentation
</md-menu-item>
</a>
</md-menu>
`;
}
private onSwitchModel(model: ModelInfo) {
this.getChatModel().selectModel(model.model_id);
}
private closePanel() {
fire(this, 'close-chat-panel', {});
}
private startNewConversation() {
this.getChatModel().setMode(ChatPanelMode.CONVERSATION);
this.getChatModel().startEmptyNewChat(true);
}
private showHistory() {
this.getChatModel().setMode(ChatPanelMode.HISTORY);
}
private backToChat() {
this.getChatModel().setMode(ChatPanelMode.CONVERSATION);
}
}
declare global {
interface HTMLElementTagNameMap {
'chat-header': ChatHeader;
}
interface HTMLElementEventMap {
'close-chat-panel': CustomEvent;
}
}