blob: 1edaff2fa0cdfca0603c171233c9b5d72329abd6 [file] [log] [blame]
import { css, html, LitElement, render } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { customElement, property, state } from "lit/decorators.js";
import { AgentMessage } from "../types";
import { marked, MarkedOptions, Renderer, Tokens } from "marked";
import mermaid from "mermaid";
import "./sketch-tool-calls";
@customElement("sketch-timeline-message")
export class SketchTimelineMessage extends LitElement {
@property()
message: AgentMessage;
@property()
previousMessage: AgentMessage;
@property()
open: boolean = false;
@state()
showInfo: boolean = false;
// 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`
.message {
position: relative;
margin-bottom: 6px;
display: flex;
flex-direction: column;
width: 100%;
}
.message-container {
display: flex;
position: relative;
width: 100%;
}
.message-metadata-left {
flex: 0 0 80px;
padding: 3px 5px;
text-align: right;
font-size: 11px;
color: #777;
align-self: flex-start;
}
.message-metadata-right {
flex: 0 0 80px;
padding: 3px 5px;
text-align: left;
font-size: 11px;
color: #777;
align-self: flex-start;
}
.message-bubble-container {
flex: 1;
display: flex;
max-width: calc(100% - 160px);
overflow: hidden;
text-overflow: ellipsis;
}
.user .message-bubble-container {
justify-content: flex-end;
}
.agent .message-bubble-container,
.tool .message-bubble-container,
.error .message-bubble-container {
justify-content: flex-start;
}
.message-content {
position: relative;
padding: 6px 10px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
max-width: 100%;
width: fit-content;
min-width: min-content;
overflow-wrap: break-word;
word-break: break-word;
}
/* User message styling */
.user .message-content {
background-color: #2196f3;
color: white;
border-bottom-right-radius: 5px;
}
/* Agent message styling */
.agent .message-content,
.tool .message-content,
.error .message-content {
background-color: #f1f1f1;
color: black;
border-bottom-left-radius: 5px;
}
/* Copy button styles */
.message-text-container,
.tool-result-container {
position: relative;
}
.message-actions {
position: absolute;
top: 5px;
right: 5px;
z-index: 10;
opacity: 0;
transition: opacity 0.2s ease;
}
.message-text-container:hover .message-actions,
.tool-result-container:hover .message-actions {
opacity: 1;
}
.message-actions {
display: flex;
gap: 6px;
}
.copy-icon,
.info-icon {
background-color: transparent;
border: none;
color: rgba(0, 0, 0, 0.6);
cursor: pointer;
padding: 3px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
transition: all 0.15s ease;
}
.user .copy-icon,
.user .info-icon {
color: rgba(255, 255, 255, 0.8);
}
.copy-icon:hover,
.info-icon:hover {
background-color: rgba(0, 0, 0, 0.08);
}
.user .copy-icon:hover,
.user .info-icon:hover {
background-color: rgba(255, 255, 255, 0.15);
}
/* Message metadata styling */
.message-type {
font-weight: bold;
font-size: 11px;
}
.message-timestamp {
display: block;
font-size: 10px;
color: #888;
margin-top: 2px;
}
.message-duration {
display: block;
font-size: 10px;
color: #888;
margin-top: 2px;
}
.message-usage {
display: block;
font-size: 10px;
color: #888;
margin-top: 3px;
}
.conversation-id {
font-family: monospace;
font-size: 12px;
padding: 2px 4px;
margin-left: auto;
}
.parent-info {
font-size: 11px;
opacity: 0.8;
}
.subconversation {
border-left: 2px solid transparent;
padding-left: 5px;
margin-left: 20px;
transition: margin-left 0.3s ease;
}
.message-text {
overflow-x: auto;
margin-bottom: 0;
font-family: sans-serif;
padding: 2px 0;
user-select: text;
cursor: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
font-size: 14px;
line-height: 1.35;
text-align: left;
}
/* Style for code blocks within messages */
.message-text pre,
.message-text code {
font-family: monospace;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
padding: 2px 4px;
overflow-x: auto;
max-width: 100%;
white-space: pre-wrap; /* Allow wrapping for very long lines */
word-break: break-all; /* Break words at any character */
box-sizing: border-box; /* Include padding in width calculation */
}
.user .message-text pre,
.user .message-text code {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.tool-details {
margin-top: 3px;
padding-top: 3px;
border-top: 1px dashed #e0e0e0;
font-size: 12px;
}
.tool-name {
font-size: 12px;
font-weight: bold;
margin-bottom: 2px;
background: #f0f0f0;
padding: 2px 4px;
border-radius: 2px;
display: flex;
align-items: center;
gap: 3px;
}
.tool-input,
.tool-result {
margin-top: 2px;
padding: 3px 5px;
background: #f7f7f7;
border-radius: 2px;
font-family: monospace;
font-size: 12px;
overflow-x: auto;
white-space: pre;
line-height: 1.3;
user-select: text;
cursor: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
}
.tool-result {
max-height: 300px;
overflow-y: auto;
}
.usage-info {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed #e0e0e0;
font-size: 12px;
color: #666;
}
/* Custom styles for IRC-like experience */
.user .message-content {
border-left-color: #2196f3;
}
.agent .message-content {
border-left-color: #4caf50;
}
.tool .message-content {
border-left-color: #ff9800;
}
.error .message-content {
border-left-color: #f44336;
}
/* Make message type display bold but without the IRC-style markers */
.message-type {
font-weight: bold;
}
/* Commit message styling */
.commits-container {
margin-top: 10px;
}
.commit-notification {
background-color: #e8f5e9;
color: #2e7d32;
font-weight: 500;
font-size: 12px;
padding: 6px 10px;
border-radius: 10px;
margin-bottom: 8px;
text-align: center;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.commit-card {
background-color: #f5f5f5;
border-radius: 8px;
overflow: hidden;
margin-bottom: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
padding: 6px 8px;
display: flex;
align-items: center;
gap: 8px;
}
.commit-hash {
color: #0366d6;
font-weight: bold;
font-family: monospace;
cursor: pointer;
text-decoration: none;
background-color: rgba(3, 102, 214, 0.08);
padding: 2px 5px;
border-radius: 4px;
}
.commit-hash:hover {
background-color: rgba(3, 102, 214, 0.15);
}
.commit-branch {
color: #28a745;
font-weight: 500;
cursor: pointer;
font-family: monospace;
background-color: rgba(40, 167, 69, 0.08);
padding: 2px 5px;
border-radius: 4px;
}
.commit-branch:hover {
background-color: rgba(40, 167, 69, 0.15);
}
.commit-subject {
font-size: 13px;
color: #333;
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.commit-diff-button {
padding: 3px 8px;
border: none;
border-radius: 4px;
background-color: #0366d6;
color: white;
font-size: 11px;
cursor: pointer;
transition: all 0.2s ease;
display: block;
margin-left: auto;
}
.commit-diff-button:hover {
background-color: #0256b4;
}
/* Tool call cards */
.tool-call-cards-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
/* Error message specific styling */
.error .message-content {
background-color: #ffebee;
border-left: 3px solid #f44336;
}
.end-of-turn {
margin-bottom: 15px;
}
.end-of-turn-indicator {
display: block;
font-size: 11px;
color: #777;
padding: 2px 0;
margin-top: 8px;
text-align: right;
font-style: italic;
}
.user .end-of-turn-indicator {
color: rgba(255, 255, 255, 0.7);
}
/* Message info panel styling */
.message-info-panel {
margin-top: 8px;
padding: 8px;
background-color: rgba(0, 0, 0, 0.03);
border-radius: 6px;
font-size: 12px;
transition: all 0.2s ease;
border-left: 2px solid rgba(0, 0, 0, 0.1);
}
.user .message-info-panel {
background-color: rgba(255, 255, 255, 0.15);
border-left: 2px solid rgba(255, 255, 255, 0.2);
}
.info-row {
margin-bottom: 3px;
display: flex;
}
.info-label {
font-weight: bold;
margin-right: 5px;
min-width: 60px;
}
.info-value {
flex: 1;
}
.conversation-id {
font-family: monospace;
font-size: 10px;
word-break: break-all;
}
.markdown-content {
box-sizing: border-box;
min-width: 200px;
margin: 0 auto;
}
.markdown-content p {
margin-block-start: 0.3em;
margin-block-end: 0.3em;
}
.markdown-content p:first-child {
margin-block-start: 0;
}
.markdown-content p:last-child {
margin-block-end: 0;
}
/* Styling for markdown elements */
.markdown-content a {
color: inherit;
text-decoration: underline;
}
.user .markdown-content a {
color: #fff;
text-decoration: underline;
}
.markdown-content ul,
.markdown-content ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
.markdown-content blockquote {
border-left: 3px solid rgba(0, 0, 0, 0.2);
padding-left: 1em;
margin-left: 0.5em;
font-style: italic;
}
.user .markdown-content blockquote {
border-left: 3px solid rgba(255, 255, 255, 0.4);
}
/* Mermaid diagram styling */
.mermaid-container {
margin: 1em 0;
padding: 0.5em;
background-color: #f8f8f8;
border-radius: 4px;
overflow-x: auto;
}
.mermaid {
text-align: center;
}
`;
// Track mermaid diagrams that need rendering
private mermaidDiagrams = new Map();
constructor() {
super();
// Initialize mermaid with specific config
mermaid.initialize({
startOnLoad: false,
suppressErrorRendering: true,
theme: "default",
securityLevel: "loose", // Allows more flexibility but be careful with user-generated content
fontFamily: "monospace",
});
}
// See https://lit.dev/docs/components/lifecycle/
connectedCallback() {
super.connectedCallback();
}
// After the component is updated and rendered, render any mermaid diagrams
updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);
this.renderMermaidDiagrams();
}
// Render mermaid diagrams after the component is updated
renderMermaidDiagrams() {
// Add a small delay to ensure the DOM is fully rendered
setTimeout(() => {
// Find all mermaid containers in our shadow root
const containers = this.shadowRoot?.querySelectorAll(".mermaid");
if (!containers || containers.length === 0) return;
// Process each mermaid diagram
containers.forEach((container) => {
const id = container.id;
const code = container.textContent || "";
if (!code || !id) return; // Use return for forEach instead of continue
try {
// Clear any previous content
container.innerHTML = code;
// Render the mermaid diagram using promise
mermaid
.render(`${id}-svg`, code)
.then(({ svg }) => {
container.innerHTML = svg;
})
.catch((err) => {
console.error("Error rendering mermaid diagram:", err);
// Show the original code as fallback
container.innerHTML = `<pre>${code}</pre>`;
});
} catch (err) {
console.error("Error processing mermaid diagram:", err);
// Show the original code as fallback
container.innerHTML = `<pre>${code}</pre>`;
}
});
}, 100); // Small delay to ensure DOM is ready
}
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
}
renderMarkdown(markdownContent: string): string {
try {
// Create a custom renderer
const renderer = new Renderer();
const originalCodeRenderer = renderer.code.bind(renderer);
// Override the code renderer to handle mermaid diagrams
renderer.code = function ({ text, lang, escaped }: Tokens.Code): string {
if (lang === "mermaid") {
// Generate a unique ID for this diagram
const id = `mermaid-diagram-${Math.random().toString(36).substring(2, 10)}`;
// Just create the container and mermaid div - we'll render it in the updated() lifecycle method
return `<div class="mermaid-container">
<div class="mermaid" id="${id}">${text}</div>
</div>`;
}
// Default rendering for other code blocks
return originalCodeRenderer({ text, lang, escaped });
};
// Set markdown options for proper code block highlighting and safety
const markedOptions: MarkedOptions = {
gfm: true, // GitHub Flavored Markdown
breaks: true, // Convert newlines to <br>
async: false,
renderer: renderer,
// DOMPurify is recommended for production, but not included in this implementation
};
return marked.parse(markdownContent, markedOptions) as string;
} catch (error) {
console.error("Error rendering markdown:", error);
// Fallback to plain text if markdown parsing fails
return markdownContent;
}
}
/**
* Format timestamp for display
*/
formatTimestamp(
timestamp: string | number | Date | null | undefined,
defaultValue: string = "",
): string {
if (!timestamp) return defaultValue;
try {
const date = new Date(timestamp);
if (isNaN(date.getTime())) return defaultValue;
// Format: Mar 13, 2025 09:53:25 AM
return date.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
} catch (e) {
return defaultValue;
}
}
formatNumber(
num: number | null | undefined,
defaultValue: string = "0",
): string {
if (num === undefined || num === null) return defaultValue;
try {
return num.toLocaleString();
} catch (e) {
return String(num);
}
}
formatCurrency(
num: number | string | null | undefined,
defaultValue: string = "$0.00",
isMessageLevel: boolean = false,
): string {
if (num === undefined || num === null) return defaultValue;
try {
// Use 4 decimal places for message-level costs, 2 for totals
const decimalPlaces = isMessageLevel ? 4 : 2;
return `$${parseFloat(String(num)).toFixed(decimalPlaces)}`;
} catch (e) {
return defaultValue;
}
}
// Format duration from nanoseconds to a human-readable string
_formatDuration(nanoseconds: number | null | undefined): string {
if (!nanoseconds) return "0s";
const seconds = nanoseconds / 1e9;
if (seconds < 60) {
return `${seconds.toFixed(1)}s`;
} else if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}min ${remainingSeconds.toFixed(0)}s`;
} else {
const hours = Math.floor(seconds / 3600);
const remainingSeconds = seconds % 3600;
const minutes = Math.floor(remainingSeconds / 60);
return `${hours}h ${minutes}min`;
}
}
showCommit(commitHash: string) {
this.dispatchEvent(
new CustomEvent("show-commit-diff", {
bubbles: true,
composed: true,
detail: { commitHash },
}),
);
}
_toggleInfo(e: Event) {
e.stopPropagation();
this.showInfo = !this.showInfo;
}
copyToClipboard(text: string, event: Event) {
const element = event.currentTarget as HTMLElement;
const rect = element.getBoundingClientRect();
navigator.clipboard
.writeText(text)
.then(() => {
this.showFloatingMessage("Copied!", rect, "success");
})
.catch((err) => {
console.error("Failed to copy text: ", err);
this.showFloatingMessage("Failed to copy!", rect, "error");
});
}
showFloatingMessage(
message: string,
targetRect: DOMRect,
type: "success" | "error",
) {
// Create floating message element
const floatingMsg = document.createElement("div");
floatingMsg.textContent = message;
floatingMsg.className = `floating-message ${type}`;
// Position it near the clicked element
// Position just above the element
const top = targetRect.top - 30;
const left = targetRect.left + targetRect.width / 2 - 40;
floatingMsg.style.position = "fixed";
floatingMsg.style.top = `${top}px`;
floatingMsg.style.left = `${left}px`;
floatingMsg.style.zIndex = "9999";
// Add to document body
document.body.appendChild(floatingMsg);
// Animate in
floatingMsg.style.opacity = "0";
floatingMsg.style.transform = "translateY(10px)";
setTimeout(() => {
floatingMsg.style.opacity = "1";
floatingMsg.style.transform = "translateY(0)";
}, 10);
// Remove after animation
setTimeout(() => {
floatingMsg.style.opacity = "0";
floatingMsg.style.transform = "translateY(-10px)";
setTimeout(() => {
document.body.removeChild(floatingMsg);
}, 300);
}, 1500);
}
render() {
// Calculate if this is an end of turn message with no parent conversation ID
const isEndOfTurn =
this.message?.end_of_turn && !this.message?.parent_conversation_id;
return html`
<div
class="message ${this.message?.type} ${isEndOfTurn
? "end-of-turn"
: ""}"
>
<div class="message-container">
<!-- Left area (empty for simplicity) -->
<div class="message-metadata-left"></div>
<!-- Message bubble -->
<div class="message-bubble-container">
<div class="message-content">
<div class="message-text-container">
<div class="message-actions">
${copyButton(this.message?.content)}
<button
class="info-icon"
title="Show message details"
@click=${this._toggleInfo}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
</button>
</div>
${this.message?.content
? html`
<div class="message-text markdown-content">
${unsafeHTML(
this.renderMarkdown(this.message?.content),
)}
</div>
`
: ""}
<!-- End of turn indicator inside the bubble -->
${isEndOfTurn && this.message?.elapsed
? html`
<div class="end-of-turn-indicator">
end of turn
(${this._formatDuration(this.message?.elapsed)})
</div>
`
: ""}
<!-- Info panel that can be toggled -->
${this.showInfo
? html`
<div class="message-info-panel">
<div class="info-row">
<span class="info-label">Type:</span>
<span class="info-value">${this.message?.type}</span>
</div>
<div class="info-row">
<span class="info-label">Time:</span>
<span class="info-value"
>${this.formatTimestamp(
this.message?.timestamp,
"",
)}</span
>
</div>
${this.message?.elapsed
? html`
<div class="info-row">
<span class="info-label">Duration:</span>
<span class="info-value"
>${this._formatDuration(
this.message?.elapsed,
)}</span
>
</div>
`
: ""}
${this.message?.usage
? html`
<div class="info-row">
<span class="info-label">Tokens:</span>
<span class="info-value">
${this.message?.usage
? html`
<div>
Input:
${this.formatNumber(
this.message?.usage?.input_tokens ||
0,
)}
</div>
${this.message?.usage
?.cache_creation_input_tokens
? html`
<div>
Cache creation:
${this.formatNumber(
this.message?.usage
?.cache_creation_input_tokens,
)}
</div>
`
: ""}
${this.message?.usage
?.cache_read_input_tokens
? html`
<div>
Cache read:
${this.formatNumber(
this.message?.usage
?.cache_read_input_tokens,
)}
</div>
`
: ""}
<div>
Output:
${this.formatNumber(
this.message?.usage?.output_tokens,
)}
</div>
<div>
Cost:
${this.formatCurrency(
this.message?.usage?.cost_usd,
)}
</div>
`
: "N/A"}
</span>
</div>
`
: ""}
${this.message?.conversation_id
? html`
<div class="info-row">
<span class="info-label">Conversation ID:</span>
<span class="info-value conversation-id"
>${this.message?.conversation_id}</span
>
</div>
`
: ""}
</div>
`
: ""}
</div>
<!-- Tool calls - only shown for agent messages -->
${this.message?.type === "agent"
? html`
<sketch-tool-calls
.toolCalls=${this.message?.tool_calls}
.open=${this.open}
></sketch-tool-calls>
`
: ""}
<!-- Commits section (redesigned as bubbles) -->
${this.message?.commits
? html`
<div class="commits-container">
<div class="commit-notification">
${this.message.commits.length} new
commit${this.message.commits.length > 1 ? "s" : ""}
detected
</div>
${this.message.commits.map((commit) => {
return html`
<div class="commit-card">
<span
class="commit-hash"
title="Click to copy: ${commit.hash}"
@click=${(e) =>
this.copyToClipboard(
commit.hash.substring(0, 8),
e,
)}
>
${commit.hash.substring(0, 8)}
</span>
${commit.pushed_branch
? html`
<span
class="commit-branch pushed-branch"
title="Click to copy: ${commit.pushed_branch}"
@click=${(e) =>
this.copyToClipboard(
commit.pushed_branch,
e,
)}
>${commit.pushed_branch}</span
>
`
: ``}
<span class="commit-subject"
>${commit.subject}</span
>
<button
class="commit-diff-button"
@click=${() => this.showCommit(commit.hash)}
>
View Diff
</button>
</div>
`;
})}
</div>
`
: ""}
</div>
</div>
<!-- Right side (empty for consistency) -->
<div class="message-metadata-right"></div>
</div>
</div>
`;
}
}
function copyButton(textToCopy: string) {
// Use an icon of overlapping rectangles for copy
const buttonClass = "copy-icon";
// SVG for copy icon (two overlapping rectangles)
const copyIcon = html`<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>`;
// SVG for success check mark
const successIcon = html`<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 6L9 17l-5-5"></path>
</svg>`;
const ret = html`<button
class="${buttonClass}"
title="Copy to clipboard"
@click=${(e: Event) => {
e.stopPropagation();
const copyButton = e.currentTarget as HTMLButtonElement;
const originalInnerHTML = copyButton.innerHTML;
navigator.clipboard
.writeText(textToCopy)
.then(() => {
copyButton.innerHTML = "";
const successElement = document.createElement("div");
copyButton.appendChild(successElement);
render(successIcon, successElement);
setTimeout(() => {
copyButton.innerHTML = originalInnerHTML;
}, 2000);
})
.catch((err) => {
console.error("Failed to copy text: ", err);
setTimeout(() => {
copyButton.innerHTML = originalInnerHTML;
}, 2000);
});
}}
>
${copyIcon}
</button>`;
return ret;
}
// Create global styles for floating messages
const floatingMessageStyles = document.createElement("style");
floatingMessageStyles.textContent = `
.floating-message {
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-family: system-ui, sans-serif;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.floating-message.success {
background-color: rgba(40, 167, 69, 0.9);
}
.floating-message.error {
background-color: rgba(220, 53, 69, 0.9);
}
/* Style for code, pre elements, and tool components to ensure proper wrapping/truncation */
pre, code, sketch-tool-calls, sketch-tool-card, sketch-tool-card-bash {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
/* Special rule for the message content container */
.message-content {
max-width: 100% !important;
overflow: hidden !important;
}
/* Ensure tool call containers don't overflow */
::slotted(sketch-tool-calls) {
max-width: 100%;
width: 100%;
overflow-wrap: break-word;
word-break: break-word;
}
`;
document.head.appendChild(floatingMessageStyles);
declare global {
interface HTMLElementTagNameMap {
"sketch-timeline-message": SketchTimelineMessage;
}
}