| import {css, html, LitElement} from 'lit'; |
| import {repeat} from 'lit/directives/repeat.js'; |
| import {customElement, property} from 'lit/decorators.js'; |
| import {State, TimelineMessage} from '../types'; |
| import './sketch-timeline-message' |
| |
| @customElement('sketch-timeline') |
| export class SketchTimeline extends LitElement { |
| @property() |
| messages: TimelineMessage[] = []; |
| |
| // See https://lit.dev/docs/components/styles/ for how lit-element handles CSS. |
| // Note that these styles only apply to the scope of this web component's |
| // shadow DOM node, so they won't leak out or collide with CSS declared in |
| // other components or the containing web page (...unless you want it to do that). |
| static styles = css` |
| /* Hide views initially to prevent flash of content */ |
| .timeline-container .timeline, |
| .timeline-container .diff-view, |
| .timeline-container .chart-view, |
| .timeline-container .terminal-view { |
| visibility: hidden; |
| } |
| |
| /* Will be set by JavaScript once we know which view to display */ |
| .timeline-container.view-initialized .timeline, |
| .timeline-container.view-initialized .diff-view, |
| .timeline-container.view-initialized .chart-view, |
| .timeline-container.view-initialized .terminal-view { |
| visibility: visible; |
| } |
| |
| .timeline-container { |
| width: 100%; |
| position: relative; |
| } |
| |
| /* Timeline styles that should remain unchanged */ |
| .timeline { |
| position: relative; |
| margin: 10px 0; |
| scroll-behavior: smooth; |
| } |
| |
| .timeline::before { |
| content: ""; |
| position: absolute; |
| top: 0; |
| bottom: 0; |
| left: 15px; |
| width: 2px; |
| background: #e0e0e0; |
| border-radius: 1px; |
| } |
| |
| /* Hide the timeline vertical line when there are no messages */ |
| .timeline.empty::before { |
| display: none; |
| } |
| `; |
| |
| constructor() { |
| super(); |
| |
| // Binding methods |
| this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this); |
| } |
| |
| /** |
| * Handle showCommitDiff event |
| */ |
| private _handleShowCommitDiff(event: CustomEvent) { |
| const { commitHash } = event.detail; |
| if (commitHash) { |
| // Bubble up the event to the app shell |
| const newEvent = new CustomEvent('show-commit-diff', { |
| detail: { commitHash }, |
| bubbles: true, |
| composed: true |
| }); |
| this.dispatchEvent(newEvent); |
| } |
| } |
| |
| // See https://lit.dev/docs/components/lifecycle/ |
| connectedCallback() { |
| super.connectedCallback(); |
| |
| // Listen for showCommitDiff events from the renderer |
| document.addEventListener('showCommitDiff', this._handleShowCommitDiff as EventListener); |
| } |
| |
| // See https://lit.dev/docs/components/lifecycle/ |
| disconnectedCallback() { |
| super.disconnectedCallback(); |
| |
| // Remove event listeners |
| document.removeEventListener('showCommitDiff', this._handleShowCommitDiff as EventListener); |
| } |
| |
| messageKey(message: TimelineMessage): string { |
| // If the message has tool calls, and any of the tool_calls get a response, we need to |
| // re-render that message. |
| const toolCallResponses = message.tool_calls?.filter((tc)=>tc.result_message).map((tc)=>tc.tool_call_id).join('-'); |
| return `message-${message.idx}-${toolCallResponses}`; |
| } |
| |
| render() { |
| return html` |
| <div class="timeline-container"> |
| ${repeat(this.messages, this.messageKey, (message, index) => { |
| let previousMessage: TimelineMessage; |
| if (index > 0) { |
| previousMessage = this.messages[index-1]; |
| } |
| return html`<sketch-timeline-message .message=${message} .previousMessage=${previousMessage}></sketch-timeline-message>`; |
| })} |
| </div> |
| `; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| "sketch-timeline": SketchTimeline; |
| } |
| } |