Unlocks editing for previous user messages in Gemini chats.
// ==UserScript==
// @name Gemini Editor
// @namespace https://github.com/ushan0v/gemini-editor
// @version 1.0.0
// @description Unlocks editing for previous user messages in Gemini chats.
// @author ushan0v
// @license MIT
// @match https://gemini.google.com/*
// @run-at document-start
// @grant none
// @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @homepageURL https://github.com/ushan0v/gemini-editor
// @supportURL https://github.com/ushan0v/gemini-editor/issues
// ==/UserScript==
(function () {
'use strict';
const DEBUG = false;
const LOG_PREFIX = '[Gemini Editor]';
const STYLE_ID = 'gemini-editor-userscript-style';
const TOOLTIP_ID = 'gemini-editor-tooltip';
const XHR_URL = Symbol('geminiEditorUrl');
const XHR_CAPTURE_ATTACHED = Symbol('geminiEditorCaptureAttached');
const XHR_STREAM_CAPTURE_LENGTH = Symbol('geminiEditorStreamCaptureLength');
const ATTRS = {
processed: 'data-gemini-editor-processed',
wrapper: 'data-gemini-editor-wrapper',
customButton: 'data-gemini-editor-button',
optimistic: 'data-gemini-editor-optimistic',
optimisticAttachments: 'data-gemini-editor-optimistic-attachments',
attachmentOwned: 'data-gemini-editor-owned',
attachmentKey: 'data-gemini-editor-attachment-key',
tooltip: 'data-gemini-editor-tooltip',
};
const SELECTORS = {
appMain: 'main',
conversationContainer: '.conversation-container',
userQuery: 'user-query',
editor: '.ql-editor.textarea',
textInputField: '.text-input-field',
queryText: '.query-text',
queryTextLine: '.query-text-line',
hiddenText: '.cdk-visually-hidden, .screen-reader-user-query-label, [aria-hidden="true"], [hidden]',
responseNodes: 'model-response, pending-response, dual-model-response, generative-ui-response',
inputAreaContainer: '.input-area-container',
attachmentPreviewWrapper: '.attachment-preview-wrapper',
nativeAttachmentPreviewWrapper: '.attachment-preview-wrapper:not([data-gemini-editor-owned="true"])',
ownedAttachmentPreviewWrapper: '.attachment-preview-wrapper[data-gemini-editor-owned="true"]',
attachmentPreviewContainer: 'uploader-file-preview-container',
ownedAttachmentPreviewContainer: 'uploader-file-preview-container[data-gemini-editor-owned="true"]',
userQueryFileButton: '[data-test-id="uploaded-file"] button[aria-label], button.new-file-preview-file[aria-label]',
userQueryImagePreview: 'img[data-test-id="uploaded-img"]',
userQueryVideoPreview: 'img[data-test-id="video-thumbnail"]',
editModeBar: '.gemini-edit-mode-bar',
copyIcon: 'mat-icon[fonticon="content_copy"]',
nativeEditIcon: 'mat-icon[fonticon="edit"]',
nativeEditButton: 'button[data-test-id="prompt-edit-button"]',
jslog: '[jslog]',
draftNode: '[data-test-draft-id]',
};
const UI_STRINGS = {
editLabel: 'Edit',
editTooltip: 'Edit prompt',
editMode: 'Editing mode',
cancel: 'Cancel',
removeFile: 'Remove file',
imagePreview: 'Image preview',
videoPreview: 'Video preview',
openImagePreview: 'Open uploaded image preview',
openVideoPreview: 'Open uploaded video preview',
unknownType: 'Unknown',
};
const state = {
editTargetContainer: null,
editContextPath: null,
pendingOverride: null,
optimisticContainer: null,
attachmentCache: new Map(),
attachmentCarryover: null,
cacheScopeConversationId: null,
observer: null,
scanQueued: false,
uiStarted: false,
tooltipTarget: null,
};
const CODE_FILE_EXTENSIONS = new Set([
'astro', 'bash', 'bat', 'c', 'cc', 'cfg', 'conf', 'cpp', 'cs',
'css', 'cts', 'cxx', 'go', 'graphql', 'h', 'hpp', 'htm', 'html',
'ini', 'java', 'js', 'json', 'jsx', 'kt', 'kts', 'less', 'lua',
'mjs', 'php', 'plist', 'properties', 'ps1',
'py', 'rb', 'rs', 'sass', 'scss', 'sh', 'sql', 'svelte', 'svg',
'swift', 'toml', 'ts', 'tsx', 'txt', 'vue', 'xml', 'yaml', 'yml',
'zsh',
]);
const PLAIN_TEXT_FILE_EXTENSIONS = new Set([
'csv', 'log', 'md', 'markdown', 'rst', 'text', 'tsv', 'txt',
]);
const ARCHIVE_FILE_EXTENSIONS = new Set([
'7z', 'bz2', 'gz', 'rar', 'tar', 'tgz', 'xz', 'zip',
]);
function logDebug() {
if (DEBUG) {
console.debug(LOG_PREFIX, '[debug]', ...arguments);
}
}
function getUiStrings() {
return UI_STRINGS;
}
function getNativeIconTemplate(fonticon) {
return document.querySelector(`mat-icon[fonticon="${fonticon}"]`)
|| document.querySelector('mat-icon.google-symbols');
}
function getScopeAttributeName(node, prefix) {
if (!node?.attributes) {
return null;
}
for (const attribute of Array.from(node.attributes)) {
if (attribute.name.startsWith(prefix)) {
return attribute.name;
}
}
return null;
}
function applyScopeAttribute(node, attributeName) {
if (node && attributeName) {
node.setAttribute(attributeName, '');
}
return node;
}
function getComposerScopeAttributes(textInputField) {
const field = textInputField || getTextInputField();
if (!field) {
return {
inputContentAttr: null,
previewContainerHostAttr: null,
previewChipContentAttr: null,
previewChipHostAttr: null,
previewInnerContentAttr: null,
};
}
const inputContentAttr = getScopeAttributeName(field, '_ngcontent-')
|| Array.from(field.children)
.map((child) => getScopeAttributeName(child, '_ngcontent-'))
.find(Boolean)
|| null;
const nativeContainer = field.querySelector(`${SELECTORS.nativeAttachmentPreviewWrapper} ${SELECTORS.attachmentPreviewContainer}`);
const nativeChip = nativeContainer?.querySelector('uploader-file-preview') || null;
const nativeInner = nativeChip?.querySelector('.file-preview-container, .file-preview, .image-preview') || null;
return {
inputContentAttr,
previewContainerHostAttr: getScopeAttributeName(nativeContainer, '_nghost-'),
previewChipContentAttr: getScopeAttributeName(nativeChip, '_ngcontent-'),
previewChipHostAttr: getScopeAttributeName(nativeChip, '_nghost-'),
previewInnerContentAttr: getScopeAttributeName(nativeInner, '_ngcontent-'),
};
}
function getOwnedTooltipElement() {
let tooltip = document.getElementById(TOOLTIP_ID);
if (tooltip) {
return tooltip;
}
tooltip = document.createElement('div');
tooltip.id = TOOLTIP_ID;
tooltip.className = 'gemini-editor-tooltip';
tooltip.setAttribute('role', 'tooltip');
tooltip.setAttribute('aria-hidden', 'true');
tooltip.hidden = true;
const target = document.body || document.documentElement;
target?.appendChild(tooltip);
return tooltip;
}
function positionOwnedTooltip(target) {
const tooltip = document.getElementById(TOOLTIP_ID);
if (!tooltip || !target?.isConnected) {
return;
}
const rect = target.getBoundingClientRect();
if (!rect.width && !rect.height) {
return;
}
const tooltipRect = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
const preferredTop = rect.top - tooltipRect.height - 8;
const fallbackTop = rect.bottom + 8;
const top = preferredTop >= 8
? preferredTop
: Math.min(fallbackTop, Math.max(8, window.innerHeight - tooltipRect.height - 8));
const left = Math.min(
Math.max(8, rect.left + (rect.width / 2) - (tooltipRect.width / 2)),
Math.max(8, viewportWidth - tooltipRect.width - 8),
);
tooltip.style.left = `${Math.round(left)}px`;
tooltip.style.top = `${Math.round(top)}px`;
}
function showOwnedTooltip(target) {
const tooltipText = target?.getAttribute?.(ATTRS.tooltip);
if (!tooltipText) {
return;
}
const tooltip = getOwnedTooltipElement();
state.tooltipTarget = target;
tooltip.textContent = tooltipText;
tooltip.hidden = false;
tooltip.setAttribute('aria-hidden', 'false');
tooltip.classList.add('visible');
window.requestAnimationFrame(() => {
if (state.tooltipTarget === target) {
positionOwnedTooltip(target);
}
});
}
function hideOwnedTooltip() {
state.tooltipTarget = null;
const tooltip = document.getElementById(TOOLTIP_ID);
if (!tooltip) {
return;
}
tooltip.classList.remove('visible');
tooltip.setAttribute('aria-hidden', 'true');
window.setTimeout(() => {
if (!tooltip.classList.contains('visible')) {
tooltip.hidden = true;
}
}, 120);
}
function syncOwnedTooltipPosition() {
if (state.tooltipTarget?.isConnected) {
positionOwnedTooltip(state.tooltipTarget);
return;
}
hideOwnedTooltip();
}
function initTooltipController() {
if (document.documentElement?.dataset?.geminiEditorTooltipReady === 'true') {
return;
}
document.documentElement.dataset.geminiEditorTooltipReady = 'true';
document.addEventListener('pointerover', (event) => {
const target = event.target?.closest?.(`[${ATTRS.tooltip}]`);
if (!target || target === state.tooltipTarget) {
return;
}
showOwnedTooltip(target);
}, true);
document.addEventListener('pointerout', (event) => {
if (!state.tooltipTarget) {
return;
}
const currentTarget = event.target?.closest?.(`[${ATTRS.tooltip}]`);
const relatedTarget = event.relatedTarget?.closest?.(`[${ATTRS.tooltip}]`) || null;
if (currentTarget === state.tooltipTarget && relatedTarget !== state.tooltipTarget) {
hideOwnedTooltip();
}
}, true);
document.addEventListener('focusin', (event) => {
const target = event.target?.closest?.(`[${ATTRS.tooltip}]`);
if (target) {
showOwnedTooltip(target);
}
}, true);
document.addEventListener('focusout', (event) => {
if (event.target?.closest?.(`[${ATTRS.tooltip}]`) === state.tooltipTarget) {
hideOwnedTooltip();
}
}, true);
window.addEventListener('scroll', syncOwnedTooltipPosition, true);
window.addEventListener('resize', syncOwnedTooltipPosition);
}
function ensureButtonRippleSpan(button) {
if (!button || button.querySelector(':scope > .mat-ripple.mat-mdc-button-ripple')) {
return;
}
const ripple = document.createElement('span');
ripple.className = 'mat-ripple mat-mdc-button-ripple';
button.appendChild(ripple);
}
function ensureComposerButtonRipples(root = document) {
root.querySelectorAll([
'button[data-test-id="bard-mode-menu-button"]',
'button.speech_dictation_mic_button',
'button.send-button.submit',
`button[${ATTRS.customButton}="true"]`,
].join(', ')).forEach(ensureButtonRippleSpan);
}
function logDebugIssue() {
if (DEBUG) {
console.debug(LOG_PREFIX, '[debug:issue]', ...arguments);
}
}
function decodeHtmlAttributeValue(rawValue) {
if (typeof rawValue !== 'string') {
return '';
}
return rawValue
.replace(/"/g, '"')
.replace(/"/g, '"')
.replace(/&/g, '&');
}
function extractBalancedBracketSegment(source, marker) {
if (typeof source !== 'string' || typeof marker !== 'string') {
return null;
}
const markerIndex = source.indexOf(marker);
if (markerIndex === -1) {
return null;
}
const startIndex = source.indexOf('[', markerIndex + marker.length);
if (startIndex === -1) {
return null;
}
let depth = 0;
let inString = false;
let escaped = false;
for (let index = startIndex; index < source.length; index += 1) {
const char = source[index];
if (escaped) {
escaped = false;
continue;
}
if (char === '\\') {
escaped = true;
continue;
}
if (char === '"') {
inString = !inString;
continue;
}
if (inString) {
continue;
}
if (char === '[') {
depth += 1;
continue;
}
if (char === ']') {
depth -= 1;
if (depth === 0) {
return source.slice(startIndex, index + 1);
}
}
}
return null;
}
function normalizeJslogMetadata(parsedMetadata) {
if (!parsedMetadata) {
return null;
}
const data = Array.isArray(parsedMetadata[0]) ? parsedMetadata[0] : parsedMetadata;
if (!Array.isArray(data)) {
return null;
}
const result = {
r: typeof data[0] === 'string' && data[0].startsWith('r_') ? data[0] : null,
c: typeof data[1] === 'string' && data[1].startsWith('c_') ? data[1] : null,
rc: typeof data[3] === 'string' && data[3].startsWith('rc_') ? data[3] : null,
};
return result.r || result.c || result.rc ? result : null;
}
function extractDataFromJslog(rawJslog) {
if (!rawJslog) {
return null;
}
const decoded = decodeHtmlAttributeValue(rawJslog);
const metadataJson = extractBalancedBracketSegment(decoded, 'BardVeMetadataKey:');
if (!metadataJson) {
return null;
}
try {
return normalizeJslogMetadata(JSON.parse(metadataJson));
} catch (error) {
logDebugIssue('Failed to parse jslog metadata.', error);
return null;
}
}
function getJslogDataScore(data) {
if (!data) {
return -1;
}
return (data.r ? 4 : 0) + (data.c ? 2 : 0) + (data.rc ? 1 : 0);
}
function mergeJslogData(base, next) {
if (!base) {
return next || null;
}
if (!next) {
return base;
}
return {
r: base.r || next.r || null,
c: base.c || next.c || null,
rc: base.rc || next.rc || null,
};
}
function getBestJslogData(root) {
if (!root) {
return null;
}
const nodes = [];
if (root instanceof Element && root.hasAttribute('jslog')) {
nodes.push(root);
}
if (root.querySelectorAll) {
nodes.push(...root.querySelectorAll(SELECTORS.jslog));
}
let best = null;
let bestScore = -1;
nodes.forEach((node) => {
const next = extractDataFromJslog(node.getAttribute('jslog'));
if (!next) {
return;
}
const merged = mergeJslogData(best, next);
const score = getJslogDataScore(merged);
if (score > bestScore) {
best = merged;
bestScore = score;
}
});
return best;
}
function getAttachmentCacheKey(conversationId, messageId) {
if (!conversationId || !messageId) {
return null;
}
return `${conversationId}::${messageId}`;
}
function parseBatchExecuteEntries(rawText) {
if (typeof rawText !== 'string' || !rawText.length) {
return [];
}
const normalized = rawText.replace(/^\)\]\}'\r?\n+/, '');
const lines = normalized.split('\n');
const entries = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
if (!line) {
continue;
}
const candidate = /^\d+$/.test(line) ? lines[index + 1] : line;
if (candidate === undefined) {
continue;
}
try {
const parsed = JSON.parse(candidate);
if (Array.isArray(parsed) && parsed.length === 1 && Array.isArray(parsed[0])) {
entries.push(parsed[0]);
} else {
entries.push(parsed);
}
if (/^\d+$/.test(line)) {
index += 1;
}
} catch {
continue;
}
}
return entries;
}
function getBatchExecutePayload(rawText, rpcid) {
const entry = parseBatchExecuteEntries(rawText).find((item) => {
return Array.isArray(item) && item[0] === 'wrb.fr' && item[1] === rpcid && typeof item[2] === 'string';
});
if (!entry) {
return null;
}
try {
return JSON.parse(entry[2]);
} catch (error) {
logDebugIssue('Failed to parse batchexecute payload.', error);
return null;
}
}
function getStreamGeneratePayloads(rawText) {
return parseBatchExecuteEntries(rawText)
.filter((item) => {
return Array.isArray(item) && item[0] === 'wrb.fr' && typeof item[2] === 'string';
})
.map((item) => {
try {
return JSON.parse(item[2]);
} catch (error) {
logDebugIssue('Failed to parse StreamGenerate payload.', error);
return null;
}
})
.filter(Boolean);
}
function isConversationId(value) {
return typeof value === 'string' && value.startsWith('c_');
}
function isMessageId(value) {
return typeof value === 'string' && value.startsWith('r_');
}
function getStreamPayloadIds(payload) {
const ids = Array.isArray(payload?.[1]) ? payload[1] : [];
const conversationId = isConversationId(ids[0])
? ids[0]
: getConversationIdFromLocation();
const messageId = isMessageId(ids[1])
? ids[1]
: (isMessageId(ids[0]) ? ids[0] : null);
return { conversationId, messageId };
}
function isRawAttachmentEntry(value) {
return Array.isArray(value)
&& typeof value[2] === 'string'
&& typeof value[5] === 'string'
&& typeof value[11] === 'string';
}
function collectTurnLikeNodes(root, out = []) {
if (!Array.isArray(root)) {
return out;
}
const turnKey = root[0];
if (Array.isArray(turnKey) && isConversationId(turnKey[0]) && isMessageId(turnKey[1])) {
out.push(root);
}
root.forEach((item) => {
if (Array.isArray(item)) {
collectTurnLikeNodes(item, out);
}
});
return out;
}
function collectAttachmentArrays(root, out = []) {
if (Array.isArray(root)) {
if (root.length > 0 && root.every(isRawAttachmentEntry)) {
out.push(root);
return out;
}
root.forEach((item) => {
collectAttachmentArrays(item, out);
});
return out;
}
if (root && typeof root === 'object') {
Object.values(root).forEach((item) => {
collectAttachmentArrays(item, out);
});
return out;
}
return out;
}
function dedupeAttachmentsByToken(attachments) {
const seen = new Set();
return attachments.filter((attachment) => {
if (!attachment?.token || seen.has(attachment.token)) {
return false;
}
seen.add(attachment.token);
return true;
});
}
function isAttachmentArray(value) {
return Array.isArray(value) && value.length > 0 && value.every(isRawAttachmentEntry);
}
function extractRawAttachmentsFromUserMessage(userMessage) {
if (!Array.isArray(userMessage)) {
return [];
}
const directCandidates = [
userMessage?.[4]?.[0]?.[3],
userMessage?.[4]?.[1],
userMessage?.[5]?.[0]?.[3],
];
for (const candidate of directCandidates) {
if (isAttachmentArray(candidate)) {
return candidate;
}
}
const attachmentArrays = collectAttachmentArrays(userMessage[4] ?? [], []);
return attachmentArrays.sort((left, right) => right.length - left.length)[0] || [];
}
function extractRawAttachmentsFromTurn(turn) {
if (!Array.isArray(turn)) {
return [];
}
const directUserMessage = Array.isArray(turn?.[2]?.[0])
? turn[2][0]
: null;
const directAttachments = extractRawAttachmentsFromUserMessage(directUserMessage);
if (directAttachments.length) {
return directAttachments;
}
const nestedUserMessage = Array.isArray(turn?.[2])
? turn[2].find((item) => {
return Array.isArray(item) && extractRawAttachmentsFromUserMessage(item).length > 0;
})
: null;
return extractRawAttachmentsFromUserMessage(nestedUserMessage);
}
function setAttachmentCarryover(conversationId, attachments, meta = {}) {
const nextAttachments = Array.isArray(attachments)
? attachments.map(cloneAttachmentRecord).filter(Boolean)
: [];
state.attachmentCarryover = nextAttachments.length
? {
conversationId,
attachments: nextAttachments,
createdAt: Date.now(),
submittedText: normalizePromptText(meta.submittedText ?? ''),
targetIndex: Number.isInteger(meta.targetIndex) ? meta.targetIndex : null,
}
: null;
}
function getAttachmentCarryover() {
if (!state.attachmentCarryover) {
return null;
}
if ((Date.now() - state.attachmentCarryover.createdAt) > 120000) {
state.attachmentCarryover = null;
return null;
}
return state.attachmentCarryover;
}
function promoteAttachmentCarryoverToContainer(container, index) {
const carryover = getAttachmentCarryover();
if (!carryover || !container) {
return false;
}
const userQuery = container.querySelector(SELECTORS.userQuery);
const currentData = mergeJslogData(
getBestJslogData(userQuery),
getBestJslogData(container),
);
const conversationId = currentData?.c || getConversationIdFromLocation();
const cacheKey = getAttachmentCacheKey(conversationId, currentData?.r);
if (!cacheKey || !currentData?.r || (carryover.conversationId && carryover.conversationId !== conversationId)) {
return false;
}
if (Number.isInteger(carryover.targetIndex) && Number.isInteger(index) && index < carryover.targetIndex) {
return false;
}
if (carryover.submittedText) {
const queryText = getPlainTextFromElement(userQuery?.querySelector(SELECTORS.queryText));
if (normalizePromptText(queryText) !== carryover.submittedText) {
return false;
}
}
const existing = state.attachmentCache.get(cacheKey);
if (Array.isArray(existing) && existing.length) {
state.attachmentCarryover = null;
return false;
}
state.attachmentCache.set(cacheKey, carryover.attachments.map(cloneAttachmentRecord).filter(Boolean));
state.attachmentCarryover = null;
logDebug('Promoted carryover attachments to refreshed message.', {
conversationId,
messageId: currentData.r,
count: state.attachmentCache.get(cacheKey)?.length ?? 0,
});
return true;
}
function getAttachmentCarryoverForContainer(container, index) {
const carryover = getAttachmentCarryover();
if (!carryover || !container) {
return [];
}
const userQuery = container.querySelector(SELECTORS.userQuery);
const currentData = mergeJslogData(
getBestJslogData(userQuery),
getBestJslogData(container),
);
const conversationId = currentData?.c || getConversationIdFromLocation();
if (carryover.conversationId && conversationId && carryover.conversationId !== conversationId) {
return [];
}
if (Number.isInteger(carryover.targetIndex) && Number.isInteger(index) && index < carryover.targetIndex) {
return [];
}
if (carryover.submittedText) {
const queryText = getPlainTextFromElement(userQuery?.querySelector(SELECTORS.queryText));
if (normalizePromptText(queryText) !== carryover.submittedText) {
return [];
}
}
return carryover.attachments.map(cloneAttachmentRecord).filter(Boolean);
}
function cloneAttachmentRecord(attachment) {
if (!attachment) {
return null;
}
return {
key: attachment.key,
kind: attachment.kind,
typeCode: attachment.typeCode,
filename: attachment.filename,
displayName: attachment.displayName,
typeLabel: attachment.typeLabel,
mime: attachment.mime,
token: attachment.token,
previewUrl: attachment.previewUrl,
downloadUrl: attachment.downloadUrl,
viewUrl: attachment.viewUrl,
width: attachment.width,
height: attachment.height,
durationSeconds: attachment.durationSeconds,
payloadRecord: cloneAttachmentPayloadRecord(attachment.payloadRecord),
};
}
function getAttachmentFileExtension(filename) {
if (typeof filename !== 'string') {
return '';
}
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
function stripAttachmentExtension(filename) {
if (typeof filename !== 'string') {
return '';
}
return filename.replace(/\.[^.]+$/, '');
}
function truncateMiddle(value, maxLength) {
if (typeof value !== 'string' || value.length <= maxLength) {
return value ?? '';
}
const edgeLength = Math.max(4, Math.floor((maxLength - 3) / 2));
return `${value.slice(0, edgeLength)}...${value.slice(-edgeLength)}`;
}
function getAttachmentMimeSubtype(mime) {
if (typeof mime !== 'string' || !mime.includes('/')) {
return '';
}
return mime.split('/')[1]?.split(';')[0]?.trim().toLowerCase() || '';
}
function isPlainTextAttachment(attachment) {
const extension = getAttachmentFileExtension(attachment?.filename);
const mime = typeof attachment?.mime === 'string' ? attachment.mime.toLowerCase() : '';
return mime === 'text/plain' || PLAIN_TEXT_FILE_EXTENSIONS.has(extension);
}
function isArchiveAttachment(attachment) {
const extension = getAttachmentFileExtension(attachment?.filename);
const mime = typeof attachment?.mime === 'string' ? attachment.mime.toLowerCase() : '';
return ARCHIVE_FILE_EXTENSIONS.has(extension)
|| mime === 'application/zip'
|| mime === 'application/x-zip-compressed'
|| mime === 'application/x-7z-compressed'
|| mime === 'application/x-rar-compressed'
|| mime === 'application/gzip'
|| mime === 'application/x-tar';
}
function formatAttachmentTypeLabel(attachment) {
const strings = getUiStrings();
const extension = getAttachmentFileExtension(attachment?.filename);
if (attachment?.kind === 'image') {
return extension ? extension.toUpperCase() : 'IMG';
}
if (attachment?.kind === 'video') {
return extension ? extension.toUpperCase() : 'VIDEO';
}
if (isCodeLikeAttachment(attachment) || isPlainTextAttachment(attachment) || isArchiveAttachment(attachment)) {
return extension ? extension.toUpperCase() : (getAttachmentMimeSubtype(attachment?.mime) || strings.unknownType).toUpperCase();
}
if (attachment?.mime === 'application/octet-stream') {
return strings.unknownType;
}
if (extension && extension.length <= 8) {
return extension.toUpperCase();
}
const mimeSubtype = getAttachmentMimeSubtype(attachment?.mime);
if (mimeSubtype && mimeSubtype.length <= 8) {
return mimeSubtype.toUpperCase();
}
return strings.unknownType;
}
function formatAttachmentDisplayName(attachment) {
if (!attachment?.filename) {
return '';
}
if (attachment.kind === 'image' || attachment.kind === 'video') {
return attachment.filename;
}
const isUnknownBinary = attachment.mime === 'application/octet-stream'
&& !isCodeLikeAttachment(attachment)
&& !isPlainTextAttachment(attachment)
&& !isArchiveAttachment(attachment);
const baseName = isUnknownBinary
? attachment.filename
: (stripAttachmentExtension(attachment.filename) || attachment.filename);
return truncateMiddle(baseName, isUnknownBinary ? 23 : 22);
}
function getAttachmentKind(rawAttachment) {
const mime = rawAttachment?.[11] ?? '';
const typeCode = Number(rawAttachment?.[1]);
if (typeCode === 1 || mime.startsWith('image/')) {
return 'image';
}
if (typeCode === 2 || mime.startsWith('video/')) {
return 'video';
}
return 'file';
}
function getAttachmentPreviewUrl(rawAttachment, kind) {
if (kind === 'image' && typeof rawAttachment?.[3] === 'string' && rawAttachment[3]) {
return rawAttachment[3];
}
const urlGroup = Array.isArray(rawAttachment?.[7]) ? rawAttachment[7] : [];
return typeof urlGroup[0] === 'string' && urlGroup[0] ? urlGroup[0] : null;
}
function getAttachmentViewUrl(rawAttachment, kind) {
const urlGroup = Array.isArray(rawAttachment?.[7]) ? rawAttachment[7] : [];
if (kind === 'video' && typeof urlGroup[2] === 'string' && urlGroup[2]) {
return urlGroup[2];
}
if (typeof rawAttachment?.[3] === 'string' && rawAttachment[3]) {
return rawAttachment[3];
}
return typeof urlGroup[0] === 'string' && urlGroup[0] ? urlGroup[0] : null;
}
function getAttachmentDimensions(rawAttachment, kind) {
if (kind === 'video' && Array.isArray(rawAttachment?.[16])) {
return {
width: Number(rawAttachment[16][2]) || null,
height: Number(rawAttachment[16][1]) || null,
};
}
if (Array.isArray(rawAttachment?.[15])) {
return {
width: Number(rawAttachment[15][0]) || null,
height: Number(rawAttachment[15][1]) || null,
};
}
return { width: null, height: null };
}
function getAttachmentDurationSeconds(rawAttachment) {
const durationParts = rawAttachment?.[16]?.[0];
if (!Array.isArray(durationParts)) {
return null;
}
const seconds = Number(durationParts[0]);
const nanos = Number(durationParts[1]);
if (!Number.isFinite(seconds)) {
return null;
}
if (!Number.isFinite(nanos)) {
return seconds;
}
return Math.max(0, Math.round(seconds + (nanos / 1000000000)));
}
function normalizeConversationAttachment(rawAttachment) {
if (!Array.isArray(rawAttachment)) {
return null;
}
const kind = getAttachmentKind(rawAttachment);
const filename = typeof rawAttachment[2] === 'string' ? rawAttachment[2] : '';
const mime = typeof rawAttachment[11] === 'string' ? rawAttachment[11] : '';
const token = typeof rawAttachment[5] === 'string' ? rawAttachment[5] : '';
if (!filename || !mime || !token) {
return null;
}
const { width, height } = getAttachmentDimensions(rawAttachment, kind);
const attachment = {
key: token || `${filename}:${mime}`,
kind,
typeCode: Number(rawAttachment[1]) || 0,
filename,
mime,
token,
previewUrl: getAttachmentPreviewUrl(rawAttachment, kind),
downloadUrl: typeof rawAttachment?.[7]?.[1] === 'string' && rawAttachment[7][1] ? rawAttachment[7][1] : null,
viewUrl: getAttachmentViewUrl(rawAttachment, kind),
width,
height,
durationSeconds: getAttachmentDurationSeconds(rawAttachment),
};
attachment.typeLabel = formatAttachmentTypeLabel(attachment);
attachment.displayName = formatAttachmentDisplayName(attachment);
return attachment;
}
function cloneAttachmentPayloadRecord(payloadRecord) {
if (!Array.isArray(payloadRecord)) {
return null;
}
return payloadRecord.map((item) => {
return Array.isArray(item) ? cloneAttachmentPayloadRecord(item) : item;
});
}
function normalizePayloadAttachmentRecord(payloadRecord, uiAttachment = {}) {
if (!Array.isArray(payloadRecord) || !Array.isArray(payloadRecord[0])) {
return null;
}
const typeCode = Number(payloadRecord[0][1]) || 0;
const filename = typeof payloadRecord[1] === 'string' ? payloadRecord[1] : '';
const mime = typeof payloadRecord[0][3] === 'string' ? payloadRecord[0][3] : '';
const token = typeof payloadRecord[2] === 'string' ? payloadRecord[2] : '';
if (!filename || !mime) {
return null;
}
const kind = typeCode === 1 || mime.startsWith('image/')
? 'image'
: (typeCode === 2 || mime.startsWith('video/') ? 'video' : 'file');
const attachment = {
key: token || `${filename}:${mime}:${payloadRecord[0][0] || ''}`,
kind,
typeCode,
filename,
mime,
token,
previewUrl: uiAttachment.previewUrl || null,
downloadUrl: null,
viewUrl: uiAttachment.viewUrl || uiAttachment.previewUrl || null,
width: null,
height: null,
durationSeconds: uiAttachment.durationSeconds ?? null,
payloadRecord: cloneAttachmentPayloadRecord(payloadRecord),
};
attachment.typeLabel = formatAttachmentTypeLabel(attachment);
attachment.displayName = formatAttachmentDisplayName(attachment);
return attachment;
}
function buildAttachmentPayloadRecord(attachment) {
const payloadRecord = cloneAttachmentPayloadRecord(attachment?.payloadRecord);
if (payloadRecord) {
return payloadRecord;
}
if (!attachment?.filename || !attachment?.mime || !attachment?.token) {
return null;
}
return [
[null, attachment.typeCode, 1, attachment.mime],
attachment.filename,
attachment.token,
];
}
function getCachedAttachmentsForMessage(conversationId, messageId) {
const cacheKey = getAttachmentCacheKey(conversationId, messageId);
const attachments = cacheKey ? state.attachmentCache.get(cacheKey) : null;
return Array.isArray(attachments)
? attachments.map(cloneAttachmentRecord).filter(Boolean)
: [];
}
function storeConversationLoadPayload(payload) {
const turns = collectTurnLikeNodes(payload);
const activeConversationId = getConversationIdFromLocation();
turns.forEach((turn) => {
const turnKey = Array.isArray(turn?.[0]) ? turn[0] : null;
const conversationId = turnKey?.[0] ?? null;
const messageId = turnKey?.[1] ?? null;
if (activeConversationId && conversationId && conversationId !== activeConversationId) {
return;
}
const cacheKey = getAttachmentCacheKey(conversationId, messageId);
if (!cacheKey) {
return;
}
const rawAttachmentArray = extractRawAttachmentsFromTurn(turn);
const attachments = dedupeAttachmentsByToken(
rawAttachmentArray
.map(normalizeConversationAttachment)
.filter(Boolean),
);
state.attachmentCache.set(cacheKey, attachments);
logDebug('Stored attachments from conversation-load.', {
conversationId,
messageId,
count: attachments.length,
});
});
}
function getAttachmentsFromPayload(payload) {
const rawAttachments = [];
collectAttachmentArrays(payload).forEach((attachmentArray) => {
rawAttachments.push(...attachmentArray);
});
return dedupeAttachmentsByToken(
rawAttachments
.map(normalizeConversationAttachment)
.filter(Boolean),
);
}
function haveSameAttachmentTokens(left, right) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
}
return left.every((attachment, index) => {
return attachment?.token && attachment.token === right[index]?.token;
});
}
function storeStreamGeneratePayload(payload) {
const { conversationId, messageId } = getStreamPayloadIds(payload);
const cacheKey = getAttachmentCacheKey(conversationId, messageId);
if (!cacheKey) {
return false;
}
const attachments = getAttachmentsFromPayload(payload);
if (!attachments.length) {
return false;
}
const existing = state.attachmentCache.get(cacheKey);
if (haveSameAttachmentTokens(existing, attachments)) {
return false;
}
state.attachmentCache.set(cacheKey, attachments);
logDebug('Stored attachments from StreamGenerate.', {
conversationId,
messageId,
count: attachments.length,
});
refreshPendingOverrideAttachments(conversationId, messageId, attachments);
return true;
}
function refreshPendingOverrideAttachments(conversationId, messageId, attachments) {
if (!state.pendingOverride?.attachments?.length || !state.editTargetContainer || !messageId) {
return;
}
const userQuery = state.editTargetContainer.querySelector(SELECTORS.userQuery);
const currentData = mergeJslogData(
getBestJslogData(userQuery),
getBestJslogData(state.editTargetContainer),
);
const targetConversationId = currentData?.c || getConversationIdFromLocation();
if (currentData?.r !== messageId || (conversationId && targetConversationId && conversationId !== targetConversationId)) {
return;
}
const usedAttachmentIndexes = new Set();
const nextAttachments = state.pendingOverride.attachments.map((pendingAttachment) => {
const replacementIndex = attachments.findIndex((attachment, index) => {
return !usedAttachmentIndexes.has(index)
&& attachment?.filename === pendingAttachment?.filename
&& attachment?.kind === pendingAttachment?.kind;
});
if (replacementIndex === -1) {
return pendingAttachment;
}
usedAttachmentIndexes.add(replacementIndex);
return attachments[replacementIndex];
});
const upgraded = nextAttachments.some((attachment, index) => {
return attachment?.token && attachment.token !== state.pendingOverride.attachments[index]?.token;
});
if (!upgraded) {
return;
}
state.pendingOverride.attachments = nextAttachments.map(cloneAttachmentRecord).filter(Boolean);
syncEditComposerAttachmentUi();
logDebug('Refreshed pending edit attachments from StreamGenerate tokens.', {
conversationId,
messageId,
count: state.pendingOverride.attachments.length,
});
}
function handleConversationLoadResponse(rawText) {
const payload = getBatchExecutePayload(rawText, 'hNvQHb');
if (!payload) {
return false;
}
storeConversationLoadPayload(payload);
return true;
}
function handleStreamGenerateResponse(rawText) {
return getStreamGeneratePayloads(rawText).reduce((storedAny, payload) => {
return storeStreamGeneratePayload(payload) || storedAny;
}, false);
}
function getAppMain() {
return document.querySelector(SELECTORS.appMain) || document.body;
}
function getEditor() {
return document.querySelector(SELECTORS.editor);
}
function getConversationContainers() {
return Array.from(document.querySelectorAll(SELECTORS.conversationContainer));
}
function normalizeRenderedLineBreaks(value) {
return typeof value === 'string' ? value.replace(/\r\n/g, '\n').replace(/\n$/, '') : '';
}
function normalizePromptText(text) {
return typeof text === 'string' ? text.replace(/\r\n/g, '\n') : '';
}
function getNormalizedPromptLines(text) {
const lines = normalizePromptText(text ?? '').split('\n');
return lines.length ? lines : [''];
}
function populatePromptLineNode(lineNode, text) {
lineNode.replaceChildren();
if (text.length) {
lineNode.textContent = text;
} else {
lineNode.appendChild(document.createElement('br'));
}
return lineNode;
}
function buildPromptLineNodes(text, createLineNode) {
return getNormalizedPromptLines(text).map((line) => {
return populatePromptLineNode(createLineNode(), line);
});
}
function getPromptTextFromQueryElement(element) {
if (!element) {
return '';
}
const lineNodes = Array.from(element.querySelectorAll(SELECTORS.queryTextLine));
if (!lineNodes.length) {
return '';
}
return lineNodes
.map((lineNode) => normalizeRenderedLineBreaks(lineNode.innerText))
.join('\n');
}
function getPromptTextFromEditor(editor) {
if (!editor) {
return '';
}
const blocks = Array.from(editor.children);
if (!blocks.length) {
return normalizeRenderedLineBreaks(editor.innerText);
}
return blocks
.map((block) => normalizeRenderedLineBreaks(block.innerText))
.join('\n');
}
function getPlainTextFromElement(element) {
if (!element) {
return '';
}
if (element.matches?.(SELECTORS.queryText)) {
return getPromptTextFromQueryElement(element);
}
if (element.matches?.(SELECTORS.editor)) {
return getPromptTextFromEditor(element);
}
const blockTags = new Set(['P', 'DIV', 'LI', 'UL', 'OL', 'PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
const parts = [];
function walk(node) {
node.childNodes.forEach((child) => {
if (child.nodeType === Node.TEXT_NODE) {
parts.push(child.nodeValue ?? '');
return;
}
if (child.nodeType !== Node.ELEMENT_NODE) {
return;
}
if (child.matches(SELECTORS.hiddenText)) {
return;
}
if (child.tagName === 'BR') {
parts.push('\n');
}
walk(child);
if (blockTags.has(child.tagName)) {
parts.push('\n');
}
});
}
walk(element);
let normalized = parts
.join('')
.replace(/\r\n/g, '\n');
if (normalized.endsWith('\n')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
function buildQuillParagraphs(text) {
return buildPromptLineNodes(text, () => document.createElement('p'));
}
function setEditorContent(editor, text) {
const fragment = document.createDocumentFragment();
buildQuillParagraphs(text).forEach((node) => fragment.appendChild(node));
editor.replaceChildren(fragment);
}
function getCurrentChatLocationKey() {
return `${window.location.pathname}${window.location.search}`;
}
function getConversationIdFromLocation() {
const match = window.location.pathname.match(/\/app\/([^/?#]+)/);
if (!match || !match[1]) {
return null;
}
return match[1].startsWith('c_') ? match[1] : `c_${match[1]}`;
}
function syncAttachmentCacheScope() {
const conversationId = getConversationIdFromLocation();
if (state.cacheScopeConversationId === conversationId) {
return;
}
if (state.cacheScopeConversationId === null) {
state.cacheScopeConversationId = conversationId;
return;
}
state.cacheScopeConversationId = conversationId;
state.attachmentCarryover = null;
logDebug('Updated active chat scope.', {
conversationId,
});
}
function getEditorText() {
const editor = getEditor();
return editor ? getPlainTextFromElement(editor) : '';
}
function getTextInputField() {
return document.querySelector(SELECTORS.textInputField);
}
function isCodeLikeAttachment(attachment) {
const extension = getAttachmentFileExtension(attachment?.filename);
if (attachment?.typeCode === 16 || CODE_FILE_EXTENSIONS.has(extension)) {
return true;
}
const mime = typeof attachment?.mime === 'string' ? attachment.mime.toLowerCase() : '';
return (mime.startsWith('text/')
&& mime !== 'text/plain')
|| mime === 'application/json'
|| mime === 'application/javascript'
|| mime === 'application/x-javascript'
|| mime === 'text/javascript'
|| mime === 'application/xml'
|| mime === 'text/xml'
|| mime === 'application/x-yaml';
}
function getAttachmentIconType(attachment) {
if (isCodeLikeAttachment(attachment)) {
return 'text/code';
}
if (isPlainTextAttachment(attachment)) {
return 'text/plain';
}
if (isArchiveAttachment(attachment)) {
return getAttachmentFileExtension(attachment?.filename) === 'zip'
? 'application/zip'
: 'application/octet-stream';
}
const mime = typeof attachment?.mime === 'string' ? attachment.mime.toLowerCase() : '';
return mime || 'application/octet-stream';
}
function getAttachmentIconUrl(attachment) {
return `https://drive-thirdparty.googleusercontent.com/32/type/${getAttachmentIconType(attachment)}`;
}
function getAttachmentIconAltText(attachment) {
const strings = getUiStrings();
return `${attachment?.typeLabel || strings.unknownType} file icon`;
}
function formatAttachmentDuration(durationSeconds) {
if (!Number.isFinite(durationSeconds) || durationSeconds < 0) {
return '';
}
const hours = Math.floor(durationSeconds / 3600);
const minutes = Math.floor((durationSeconds % 3600) / 60);
const seconds = Math.floor(durationSeconds % 60);
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
return `${minutes}:${String(seconds).padStart(2, '0')}`;
}
function createMaterialIcon(name) {
const template = getNativeIconTemplate(name);
if (template) {
const icon = template.cloneNode(true);
icon.removeAttribute('id');
icon.removeAttribute('aria-hidden');
icon.setAttribute('aria-hidden', 'true');
icon.setAttribute('fonticon', name);
icon.setAttribute('data-mat-icon-name', name);
if (name === 'play_arrow') {
icon.classList.add('icon-filled');
} else {
icon.classList.remove('icon-filled');
}
return icon;
}
const icon = document.createElement('mat-icon');
icon.setAttribute('role', 'img');
icon.setAttribute('fonticon', name);
icon.setAttribute('aria-hidden', 'true');
icon.setAttribute('data-mat-icon-type', 'font');
icon.setAttribute('data-mat-icon-name', name);
icon.className = 'mat-icon notranslate google-symbols mat-ligature-font mat-icon-no-color';
if (name === 'play_arrow') {
icon.classList.add('icon-filled');
}
icon.textContent = name;
return icon;
}
function getComposerNativeRemoveButtons(textInputField) {
const field = textInputField || getTextInputField();
if (!field) {
return [];
}
return Array.from(field.querySelectorAll(`uploader-file-preview:not([${ATTRS.attachmentOwned}="true"]) button[data-test-id="cancel-button"]`));
}
function clearNativeComposerAttachments(textInputField) {
const removeButtons = getComposerNativeRemoveButtons(textInputField);
removeButtons.forEach((button) => {
button.click();
});
}
function extractFilenameFromRemoveAriaLabel(label) {
if (typeof label !== 'string' || !label.trim()) {
return '';
}
const strings = getUiStrings();
const prefixes = [
strings.removeFile,
].filter(Boolean);
for (const prefix of prefixes) {
if (label.startsWith(`${prefix} `)) {
return label.slice(prefix.length + 1).trim();
}
}
return '';
}
function getVisibleNativeComposerAttachmentFilenames(textInputField) {
const field = textInputField || getTextInputField();
if (!field) {
return [];
}
const filenames = [];
const nativeChips = Array.from(field.querySelectorAll(`uploader-file-preview:not([${ATTRS.attachmentOwned}="true"])`));
nativeChips.forEach((chip) => {
const cancelLabel = chip.querySelector('button[data-test-id="cancel-button"]')?.getAttribute('aria-label');
const fileNameFromLabel = extractFilenameFromRemoveAriaLabel(cancelLabel);
if (fileNameFromLabel) {
filenames.push(fileNameFromLabel);
return;
}
const titledNode = chip.querySelector('[title]');
const titledValue = titledNode?.getAttribute('title');
if (titledValue) {
filenames.push(titledValue);
}
});
return filenames;
}
function parseAttachmentDurationLabel(value) {
if (typeof value !== 'string') {
return null;
}
const parts = value.trim().split(':').map((part) => Number(part));
if (!parts.length || parts.some((part) => !Number.isFinite(part) || part < 0)) {
return null;
}
return parts.reduce((total, part) => (total * 60) + part, 0);
}
function getVisibleNativeComposerAttachmentDetails(textInputField) {
const field = textInputField || getTextInputField();
if (!field) {
return [];
}
return Array.from(field.querySelectorAll(`uploader-file-preview:not([${ATTRS.attachmentOwned}="true"])`))
.map((chip) => {
const cancelLabel = chip.querySelector('button[data-test-id="cancel-button"]')?.getAttribute('aria-label');
const filename = extractFilenameFromRemoveAriaLabel(cancelLabel)
|| chip.querySelector('[title]')?.getAttribute('title')
|| '';
const imagePreview = chip.querySelector('img[data-test-id="image-preview"]');
const videoPreview = chip.querySelector('img[data-test-id="video-preview"]');
const previewUrl = normalizeAttachmentMatchUrl(
imagePreview?.getAttribute('src')
|| videoPreview?.getAttribute('src')
|| '',
);
const durationSeconds = parseAttachmentDurationLabel(
chip.querySelector('[data-test-id="video-timecode"]')?.textContent || '',
);
return {
filename,
previewUrl,
viewUrl: previewUrl,
durationSeconds,
};
})
.filter((attachment) => attachment.filename || attachment.previewUrl);
}
function filterNativePayloadAttachmentsByComposerUi(nativeAttachments, textInputField) {
if (!Array.isArray(nativeAttachments) || !nativeAttachments.length) {
return [];
}
const visibleFilenames = getVisibleNativeComposerAttachmentFilenames(textInputField);
if (!visibleFilenames.length) {
return [];
}
const counts = new Map();
visibleFilenames.forEach((filename) => {
counts.set(filename, (counts.get(filename) || 0) + 1);
});
return nativeAttachments.filter((attachmentRecord) => {
const filename = typeof attachmentRecord?.[1] === 'string'
? attachmentRecord[1]
: '';
const remaining = counts.get(filename) || 0;
if (!filename || remaining < 1) {
return false;
}
counts.set(filename, remaining - 1);
return true;
});
}
function normalizeNativePayloadAttachments(nativeAttachments, textInputField) {
if (!Array.isArray(nativeAttachments) || !nativeAttachments.length) {
return [];
}
const uiAttachments = getVisibleNativeComposerAttachmentDetails(textInputField);
return nativeAttachments
.map((payloadRecord, index) => {
const filename = typeof payloadRecord?.[1] === 'string' ? payloadRecord[1] : '';
const matchingUiAttachment = uiAttachments.find((attachment) => {
return attachment.filename && attachment.filename === filename;
}) || uiAttachments[index] || {};
return normalizePayloadAttachmentRecord(payloadRecord, matchingUiAttachment);
})
.filter(Boolean);
}
function normalizeAttachmentMatchUrl(url) {
return typeof url === 'string' ? url.trim() : '';
}
function getVisibleUserQueryAttachmentDescriptors(userQuery) {
if (!userQuery) {
return [];
}
const previewNodes = Array.from(userQuery.querySelectorAll('user-query-file-preview'));
const descriptorNodes = previewNodes.length ? previewNodes : [userQuery];
const descriptors = [];
descriptorNodes.forEach((node) => {
const fileButton = node.querySelector(SELECTORS.userQueryFileButton);
if (fileButton) {
descriptors.push({
kind: 'file',
filename: fileButton.getAttribute('aria-label')?.trim() || '',
previewUrl: '',
});
return;
}
const imagePreview = node.querySelector(SELECTORS.userQueryImagePreview);
if (imagePreview) {
descriptors.push({
kind: 'image',
filename: '',
previewUrl: normalizeAttachmentMatchUrl(imagePreview.getAttribute('src')),
});
return;
}
const videoPreview = node.querySelector(SELECTORS.userQueryVideoPreview);
if (videoPreview) {
descriptors.push({
kind: 'video',
filename: '',
previewUrl: normalizeAttachmentMatchUrl(videoPreview.getAttribute('src')),
});
}
});
if (descriptors.length) {
return descriptors;
}
userQuery.querySelectorAll(SELECTORS.userQueryFileButton).forEach((button) => {
descriptors.push({
kind: 'file',
filename: button.getAttribute('aria-label')?.trim() || '',
previewUrl: '',
});
});
userQuery.querySelectorAll(SELECTORS.userQueryImagePreview).forEach((image) => {
descriptors.push({
kind: 'image',
filename: '',
previewUrl: normalizeAttachmentMatchUrl(image.getAttribute('src')),
});
});
userQuery.querySelectorAll(SELECTORS.userQueryVideoPreview).forEach((image) => {
descriptors.push({
kind: 'video',
filename: '',
previewUrl: normalizeAttachmentMatchUrl(image.getAttribute('src')),
});
});
return descriptors.filter((descriptor) => descriptor.filename || descriptor.previewUrl || descriptor.kind);
}
function filterCachedAttachmentsByUserQueryUi(attachments, userQuery) {
if (!Array.isArray(attachments) || !attachments.length) {
return [];
}
const descriptors = getVisibleUserQueryAttachmentDescriptors(userQuery);
if (!descriptors.length) {
return [];
}
const usedAttachmentIndexes = new Set();
const findAttachmentIndex = (descriptor) => {
const normalizedPreviewUrl = normalizeAttachmentMatchUrl(descriptor.previewUrl);
if (descriptor.filename) {
const filenameIndex = attachments.findIndex((attachment, index) => {
return !usedAttachmentIndexes.has(index) && attachment?.filename === descriptor.filename;
});
if (filenameIndex !== -1) {
return filenameIndex;
}
}
if (normalizedPreviewUrl) {
const previewIndex = attachments.findIndex((attachment, index) => {
return !usedAttachmentIndexes.has(index) && [
attachment?.previewUrl,
attachment?.viewUrl,
]
.map(normalizeAttachmentMatchUrl)
.includes(normalizedPreviewUrl);
});
if (previewIndex !== -1) {
return previewIndex;
}
}
return attachments.findIndex((attachment, index) => {
return !usedAttachmentIndexes.has(index) && attachment?.kind === descriptor.kind;
});
};
return descriptors
.map((descriptor) => {
const index = findAttachmentIndex(descriptor);
if (index === -1) {
return null;
}
usedAttachmentIndexes.add(index);
return attachments[index];
})
.filter(Boolean);
}
function removePendingAttachment(attachmentKey) {
if (!state.pendingOverride?.attachments?.length) {
return;
}
state.pendingOverride.attachments = state.pendingOverride.attachments.filter((attachment) => {
return attachment?.key !== attachmentKey;
});
logDebug('Removed pending attachment from edit state.', {
attachmentKey,
remaining: state.pendingOverride.attachments.length,
});
syncEditComposerAttachmentUi();
}
function createAttachmentRemoveButton(attachment, contentScopeAttr) {
const strings = getUiStrings();
const button = document.createElement('button');
button.type = 'button';
button.className = 'cancel-button ng-star-inserted';
button.setAttribute('data-test-id', 'cancel-button');
button.setAttribute('aria-label', `${strings.removeFile} ${attachment.filename}`);
button.setAttribute(ATTRS.attachmentOwned, 'true');
const icon = createMaterialIcon('close');
applyScopeAttribute(button, contentScopeAttr);
applyScopeAttribute(icon, contentScopeAttr);
button.appendChild(icon);
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
removePendingAttachment(attachment.key);
});
return button;
}
function createOwnedAttachmentShell(textInputField, attachment) {
const scope = getComposerScopeAttributes(textInputField);
const chip = document.createElement('uploader-file-preview');
chip.className = 'file-preview-chip ng-star-inserted';
chip.setAttribute(ATTRS.attachmentOwned, 'true');
chip.setAttribute(ATTRS.attachmentKey, attachment.key);
applyScopeAttribute(chip, scope.previewChipContentAttr);
applyScopeAttribute(chip, scope.previewChipHostAttr);
const container = document.createElement('div');
container.className = 'mat-mdc-tooltip-trigger file-preview-container';
container.setAttribute(ATTRS.attachmentOwned, 'true');
container.setAttribute(ATTRS.tooltip, attachment.filename);
applyScopeAttribute(container, scope.previewInnerContentAttr);
chip.appendChild(container);
return { chip, container, scope };
}
function createOwnedFileAttachmentChip(textInputField, attachment) {
const { chip, container, scope } = createOwnedAttachmentShell(textInputField, attachment);
const preview = document.createElement('div');
preview.className = 'file-preview discovery-feed-theme ng-star-inserted';
preview.setAttribute('data-test-id', 'file-preview');
preview.setAttribute(ATTRS.attachmentOwned, 'true');
applyScopeAttribute(preview, scope.previewInnerContentAttr);
const icon = document.createElement('img');
icon.className = 'file-icon';
icon.setAttribute('data-test-id', 'file-icon-img');
icon.src = getAttachmentIconUrl(attachment);
icon.alt = getAttachmentIconAltText(attachment);
applyScopeAttribute(icon, scope.previewInnerContentAttr);
const name = document.createElement('div');
name.className = 'file-name';
name.setAttribute('data-test-id', 'file-name');
name.title = attachment.filename;
name.textContent = ` ${attachment.displayName} `;
applyScopeAttribute(name, scope.previewInnerContentAttr);
const type = document.createElement('div');
type.className = 'file-type';
type.textContent = attachment.typeLabel;
applyScopeAttribute(type, scope.previewInnerContentAttr);
preview.appendChild(icon);
preview.appendChild(name);
preview.appendChild(type);
preview.appendChild(createAttachmentRemoveButton(attachment, scope.previewInnerContentAttr));
container.appendChild(preview);
return chip;
}
function createOwnedImageAttachmentChip(textInputField, attachment) {
const { chip, container, scope } = createOwnedAttachmentShell(textInputField, attachment);
const previewButton = document.createElement('button');
previewButton.type = 'button';
previewButton.className = 'image-preview clickable ng-star-inserted';
previewButton.setAttribute(ATTRS.attachmentOwned, 'true');
applyScopeAttribute(previewButton, scope.previewInnerContentAttr);
const image = document.createElement('img');
image.setAttribute('data-test-id', 'image-preview');
image.setAttribute('aria-label', getUiStrings().imagePreview);
image.src = attachment.previewUrl || attachment.viewUrl || '';
applyScopeAttribute(image, scope.previewInnerContentAttr);
previewButton.appendChild(image);
if (attachment.viewUrl) {
previewButton.addEventListener('click', () => {
window.open(attachment.viewUrl, '_blank', 'noopener,noreferrer');
});
}
container.appendChild(previewButton);
container.appendChild(createAttachmentRemoveButton(attachment, scope.previewInnerContentAttr));
return chip;
}
function createOwnedVideoAttachmentChip(textInputField, attachment) {
const strings = getUiStrings();
const { chip, container, scope } = createOwnedAttachmentShell(textInputField, attachment);
const preview = document.createElement('div');
preview.className = 'image-preview ng-star-inserted';
preview.setAttribute(ATTRS.attachmentOwned, 'true');
applyScopeAttribute(preview, scope.previewInnerContentAttr);
const imageContainer = document.createElement('div');
imageContainer.className = 'video-preview-img-container';
applyScopeAttribute(imageContainer, scope.previewInnerContentAttr);
const image = document.createElement('img');
image.setAttribute('data-test-id', 'video-preview');
image.setAttribute('aria-label', strings.videoPreview);
image.src = attachment.previewUrl || attachment.viewUrl || '';
applyScopeAttribute(image, scope.previewInnerContentAttr);
imageContainer.appendChild(image);
const timecodeWrapper = document.createElement('div');
timecodeWrapper.className = 'timecode-wrapper ng-star-inserted';
applyScopeAttribute(timecodeWrapper, scope.previewInnerContentAttr);
const timecode = document.createElement('span');
timecode.setAttribute('data-test-id', 'video-timecode');
timecode.className = 'video-timecode gds-label-m';
timecode.textContent = ` ${formatAttachmentDuration(attachment.durationSeconds)} `;
applyScopeAttribute(timecode, scope.previewInnerContentAttr);
timecodeWrapper.appendChild(timecode);
preview.appendChild(imageContainer);
preview.appendChild(timecodeWrapper);
preview.appendChild(createAttachmentRemoveButton(attachment, scope.previewInnerContentAttr));
container.appendChild(preview);
return chip;
}
function createOwnedAttachmentChip(textInputField, attachment) {
if (attachment.kind === 'image' && (attachment.previewUrl || attachment.viewUrl)) {
return createOwnedImageAttachmentChip(textInputField, attachment);
}
if (attachment.kind === 'video' && (attachment.previewUrl || attachment.viewUrl)) {
return createOwnedVideoAttachmentChip(textInputField, attachment);
}
return createOwnedFileAttachmentChip(textInputField, attachment);
}
function getOwnedComposerAttachmentNodes(container) {
if (!container) {
return [];
}
return Array.from(container.children).filter((node) => {
return node.nodeType === Node.ELEMENT_NODE && node.getAttribute(ATTRS.attachmentOwned) === 'true';
});
}
function removeOwnedAttachmentUi(textInputField) {
const field = textInputField || getTextInputField();
if (!field) {
return;
}
if (state.tooltipTarget?.closest?.(`[${ATTRS.attachmentOwned}="true"]`)) {
hideOwnedTooltip();
}
field.querySelectorAll(`uploader-file-preview[${ATTRS.attachmentOwned}="true"]`).forEach((node) => {
node.remove();
});
const ownedWrapper = field.querySelector(SELECTORS.ownedAttachmentPreviewWrapper);
if (ownedWrapper) {
ownedWrapper.remove();
}
if (!field.querySelector(SELECTORS.attachmentPreviewWrapper)) {
field.classList.remove('with-file-preview');
}
}
function ensureOwnedAttachmentContainer(textInputField) {
const nativeWrapper = textInputField.querySelector(SELECTORS.nativeAttachmentPreviewWrapper);
if (nativeWrapper) {
textInputField.querySelector(SELECTORS.ownedAttachmentPreviewWrapper)?.remove();
return nativeWrapper.querySelector(SELECTORS.attachmentPreviewContainer);
}
const scope = getComposerScopeAttributes(textInputField);
let ownedWrapper = textInputField.querySelector(SELECTORS.ownedAttachmentPreviewWrapper);
if (!ownedWrapper) {
ownedWrapper = document.createElement('div');
ownedWrapper.className = 'attachment-preview-wrapper ng-star-inserted';
ownedWrapper.setAttribute(ATTRS.attachmentOwned, 'true');
applyScopeAttribute(ownedWrapper, scope.inputContentAttr);
textInputField.insertBefore(ownedWrapper, textInputField.firstChild);
}
let ownedContainer = ownedWrapper.querySelector(SELECTORS.ownedAttachmentPreviewContainer);
if (!ownedContainer) {
ownedContainer = document.createElement('uploader-file-preview-container');
ownedContainer.className = 'uploader-file-preview-container ng-star-inserted';
ownedContainer.setAttribute(ATTRS.attachmentOwned, 'true');
applyScopeAttribute(ownedContainer, scope.inputContentAttr);
applyScopeAttribute(ownedContainer, scope.previewContainerHostAttr);
ownedWrapper.appendChild(ownedContainer);
}
return ownedContainer;
}
function syncEditComposerAttachmentUi() {
const textInputField = getTextInputField();
const attachments = Array.isArray(state.pendingOverride?.attachments)
? state.pendingOverride.attachments
: [];
if (!textInputField) {
return;
}
if (!attachments.length) {
removeOwnedAttachmentUi(textInputField);
return;
}
textInputField.classList.add('with-file-preview');
const ownedContainer = ensureOwnedAttachmentContainer(textInputField);
if (!ownedContainer) {
return;
}
const nextKeys = attachments.map((attachment) => attachment.key);
const currentKeys = getOwnedComposerAttachmentNodes(ownedContainer).map((node) => {
return node.getAttribute(ATTRS.attachmentKey);
});
const needsRender = nextKeys.length !== currentKeys.length
|| nextKeys.some((key, index) => key !== currentKeys[index]);
if (!needsRender) {
return;
}
const fragment = document.createDocumentFragment();
attachments.forEach((attachment) => {
fragment.appendChild(createOwnedAttachmentChip(textInputField, attachment));
});
const nativeWrapper = textInputField.querySelector(SELECTORS.nativeAttachmentPreviewWrapper);
if (nativeWrapper) {
const firstNativeNode = Array.from(ownedContainer.children).find((node) => {
return node.nodeType === Node.ELEMENT_NODE && node.getAttribute(ATTRS.attachmentOwned) !== 'true';
}) || null;
getOwnedComposerAttachmentNodes(ownedContainer).forEach((node) => {
node.remove();
});
ownedContainer.insertBefore(fragment, firstNativeNode);
return;
}
ownedContainer.replaceChildren(fragment);
}
function parsePixelValue(value) {
if (typeof value !== 'string') {
return null;
}
const match = value.trim().match(/^(-?\d+(?:\.\d+)?)px$/);
if (!match) {
return null;
}
const pixels = Number(match[1]);
return Number.isFinite(pixels) && pixels > 0 ? pixels : null;
}
function getPendingConversationMinHeight(targetContainer) {
const containers = getConversationContainers();
const lastContainer = containers[containers.length - 1] ?? null;
const candidates = [];
if (lastContainer) {
candidates.push(lastContainer);
}
if (targetContainer && targetContainer !== lastContainer) {
candidates.push(targetContainer);
}
for (const container of candidates) {
const inlineMinHeight = parsePixelValue(container.style?.minHeight || '');
if (inlineMinHeight) {
return `${Math.round(inlineMinHeight)}px`;
}
const computedMinHeight = parsePixelValue(window.getComputedStyle(container).minHeight);
if (computedMinHeight) {
return `${Math.round(computedMinHeight)}px`;
}
}
return null;
}
function getOptimisticConversationMinHeight(targetContainer) {
const pendingMinHeight = parsePixelValue(getPendingConversationMinHeight(targetContainer) || '');
const targetRect = targetContainer?.getBoundingClientRect?.();
const remainingViewportHeight = targetRect
? Math.max(0, Math.round(window.innerHeight - targetRect.top))
: 0;
const nextMinHeight = Math.max(pendingMinHeight || 0, remainingViewportHeight || 0);
return nextMinHeight > 0 ? `${nextMinHeight}px` : null;
}
function copyAngularScopeAttributes(source, target) {
if (!source?.attributes || !target) {
return;
}
Array.from(source.attributes).forEach((attribute) => {
if (attribute.name.startsWith('_ngcontent-') || attribute.name.startsWith('_nghost-')) {
target.setAttribute(attribute.name, attribute.value);
}
});
}
function cloneResponseShellNode(source, fallbackTag, fallbackClassName) {
const node = source ? source.cloneNode(false) : document.createElement(fallbackTag);
if (!source && fallbackClassName) {
node.className = fallbackClassName;
}
node.removeAttribute('id');
node.removeAttribute('jslog');
node.removeAttribute('aria-describedby');
node.removeAttribute('cdk-describedby-host');
return node;
}
function stripClonedResponseRuntimeAttributes(root) {
root.removeAttribute('id');
root.removeAttribute('jslog');
root.removeAttribute('aria-describedby');
root.removeAttribute('cdk-describedby-host');
root.querySelectorAll('[jslog], [aria-describedby], [cdk-describedby-host]').forEach((node) => {
node.removeAttribute('jslog');
node.removeAttribute('aria-describedby');
node.removeAttribute('cdk-describedby-host');
});
root.querySelectorAll('[id]').forEach((node) => {
if (typeof SVGElement === 'undefined' || !(node instanceof SVGElement)) {
node.removeAttribute('id');
}
});
}
function uniquifyClonedSvgIds(root) {
root.querySelectorAll('svg').forEach((svg, svgIndex) => {
const idNodes = Array.from(svg.querySelectorAll('[id]'));
if (!idNodes.length) {
return;
}
const suffix = `gemini-editor-${Date.now().toString(36)}-${svgIndex}-${Math.random().toString(36).slice(2)}`;
const idMap = new Map();
idNodes.forEach((node) => {
const oldId = node.getAttribute('id');
if (!oldId) {
return;
}
const newId = `${oldId}-${suffix}`;
idMap.set(oldId, newId);
node.setAttribute('id', newId);
});
if (!idMap.size) {
return;
}
const updateReferenceValue = (value) => {
let nextValue = value.replace(/url\(#([^)]+)\)/g, (match, id) => {
return idMap.has(id) ? `url(#${idMap.get(id)})` : match;
});
if (nextValue.startsWith('#') && idMap.has(nextValue.slice(1))) {
nextValue = `#${idMap.get(nextValue.slice(1))}`;
}
return nextValue;
};
[svg, ...Array.from(svg.querySelectorAll('*'))].forEach((node) => {
Array.from(node.attributes || []).forEach((attribute) => {
if (attribute.value.includes('#')) {
node.setAttribute(attribute.name, updateReferenceValue(attribute.value));
}
});
});
});
}
function createFallbackPendingAvatar(sourceResponseNode) {
const avatar = document.createElement('div');
avatar.className = 'avatar avatar_primary ng-star-inserted';
copyAngularScopeAttributes(sourceResponseNode, avatar);
const model = document.createElement('div');
model.className = 'avatar_primary_model is-gpi-avatar';
copyAngularScopeAttributes(sourceResponseNode, model);
const icon = document.createElement('mat-icon');
icon.className = 'google-symbols notranslate';
icon.setAttribute('fonticon', 'auto_awesome');
icon.textContent = 'auto_awesome';
model.appendChild(icon);
avatar.appendChild(model);
return avatar;
}
function createOptimisticResponseSlot(sourceResponseNode) {
if (!sourceResponseNode) {
return null;
}
const sourceResponseContainer = sourceResponseNode.querySelector('response-container');
const sourceInnerContainer = sourceResponseContainer?.querySelector('.response-container') || null;
const sourceHeader = sourceResponseNode.querySelector('.response-container-header');
const sourceControls = sourceHeader?.querySelector('.response-container-header-controls') || null;
const sourceAvatarWrapper = sourceHeader?.querySelector('.response-container-header-avatar') || null;
const sourceAvatar = sourceAvatarWrapper?.querySelector('.avatar')
|| sourceResponseNode.querySelector('.avatar.avatar_primary, .avatar');
const placeholder = document.createElement('pending-response');
placeholder.className = sourceResponseNode.className || 'ng-star-inserted';
placeholder.setAttribute('data-gemini-editor-pending-response', 'true');
copyAngularScopeAttributes(sourceResponseNode, placeholder);
const responseContainer = cloneResponseShellNode(
sourceResponseContainer,
'response-container',
'ng-star-inserted',
);
const innerContainer = cloneResponseShellNode(
sourceInnerContainer,
'div',
'response-container response-container-with-gpi is-mobile',
);
if (!innerContainer.classList.contains('response-container')) {
innerContainer.classList.add('response-container');
}
const header = cloneResponseShellNode(
sourceHeader,
'div',
'response-container-header ng-star-inserted',
);
const controls = cloneResponseShellNode(
sourceControls,
'div',
'response-container-header-controls',
);
const avatarWrapper = cloneResponseShellNode(
sourceAvatarWrapper,
'div',
'response-container-header-avatar ng-star-inserted',
);
avatarWrapper.appendChild(
sourceAvatar ? sourceAvatar.cloneNode(true) : createFallbackPendingAvatar(sourceResponseNode),
);
header.appendChild(controls);
header.appendChild(avatarWrapper);
innerContainer.appendChild(header);
responseContainer.appendChild(innerContainer);
placeholder.appendChild(responseContainer);
stripClonedResponseRuntimeAttributes(placeholder);
uniquifyClonedSvgIds(placeholder);
placeholder.style.display = 'block';
placeholder.style.width = '100%';
placeholder.style.flex = '1 1 auto';
placeholder.style.minHeight = '48px';
return placeholder;
}
function setOptimisticQueryText(queryTextElement, text) {
if (!queryTextElement) {
return;
}
const lineTemplate = queryTextElement.querySelector(SELECTORS.queryTextLine);
const lineNodes = buildPromptLineNodes(text, () => {
const paragraph = lineTemplate
? lineTemplate.cloneNode(false)
: document.createElement('p');
if (!lineTemplate) {
paragraph.className = 'query-text-line';
}
return paragraph;
});
const childNodes = Array.from(queryTextElement.childNodes);
const fragment = document.createDocumentFragment();
let lineNodesInserted = false;
childNodes.forEach((node) => {
const isLineNode = node.nodeType === Node.ELEMENT_NODE
&& node instanceof Element
&& node.matches(SELECTORS.queryTextLine);
if (isLineNode) {
if (!lineNodesInserted) {
lineNodes.forEach((lineNode) => fragment.appendChild(lineNode));
lineNodesInserted = true;
}
return;
}
fragment.appendChild(node.cloneNode(true));
});
if (!lineNodesInserted) {
lineNodes.forEach((lineNode) => fragment.appendChild(lineNode));
}
queryTextElement.replaceChildren(fragment);
}
function getAttachmentPreviewDescriptor(node) {
const fileButton = node?.querySelector?.(SELECTORS.userQueryFileButton);
if (fileButton) {
return {
kind: 'file',
filename: fileButton.getAttribute('aria-label')?.trim() || '',
previewUrl: '',
};
}
const imagePreview = node?.querySelector?.(SELECTORS.userQueryImagePreview);
if (imagePreview) {
return {
kind: 'image',
filename: '',
previewUrl: normalizeAttachmentMatchUrl(imagePreview.getAttribute('src')),
};
}
const videoPreview = node?.querySelector?.(SELECTORS.userQueryVideoPreview);
if (videoPreview) {
return {
kind: 'video',
filename: '',
previewUrl: normalizeAttachmentMatchUrl(videoPreview.getAttribute('src')),
};
}
return {
kind: '',
filename: '',
previewUrl: '',
};
}
function getAttachmentPreviewItemNode(previewNode) {
const parent = previewNode?.parentElement;
if (parent?.parentElement?.classList?.contains('scrollable-area')) {
return parent;
}
return previewNode;
}
function getUserQueryAttachmentPreviewItems(userQuery) {
return Array.from(userQuery?.querySelectorAll?.('user-query-file-preview') || [])
.map((previewNode, index) => {
return {
index,
previewNode,
itemNode: getAttachmentPreviewItemNode(previewNode),
descriptor: getAttachmentPreviewDescriptor(previewNode),
};
});
}
function selectAttachmentPreviewIndexes(userQuery, attachments) {
if (!Array.isArray(attachments) || !attachments.length) {
return new Set();
}
const items = getUserQueryAttachmentPreviewItems(userQuery);
const usedIndexes = new Set();
const findMatchIndex = (attachment) => {
const normalizedPreviewUrls = [
attachment?.previewUrl,
attachment?.viewUrl,
].map(normalizeAttachmentMatchUrl).filter(Boolean);
if (attachment?.filename) {
const filenameIndex = items.findIndex((item) => {
return !usedIndexes.has(item.index)
&& item.descriptor.filename
&& item.descriptor.filename === attachment.filename;
});
if (filenameIndex !== -1) {
return items[filenameIndex].index;
}
}
if (normalizedPreviewUrls.length) {
const previewIndex = items.findIndex((item) => {
return !usedIndexes.has(item.index)
&& item.descriptor.previewUrl
&& normalizedPreviewUrls.includes(item.descriptor.previewUrl);
});
if (previewIndex !== -1) {
return items[previewIndex].index;
}
}
const kindIndex = items.findIndex((item) => {
return !usedIndexes.has(item.index)
&& attachment?.kind
&& item.descriptor.kind === attachment.kind;
});
return kindIndex === -1 ? -1 : items[kindIndex].index;
};
attachments.forEach((attachment) => {
const index = findMatchIndex(attachment);
if (index !== -1) {
usedIndexes.add(index);
}
});
return usedIndexes;
}
function removeAttachmentPreviewItem(previewNode) {
const itemNode = getAttachmentPreviewItemNode(previewNode);
if (itemNode?.parentNode) {
itemNode.remove();
} else {
previewNode.remove();
}
}
function cloneFilteredUserQueryAttachmentContainers(sourceUserQuery, attachments) {
const selectedIndexes = selectAttachmentPreviewIndexes(sourceUserQuery, attachments);
if (!selectedIndexes.size) {
return [];
}
let previewIndex = 0;
return getTopLevelUserQueryAttachmentContainers(sourceUserQuery)
.map((sourceAttachmentContainer) => {
const clone = sourceAttachmentContainer.cloneNode(true);
Array.from(clone.querySelectorAll('user-query-file-preview')).forEach((previewNode) => {
if (!selectedIndexes.has(previewIndex)) {
removeAttachmentPreviewItem(previewNode);
}
previewIndex += 1;
});
if (!clone.querySelector('user-query-file-preview')) {
return null;
}
clone.setAttribute(ATTRS.optimisticAttachments, 'true');
stripClonedAttachmentRuntimeAttributes(clone);
return clone;
})
.filter(Boolean);
}
function getUserQueryAttachmentInsertBeforeNode(userQuery) {
const root = getUserQueryRoot(userQuery);
if (!root) {
return { root: null, beforeNode: null };
}
const queryContent = Array.from(root.children).find((node) => {
return node.nodeType === Node.ELEMENT_NODE
&& node instanceof Element
&& node.classList.contains('query-content');
}) || null;
return {
root,
beforeNode: queryContent || root.firstChild,
};
}
function replaceOptimisticUserQueryAttachmentsFromRecords(targetUserQuery, sourceUserQuery, attachments) {
if (!targetUserQuery || !sourceUserQuery) {
return false;
}
getTopLevelUserQueryAttachmentContainers(targetUserQuery).forEach((node) => node.remove());
const clonedContainers = cloneFilteredUserQueryAttachmentContainers(sourceUserQuery, attachments);
if (!clonedContainers.length) {
return false;
}
const { root, beforeNode } = getUserQueryAttachmentInsertBeforeNode(targetUserQuery);
if (!root) {
return false;
}
clonedContainers.forEach((container) => {
root.insertBefore(container, beforeNode);
});
return true;
}
function appendOptimisticUserQueryAttachmentsFromSource(sourceContainer, targetContainer) {
const sourceUserQuery = sourceContainer?.querySelector?.(SELECTORS.userQuery);
const targetUserQuery = targetContainer?.querySelector?.(SELECTORS.userQuery);
if (!sourceUserQuery || !targetUserQuery) {
return false;
}
const sourceAttachmentContainers = getTopLevelUserQueryAttachmentContainers(sourceUserQuery)
.filter((node) => {
return Boolean(node.querySelector([
SELECTORS.userQueryFileButton,
SELECTORS.userQueryImagePreview,
SELECTORS.userQueryVideoPreview,
].join(', ')));
});
if (!sourceAttachmentContainers.length) {
return false;
}
const { root, beforeNode } = getUserQueryAttachmentInsertBeforeNode(targetUserQuery);
if (!root) {
return false;
}
sourceAttachmentContainers.forEach((sourceAttachmentContainer) => {
const clone = sourceAttachmentContainer.cloneNode(true);
clone.setAttribute(ATTRS.optimisticAttachments, 'true');
stripClonedAttachmentRuntimeAttributes(clone);
root.insertBefore(clone, beforeNode);
});
return true;
}
function createOptimisticConversationContainer(sourceContainer, text, pendingMinHeight, attachments = []) {
if (!sourceContainer) {
return null;
}
const optimisticContainer = sourceContainer.cloneNode(true);
optimisticContainer.setAttribute(ATTRS.optimistic, 'true');
optimisticContainer.removeAttribute('id');
optimisticContainer.querySelectorAll('[id]').forEach((node) => {
if (typeof SVGElement === 'undefined' || !(node instanceof SVGElement)) {
node.removeAttribute('id');
}
});
uniquifyClonedSvgIds(optimisticContainer);
if (pendingMinHeight) {
optimisticContainer.style.minHeight = pendingMinHeight;
}
const responseNode = optimisticContainer.querySelector(SELECTORS.responseNodes);
if (responseNode) {
const responseSlot = createOptimisticResponseSlot(responseNode);
if (responseSlot) {
responseNode.replaceWith(responseSlot);
} else {
responseNode.remove();
}
}
const queryTextElement = optimisticContainer.querySelector(SELECTORS.queryText);
setOptimisticQueryText(queryTextElement, text);
replaceOptimisticUserQueryAttachmentsFromRecords(
optimisticContainer.querySelector(SELECTORS.userQuery),
sourceContainer.querySelector(SELECTORS.userQuery),
attachments,
);
return optimisticContainer;
}
function moveCaretToEnd(editor) {
editor.focus();
const selection = window.getSelection();
if (!selection) {
return;
}
const range = document.createRange();
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
let lastTextNode = null;
while (walker.nextNode()) {
lastTextNode = walker.currentNode;
}
if (lastTextNode) {
range.setStart(lastTextNode, lastTextNode.textContent?.length ?? 0);
} else if (editor.lastElementChild) {
range.selectNodeContents(editor.lastElementChild);
} else {
range.selectNodeContents(editor);
}
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
function setDraftText(text) {
const editor = getEditor();
if (!editor) {
return;
}
setEditorContent(editor, text ?? '');
editor.dispatchEvent(new Event('input', { bubbles: true }));
window.setTimeout(() => moveCaretToEnd(editor), 0);
}
function getConversationContainersFrom(container) {
const containers = getConversationContainers();
const startIndex = containers.indexOf(container);
if (startIndex === -1) {
return [];
}
return containers.slice(startIndex);
}
function getUserQueryRoot(userQuery) {
const roots = Array.from(userQuery?.querySelectorAll?.('.user-query-container') || []);
return roots.find((root) => {
return Array.from(root.children).some((child) => {
return child.nodeType === Node.ELEMENT_NODE
&& child instanceof Element
&& (
child.classList.contains('query-content')
|| child.classList.contains('file-preview-container')
);
});
}) || roots[roots.length - 1] || userQuery || null;
}
function getTopLevelUserQueryAttachmentContainers(userQuery) {
const root = getUserQueryRoot(userQuery);
if (!root) {
return [];
}
return Array.from(root.children).filter((node) => {
return node.nodeType === Node.ELEMENT_NODE
&& node instanceof Element
&& node.classList.contains('file-preview-container');
});
}
function hasNativeUserQueryAttachmentContainer(userQuery) {
return getTopLevelUserQueryAttachmentContainers(userQuery).some((node) => {
return node.getAttribute(ATTRS.optimisticAttachments) !== 'true'
&& Boolean(node.querySelector([
SELECTORS.userQueryFileButton,
SELECTORS.userQueryImagePreview,
SELECTORS.userQueryVideoPreview,
].join(', ')));
});
}
function removeOptimisticUserQueryAttachmentContainers(userQuery) {
getTopLevelUserQueryAttachmentContainers(userQuery)
.filter((node) => node.getAttribute(ATTRS.optimisticAttachments) === 'true')
.forEach((node) => node.remove());
}
function cleanupOptimisticUserQueryAttachments(userQuery) {
if (hasNativeUserQueryAttachmentContainer(userQuery)) {
removeOptimisticUserQueryAttachmentContainers(userQuery);
}
}
function cleanupOptimisticUserQueryAttachmentsInDocument() {
document.querySelectorAll(SELECTORS.userQuery).forEach((userQuery) => {
cleanupOptimisticUserQueryAttachments(userQuery);
});
}
function stripClonedAttachmentRuntimeAttributes(root) {
root.removeAttribute('id');
root.removeAttribute('jslog');
root.removeAttribute('aria-describedby');
root.removeAttribute('cdk-describedby-host');
root.querySelectorAll('[id], [jslog], [aria-describedby], [cdk-describedby-host]').forEach((node) => {
node.removeAttribute('id');
node.removeAttribute('jslog');
node.removeAttribute('aria-describedby');
node.removeAttribute('cdk-describedby-host');
});
}
function copyOptimisticUserQueryAttachments(sourceContainer, targetContainer) {
const sourceUserQuery = sourceContainer?.querySelector?.(SELECTORS.userQuery);
const targetUserQuery = targetContainer?.querySelector?.(SELECTORS.userQuery);
if (!sourceUserQuery || !targetUserQuery || hasNativeUserQueryAttachmentContainer(targetUserQuery)) {
return false;
}
const sourceAttachmentContainers = getTopLevelUserQueryAttachmentContainers(sourceUserQuery)
.filter((node) => {
return Boolean(node.querySelector([
SELECTORS.userQueryFileButton,
SELECTORS.userQueryImagePreview,
SELECTORS.userQueryVideoPreview,
].join(', ')));
});
if (!sourceAttachmentContainers.length) {
return false;
}
removeOptimisticUserQueryAttachmentContainers(targetUserQuery);
const targetRoot = getUserQueryRoot(targetUserQuery);
if (!targetRoot) {
return false;
}
const queryContent = Array.from(targetRoot.children).find((node) => {
return node.nodeType === Node.ELEMENT_NODE
&& node instanceof Element
&& node.classList.contains('query-content');
}) || null;
sourceAttachmentContainers.forEach((sourceAttachmentContainer) => {
const clonedAttachmentContainer = sourceAttachmentContainer.cloneNode(true);
clonedAttachmentContainer.setAttribute(ATTRS.optimisticAttachments, 'true');
stripClonedAttachmentRuntimeAttributes(clonedAttachmentContainer);
targetRoot.insertBefore(clonedAttachmentContainer, queryContent || targetRoot.firstChild);
});
logDebug('Copied optimistic attachment UI to refreshed message.');
return true;
}
function syncOptimisticConversation() {
if (!state.optimisticContainer) {
return;
}
if (!state.optimisticContainer.isConnected) {
state.optimisticContainer = null;
return;
}
let node = state.optimisticContainer.nextElementSibling;
while (node) {
if (node.matches?.(SELECTORS.conversationContainer)) {
if (node.matches?.('pending-request')) {
appendOptimisticUserQueryAttachmentsFromSource(node, state.optimisticContainer);
node.remove();
return;
}
promoteAttachmentCarryoverToContainer(node, getConversationContainers().indexOf(node));
copyOptimisticUserQueryAttachments(state.optimisticContainer, node);
state.optimisticContainer.remove();
state.optimisticContainer = null;
return;
}
node = node.nextElementSibling;
}
}
function injectStyles() {
let style = document.getElementById(STYLE_ID);
const shouldAppendStyle = !style;
if (!style) {
style = document.createElement('style');
style.id = STYLE_ID;
}
style.textContent = `
.gemini-edit-mode-bar {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
box-sizing: border-box;
padding: 10px 24px;
margin-bottom: 0;
border-radius: 28px 28px 0 0;
background-color: var(--gem-sys-color--surface-container-high);
border: none;
font-family: "Google Sans", "Helvetica Neue", sans-serif;
animation: geminiEditSlideIn 0.2s ease;
z-index: 1;
position: relative;
top: 0;
}
@keyframes geminiEditSlideIn {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.gemini-input-active-edit input-area-v2 {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
border-top: none !important;
}
.gemini-edit-left {
display: flex;
align-items: center;
gap: 12px;
}
.gemini-edit-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--gem-sys-color--primary, #a8c7fa);
}
.gemini-edit-label {
font-size: 14px;
font-weight: 500;
color: var(--gem-sys-color--on-surface, #e3e3e3);
letter-spacing: 0.1px;
}
.gemini-edit-cancel {
background: transparent;
border: 1px solid var(--gem-sys-color--outline, #8e918f);
color: var(--gem-sys-color--primary, #a8c7fa);
cursor: pointer;
font-family: "Google Sans", sans-serif;
font-weight: 500;
font-size: 13px;
padding: 6px 16px;
border-radius: 100px;
transition: all 0.2s;
}
.gemini-edit-cancel:hover {
background-color: rgba(var(--gem-sys-color--primary-rgb, 168, 199, 250), 0.08);
border-color: var(--gem-sys-color--primary, #a8c7fa);
}
.gemini-editor-tooltip {
position: fixed;
z-index: 2147483647;
max-width: min(320px, calc(100vw - 16px));
padding: 6px 8px;
border-radius: 4px;
background: rgba(32, 33, 36, 0.96);
color: #fff;
font-family: "Google Sans", "Helvetica Neue", sans-serif;
font-size: 12px;
font-weight: 500;
line-height: 16px;
letter-spacing: 0.1px;
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.12s linear, transform 0.12s ease;
white-space: nowrap;
}
.gemini-editor-tooltip.visible {
opacity: 1;
transform: translateY(0);
}
.input-area-container [${ATTRS.wrapper}="true"],
.input-area-container [${ATTRS.customButton}="true"],
.text-input-field [${ATTRS.wrapper}="true"],
.text-input-field [${ATTRS.customButton}="true"] {
display: none !important;
}
user-query-content.edit-mode [${ATTRS.wrapper}="true"],
user-query-content.edit-mode [${ATTRS.customButton}="true"] {
display: none !important;
}
user-query[${ATTRS.processed}="true"] div:has(> ${SELECTORS.nativeEditButton}:not([${ATTRS.customButton}="true"])) {
display: none !important;
}
[data-gemini-editor-pending-response="true"] .avatar_primary_animation {
transform-origin: center;
animation: geminiEditorPendingAvatarPulse 1.35s ease-in-out infinite;
}
[data-gemini-editor-pending-response="true"] .avatar_primary_animation svg {
transform-origin: center;
animation: geminiEditorPendingAvatarTurn 1.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
[data-gemini-editor-pending-response="true"] .avatar_primary_animation svg g[mask] {
transform-origin: center;
animation: geminiEditorPendingAvatarSweep 1.35s ease-in-out infinite;
}
@keyframes geminiEditorPendingAvatarPulse {
0%, 100% {
opacity: 0.72;
filter: saturate(0.95);
}
45% {
opacity: 1;
filter: saturate(1.25);
}
}
@keyframes geminiEditorPendingAvatarTurn {
0% {
transform: rotate(0deg) scale(0.92);
}
45% {
transform: rotate(90deg) scale(1.04);
}
100% {
transform: rotate(180deg) scale(0.92);
}
}
@keyframes geminiEditorPendingAvatarSweep {
0%, 100% {
opacity: 0.68;
}
50% {
opacity: 1;
}
}
.text-input-field .attachment-preview-wrapper[${ATTRS.attachmentOwned}="true"] {
grid-area: file-preview;
display: flex;
gap: var(--gem-sys-spacing--s);
flex-wrap: nowrap;
overflow-x: auto;
max-height: 168px;
margin-inline: 0;
width: 100%;
align-self: stretch;
}
.text-input-field:has(.attachment-preview-wrapper[${ATTRS.attachmentOwned}="true"]) {
row-gap: var(--gem-sys-spacing--s);
}
.attachment-preview-wrapper[${ATTRS.attachmentOwned}="true"] > uploader-file-preview-container[${ATTRS.attachmentOwned}="true"] {
display: contents;
}
uploader-file-preview-container[${ATTRS.attachmentOwned}="true"] {
display: inline-flex;
flex-flow: nowrap;
overflow: auto;
width: auto;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] {
flex-shrink: 0;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-preview-container {
padding: 0;
position: relative;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-preview,
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .image-preview {
position: relative;
border-radius: var(--gem-sys-shape--corner-large);
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-preview.discovery-feed-theme {
background: var(--gem-sys-color--surface-container-high);
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-preview {
width: 208px;
height: unset;
padding: var(--gem-sys-spacing--l) 44px var(--gem-sys-spacing--l) var(--gem-sys-spacing--l);
display: grid;
grid-template:
"name name" 1fr
"icon type" var(--gem-sys-spacing--xl)
/ var(--gem-sys-spacing--xl) 1fr;
gap: var(--gem-sys-spacing--s) var(--gem-sys-spacing--xs);
align-items: center;
box-sizing: border-box;
text-align: start;
border-radius: var(--gem-sys-shape--corner-large-increased);
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-icon,
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-name,
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-type {
margin: 0;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-name {
grid-area: name;
color: var(--gem-sys-color--on-surface-variant);
font-family: "Google Sans Flex", "Google Sans", "Helvetica Neue", sans-serif;
font-size: var(--gem-sys-typography-type-scale--title-s-font-size);
font-weight: var(--gem-sys-typography-type-scale--title-s-font-weight);
letter-spacing: var(--gem-sys-typography-type-scale--title-s-font-tracking);
line-height: var(--gem-sys-typography-type-scale--title-s-line-height);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-icon {
grid-area: icon;
place-self: center;
width: var(--gem-sys-spacing--l);
height: var(--gem-sys-spacing--l);
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-type {
grid-area: type;
color: var(--gem-sys-color--on-surface-variant);
font-family: "Google Sans Flex", "Google Sans", "Helvetica Neue", sans-serif;
font-size: var(--gem-sys-typography-type-scale--label-m-font-size);
font-weight: var(--gem-sys-typography-type-scale--label-m-font-weight);
letter-spacing: var(--gem-sys-typography-type-scale--label-m-font-tracking);
line-height: var(--gem-sys-typography-type-scale--label-m-line-height);
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .image-preview {
width: 80px;
height: 80px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background: var(--gem-sys-color--surface-container-highest);
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] button.image-preview {
background: none;
border: none;
margin: 0;
padding: 0;
text-decoration: underline;
cursor: pointer;
color: var(--gem-sys-color--primary);
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .image-preview > img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .video-preview-img-container {
position: relative;
display: inline-block;
width: 100%;
height: 100%;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .video-preview-img-container::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.6));
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .video-preview-img-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .timecode-wrapper {
padding: var(--gem-sys-spacing--s);
position: absolute;
inset-inline-start: 0;
bottom: 0;
display: flex;
height: var(--gem-sys-spacing--l);
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .timecode-wrapper .video-timecode {
color: #fff;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .cancel-button {
width: unset;
height: unset;
padding: var(--gem-sys-spacing--s);
box-sizing: border-box;
position: absolute;
top: 0;
right: 0;
background: transparent;
border: none;
cursor: pointer;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .cancel-button > .mat-icon {
display: none;
position: relative;
align-items: center;
justify-content: center;
font-size: var(--gem-sys-spacing--xxl);
width: var(--gem-sys-spacing--xxl);
height: var(--gem-sys-spacing--xxl);
padding: var(--gem-sys-spacing--xs);
color: var(--gem-sys-color--on-surface-variant);
background-color: var(--gem-sys-color--surface);
border-radius: 50%;
opacity: 1;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .cancel-button > .mat-icon::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background-color: var(--gem-sys-color--on-surface-variant);
opacity: 0;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .cancel-button:hover > .mat-icon::after {
opacity: 0.08;
}
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-preview:hover .cancel-button > .mat-icon,
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .image-preview:hover .cancel-button > .mat-icon,
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-preview-container:hover > .image-preview + .cancel-button > .mat-icon,
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-preview-container:hover > button.image-preview + .cancel-button > .mat-icon,
uploader-file-preview[${ATTRS.attachmentOwned}="true"] .file-preview-container:focus-within > button.image-preview + .cancel-button > .mat-icon {
display: flex;
background-color: var(--gem-sys-color--surface);
}
`;
const target = document.head || document.documentElement;
if (target && shouldAppendStyle) {
target.appendChild(style);
}
}
function createEditButtonElement(copyButton) {
const strings = getUiStrings();
const container = document.createElement('div');
const baseWrapperClass = copyButton?.parentElement?.className || 'ng-star-inserted';
container.className = baseWrapperClass;
container.setAttribute(ATTRS.wrapper, 'true');
const button = copyButton ? copyButton.cloneNode(true) : document.createElement('button');
button.setAttribute('type', 'button');
button.setAttribute('aria-label', strings.editLabel);
button.setAttribute('data-gemini-editor-role', 'edit-button');
button.setAttribute(ATTRS.customButton, 'true');
button.setAttribute(ATTRS.tooltip, strings.editTooltip);
button.disabled = false;
button.removeAttribute('disabled');
button.removeAttribute('aria-disabled');
button.removeAttribute('aria-describedby');
button.removeAttribute('cdk-describedby-host');
button.removeAttribute('data-test-id');
button.removeAttribute('jslog');
button.removeAttribute('mattooltip');
const icon = button.querySelector('mat-icon');
if (icon) {
icon.setAttribute('fonticon', 'edit');
icon.setAttribute('data-mat-icon-name', 'edit');
icon.textContent = '';
}
ensureButtonRippleSpan(button);
container.appendChild(button);
return { container, button };
}
function getResponseJslogNode(container) {
const selectors = [
'model-response [jslog]',
'pending-response [jslog]',
'dual-model-response [jslog]',
'generative-ui-response [jslog]',
];
for (const selector of selectors) {
const node = container.querySelector(selector);
if (node) {
return node;
}
}
return null;
}
function getParentData(currentContainer, index, allContainers) {
const parentData = { r: null, c: null, rc: null };
if (index > 0) {
const previousContainer = allContainers[index - 1];
const previousUserQuery = previousContainer.querySelector(SELECTORS.userQuery);
const previousModelNode = getResponseJslogNode(previousContainer);
const userData = getBestJslogData(previousUserQuery);
const modelData = getBestJslogData(previousModelNode);
let rc = modelData?.rc ?? null;
if (!rc) {
const draftNode = previousContainer.querySelector(SELECTORS.draftNode);
rc = draftNode?.getAttribute('data-test-draft-id') ?? null;
}
parentData.r = userData?.r || modelData?.r || null;
parentData.c = userData?.c || modelData?.c || null;
parentData.rc = rc;
return parentData;
}
const currentUserQuery = currentContainer.querySelector(SELECTORS.userQuery);
const currentData = getBestJslogData(currentUserQuery);
parentData.c = currentData?.c || getConversationIdFromLocation();
return parentData;
}
function ensureEditModeBanner() {
const strings = getUiStrings();
const inputAreaContainer = document.querySelector(SELECTORS.inputAreaContainer);
if (!inputAreaContainer) {
return;
}
inputAreaContainer.classList.add('gemini-input-active-edit');
if (document.querySelector(SELECTORS.editModeBar)) {
return;
}
const banner = document.createElement('div');
banner.className = 'gemini-edit-mode-bar';
const left = document.createElement('div');
left.className = 'gemini-edit-left';
const iconWrapper = document.createElement('div');
iconWrapper.className = 'gemini-edit-icon';
const svgNs = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNs, 'svg');
svg.setAttribute('width', '20');
svg.setAttribute('height', '20');
svg.setAttribute('viewBox', '0 -960 960 960');
svg.setAttribute('fill', 'currentColor');
const path = document.createElementNS(svgNs, 'path');
path.setAttribute('d', 'M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 17l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z');
svg.appendChild(path);
iconWrapper.appendChild(svg);
const label = document.createElement('span');
label.className = 'gemini-edit-label';
label.textContent = strings.editMode;
const cancelButton = document.createElement('button');
cancelButton.className = 'gemini-edit-cancel';
cancelButton.type = 'button';
cancelButton.textContent = strings.cancel;
cancelButton.addEventListener('click', disableEditMode);
left.appendChild(iconWrapper);
left.appendChild(label);
banner.appendChild(left);
banner.appendChild(cancelButton);
inputAreaContainer.prepend(banner);
}
function enableEditMode(text, parentData, targetContainer, attachments) {
state.editTargetContainer = targetContainer;
state.editContextPath = getCurrentChatLocationKey();
state.pendingOverride = {
...parentData,
attachments: Array.isArray(attachments)
? attachments.map(cloneAttachmentRecord).filter(Boolean)
: [],
};
logDebug('Edit mode enabled.', {
text,
parentData,
attachmentCount: state.pendingOverride.attachments.length,
path: state.editContextPath,
});
setDraftText(text);
clearNativeComposerAttachments();
ensureEditModeBanner();
syncEditComposerAttachmentUi();
}
function disableEditMode() {
logDebug('Edit mode disabled.');
state.editTargetContainer = null;
state.editContextPath = null;
state.pendingOverride = null;
const banner = document.querySelector(SELECTORS.editModeBar);
if (banner) {
banner.remove();
}
const inputAreaContainer = document.querySelector(SELECTORS.inputAreaContainer);
if (inputAreaContainer) {
inputAreaContainer.classList.remove('gemini-input-active-edit');
}
removeOwnedAttachmentUi();
clearNativeComposerAttachments();
setDraftText('');
}
function syncEditModeWithCurrentChat() {
if (!state.editTargetContainer) {
return;
}
if (state.editContextPath !== getCurrentChatLocationKey() || !state.editTargetContainer.isConnected) {
disableEditMode();
}
}
function clearHistoryFrom(container) {
getConversationContainersFrom(container).forEach((node) => node.remove());
}
function getPendingOverrideAttachmentRecords() {
return Array.isArray(state.pendingOverride?.attachments)
? state.pendingOverride.attachments.map(cloneAttachmentRecord).filter(Boolean)
: [];
}
function beginOptimisticEditUi(targetContainer, submittedText, attachments = getPendingOverrideAttachmentRecords()) {
if (state.optimisticContainer?.isConnected) {
return true;
}
if (!targetContainer?.parentElement) {
return false;
}
const pendingMinHeight = getOptimisticConversationMinHeight(targetContainer);
const optimisticContainer = createOptimisticConversationContainer(
targetContainer,
submittedText,
pendingMinHeight,
attachments,
);
if (!optimisticContainer) {
return false;
}
targetContainer.parentElement.insertBefore(optimisticContainer, targetContainer);
state.optimisticContainer = optimisticContainer;
clearHistoryFrom(targetContainer);
return true;
}
function handleOverrideSuccess(targetContainer, submittedText, attachments = getPendingOverrideAttachmentRecords()) {
logDebug('Applying optimistic edit UI.', {
submittedText,
hasTarget: Boolean(targetContainer),
});
disableEditMode();
if (state.optimisticContainer?.isConnected) {
return;
}
state.optimisticContainer = null;
beginOptimisticEditUi(targetContainer, submittedText, attachments);
}
function handlePendingEditSubmitIntent() {
if (!state.pendingOverride || !state.editTargetContainer) {
return;
}
beginOptimisticEditUi(state.editTargetContainer, getEditorText(), getPendingOverrideAttachmentRecords());
}
function getPendingRequestText(pendingRequest) {
return getPlainTextFromElement(pendingRequest?.querySelector(SELECTORS.queryText))
|| getEditorText();
}
function syncPendingEditRequest() {
if (
!state.pendingOverride
|| !state.editTargetContainer
|| state.optimisticContainer?.isConnected
) {
return false;
}
const pendingRequest = document.querySelector('pending-request');
if (!pendingRequest) {
return false;
}
beginOptimisticEditUi(state.editTargetContainer, getPendingRequestText(pendingRequest), getPendingOverrideAttachmentRecords());
if (pendingRequest.isConnected) {
pendingRequest.remove();
}
return true;
}
function handleEditClick(userQuery) {
const currentContainer = userQuery.closest(SELECTORS.conversationContainer);
if (!currentContainer) {
logDebug('Edit click ignored: no conversation container.');
return;
}
const allContainers = getConversationContainers();
const index = allContainers.indexOf(currentContainer);
if (index === -1) {
return;
}
const textElement = userQuery.querySelector(SELECTORS.queryText);
const text = getPlainTextFromElement(textElement);
const parentData = getParentData(currentContainer, index, allContainers);
const currentData = mergeJslogData(
getBestJslogData(userQuery),
getBestJslogData(currentContainer),
);
let cachedAttachments = getCachedAttachmentsForMessage(
currentData?.c || getConversationIdFromLocation(),
currentData?.r,
);
if (!cachedAttachments.length && promoteAttachmentCarryoverToContainer(currentContainer, index)) {
cachedAttachments = getCachedAttachmentsForMessage(
currentData?.c || getConversationIdFromLocation(),
currentData?.r,
);
}
const carryoverAttachments = !cachedAttachments.length
? getAttachmentCarryoverForContainer(currentContainer, index)
: [];
const sourceAttachments = cachedAttachments.length ? cachedAttachments : carryoverAttachments;
const attachments = filterCachedAttachmentsByUserQueryUi(sourceAttachments, userQuery);
logDebug('Opening edit mode.', {
index,
text,
parentData,
messageId: currentData?.r ?? null,
cachedAttachmentCount: cachedAttachments.length,
carryoverAttachmentCount: carryoverAttachments.length,
attachmentCount: attachments.length,
});
enableEditMode(text, parentData, currentContainer, attachments);
}
function isLastConversationContainer(container) {
const containers = getConversationContainers();
return Boolean(container && containers[containers.length - 1] === container);
}
function getNativeEditButton(userQuery) {
const nativeButton = Array.from(userQuery.querySelectorAll(SELECTORS.nativeEditButton))
.find((button) => button.getAttribute(ATTRS.customButton) !== 'true');
if (nativeButton) {
return nativeButton;
}
return Array.from(userQuery.querySelectorAll(SELECTORS.nativeEditIcon))
.map((icon) => icon.closest('button'))
.find((button) => {
return button && button.getAttribute(ATTRS.customButton) !== 'true';
}) || null;
}
function hideNativeEditButton(nativeEditButton) {
const wrapper = nativeEditButton?.closest?.('div');
if (wrapper && wrapper.getAttribute(ATTRS.wrapper) !== 'true') {
wrapper.style.display = 'none';
}
}
function isNativeEditModeContainer(container) {
return Boolean(container?.querySelector?.('.edit-button-area button.cancel-button, .edit-button-area button.update-button'));
}
function removeCustomEditButtons(root) {
if (!root?.querySelectorAll) {
return;
}
root.querySelectorAll(`[${ATTRS.wrapper}="true"]`).forEach((node) => {
node.remove();
});
root.querySelectorAll(`[${ATTRS.customButton}="true"]`).forEach((button) => {
const wrapper = button.closest(`[${ATTRS.wrapper}="true"]`);
if (wrapper) {
wrapper.remove();
} else {
button.remove();
}
});
}
function cleanupMisplacedCustomEditButtons() {
document.querySelectorAll([
SELECTORS.inputAreaContainer,
SELECTORS.textInputField,
].join(', ')).forEach(removeCustomEditButtons);
getConversationContainers().forEach((container) => {
if (isNativeEditModeContainer(container)) {
removeCustomEditButtons(container);
}
});
}
function getCustomEditButtonWrappers(userQuery) {
const wrappers = Array.from(userQuery.querySelectorAll(`[${ATTRS.wrapper}="true"]`));
const orphanButtons = Array.from(userQuery.querySelectorAll(`[${ATTRS.customButton}="true"]`))
.filter((button) => !button.closest(`[${ATTRS.wrapper}="true"]`));
return {
wrappers,
orphanButtons,
count: wrappers.length + orphanButtons.length,
};
}
function hasStableCustomEditButton(userQuery, buttonsContainer) {
const custom = getCustomEditButtonWrappers(userQuery);
return custom.count === 1
&& custom.wrappers.length === 1
&& custom.wrappers[0].parentElement === buttonsContainer
&& Boolean(custom.wrappers[0].querySelector(`[${ATTRS.customButton}="true"]`));
}
function triggerNativeEditMode(userQuery, nativeEditButton, customButtonContainer) {
if (!nativeEditButton) {
return false;
}
const currentContainer = userQuery.closest(SELECTORS.conversationContainer);
const wrapper = nativeEditButton.closest('div');
const previousDisplay = wrapper?.style?.display;
customButtonContainer?.remove();
removeCustomEditButtons(currentContainer);
if (wrapper) {
wrapper.style.display = '';
}
const hadScriptEditMode = Boolean(state.pendingOverride || state.editTargetContainer);
nativeEditButton.click();
if (wrapper) {
wrapper.style.display = previousDisplay || 'none';
}
cleanupMisplacedCustomEditButtons();
window.requestAnimationFrame(cleanupMisplacedCustomEditButtons);
window.setTimeout(cleanupMisplacedCustomEditButtons, 100);
window.setTimeout(cleanupMisplacedCustomEditButtons, 300);
window.setTimeout(() => {
cleanupMisplacedCustomEditButtons();
if (!isNativeEditModeContainer(currentContainer)) {
processUserQuery(userQuery);
}
}, 1500);
if (hadScriptEditMode) {
disableEditMode();
}
return true;
}
function canUseNativeEditMode(currentContainer, nativeEditButton) {
return Boolean(nativeEditButton && isLastConversationContainer(currentContainer));
}
function processUserQuery(userQuery) {
if (!userQuery) {
return;
}
const currentContainer = userQuery.closest(SELECTORS.conversationContainer);
if (isNativeEditModeContainer(currentContainer)) {
removeCustomEditButtons(currentContainer);
return;
}
const nativeEditButton = getNativeEditButton(userQuery);
const copyIcon = userQuery.querySelector(SELECTORS.copyIcon);
if (!copyIcon) {
return;
}
const copyButton = copyIcon.closest('button');
const copyWrapper = copyButton?.parentElement;
const buttonsContainer = copyWrapper?.parentElement;
if (!copyButton || !copyWrapper || !buttonsContainer) {
return;
}
if (hasStableCustomEditButton(userQuery, buttonsContainer)) {
userQuery.setAttribute(ATTRS.processed, 'true');
hideNativeEditButton(nativeEditButton);
return;
}
removeCustomEditButtons(userQuery);
const { container, button } = createEditButtonElement(copyButton);
if (nativeEditButton) {
const nativeWrapper = nativeEditButton.closest('div');
if (nativeWrapper) {
nativeWrapper.style.display = 'none';
buttonsContainer.insertBefore(container, nativeWrapper);
} else if (copyWrapper.nextSibling) {
buttonsContainer.insertBefore(container, copyWrapper.nextSibling);
} else {
buttonsContainer.appendChild(container);
}
} else if (copyWrapper.nextSibling) {
buttonsContainer.insertBefore(container, copyWrapper.nextSibling);
} else {
buttonsContainer.appendChild(container);
}
userQuery.setAttribute(ATTRS.processed, 'true');
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (event.ctrlKey && canUseNativeEditMode(currentContainer, nativeEditButton)) {
triggerNativeEditMode(userQuery, nativeEditButton, container);
return;
}
logDebug('Custom edit button clicked.', {
text: userQuery.querySelector(SELECTORS.queryText)?.innerText ?? null,
});
handleEditClick(userQuery);
});
}
function scanConversationContainers() {
injectStyles();
ensureComposerButtonRipples();
cleanupMisplacedCustomEditButtons();
syncAttachmentCacheScope();
syncEditModeWithCurrentChat();
syncOptimisticConversation();
const containers = getConversationContainers();
containers.forEach((container) => {
const userQuery = container.querySelector(SELECTORS.userQuery);
if (userQuery) {
cleanupOptimisticUserQueryAttachments(userQuery);
processUserQuery(userQuery);
}
});
if (getAttachmentCarryover() && !state.optimisticContainer) {
for (const [index, container] of containers.entries()) {
if (promoteAttachmentCarryoverToContainer(container, index)) {
break;
}
}
}
if (state.pendingOverride) {
ensureEditModeBanner();
}
syncEditComposerAttachmentUi();
}
function scheduleScan() {
if (state.scanQueued) {
return;
}
state.scanQueued = true;
const schedule = window.requestAnimationFrame
? window.requestAnimationFrame.bind(window)
: (callback) => window.setTimeout(callback, 16);
schedule(() => {
state.scanQueued = false;
scanConversationContainers();
});
}
function attachObserver() {
if (state.observer) {
return;
}
const observerTarget = getAppMain();
if (!observerTarget) {
return;
}
state.observer = new MutationObserver(() => {
syncPendingEditRequest();
syncOptimisticConversation();
cleanupOptimisticUserQueryAttachmentsInDocument();
scheduleScan();
});
state.observer.observe(observerTarget, {
childList: true,
subtree: true,
});
}
function isEnabledSendButton(button) {
return button
&& !button.disabled
&& button.getAttribute('aria-disabled') !== 'true';
}
function handleSubmitIntentCapture(event) {
if (!state.pendingOverride) {
return;
}
const sendButton = event.target?.closest?.('button.send-button.submit');
if (isEnabledSendButton(sendButton)) {
handlePendingEditSubmitIntent();
}
}
function handleEditorSubmitKeyCapture(event) {
if (
!state.pendingOverride
|| event.defaultPrevented
|| event.isComposing
|| event.key !== 'Enter'
|| event.shiftKey
|| event.altKey
|| event.ctrlKey
|| event.metaKey
|| !event.target?.closest?.(SELECTORS.editor)
) {
return;
}
handlePendingEditSubmitIntent();
}
function patchHistoryNavigation() {
if (window.history.pushState.__geminiEditorPatched || window.history.replaceState.__geminiEditorPatched) {
return;
}
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
window.history.pushState = function pushState() {
const result = originalPushState.apply(this, arguments);
window.setTimeout(scheduleScan, 0);
return result;
};
window.history.replaceState = function replaceState() {
const result = originalReplaceState.apply(this, arguments);
window.setTimeout(scheduleScan, 0);
return result;
};
window.history.pushState.__geminiEditorPatched = true;
window.history.replaceState.__geminiEditorPatched = true;
}
function startUiController() {
if (state.uiStarted) {
return;
}
state.uiStarted = true;
injectStyles();
initTooltipController();
scheduleScan();
attachObserver();
patchHistoryNavigation();
window.addEventListener('pageshow', scheduleScan);
window.addEventListener('popstate', scheduleScan);
document.addEventListener('DOMContentLoaded', () => {
attachObserver();
scheduleScan();
}, { once: true });
document.addEventListener('click', handleSubmitIntentCapture, true);
document.addEventListener('keydown', handleEditorSubmitKeyCapture, true);
}
function isGeminiGenerateRequest(url) {
return typeof url === 'string' && url.includes('StreamGenerate');
}
function isBatchExecuteRequest(url) {
return typeof url === 'string' && url.includes('/_/BardChatUi/data/batchexecute');
}
function maybeCaptureConversationLoad(rawText) {
const captured = handleConversationLoadResponse(rawText);
if (captured) {
logDebug('Captured conversation-load payload.', {
cachedMessages: state.attachmentCache.size,
});
}
return captured;
}
function maybeCaptureStreamGenerate(rawText) {
const captured = handleStreamGenerateResponse(rawText);
if (captured) {
logDebug('Captured StreamGenerate attachments.', {
cachedMessages: state.attachmentCache.size,
});
window.setTimeout(scheduleScan, 0);
}
return captured;
}
function getRequestBodyText(body) {
if (typeof body === 'string') {
return body;
}
if (body instanceof URLSearchParams) {
return body.toString();
}
return '';
}
function parseStreamGenerateRequestBody(body) {
const bodyText = getRequestBodyText(body);
if (!bodyText) {
return null;
}
try {
const params = new URLSearchParams(bodyText);
const requestPayload = params.get('f.req');
if (!requestPayload) {
return null;
}
const outerPayload = JSON.parse(requestPayload);
if (!Array.isArray(outerPayload) || typeof outerPayload[1] !== 'string') {
return null;
}
const innerPayload = JSON.parse(outerPayload[1]);
return Array.isArray(innerPayload) ? innerPayload : null;
} catch (error) {
logDebugIssue('Failed to parse StreamGenerate request body.', error);
return null;
}
}
function getTextFromStreamGenerateRequestPayload(innerPayload) {
return typeof innerPayload?.[0]?.[0] === 'string' ? innerPayload[0][0] : '';
}
function getConversationIdFromStreamGenerateRequestPayload(innerPayload) {
const conversationId = innerPayload?.[2]?.[0];
return isConversationId(conversationId) ? conversationId : getConversationIdFromLocation();
}
function maybeCaptureOutgoingStreamGenerate(body, meta = {}) {
const innerPayload = parseStreamGenerateRequestBody(body);
const nativeAttachments = Array.isArray(innerPayload?.[0]?.[3]) ? innerPayload[0][3] : [];
if (!nativeAttachments.length) {
return false;
}
const attachments = normalizeNativePayloadAttachments(nativeAttachments, getTextInputField());
if (!attachments.length) {
return false;
}
const submittedText = meta.submittedText || getTextFromStreamGenerateRequestPayload(innerPayload);
setAttachmentCarryover(getConversationIdFromStreamGenerateRequestPayload(innerPayload), attachments, {
submittedText,
targetIndex: Number.isInteger(meta.targetIndex) ? meta.targetIndex : null,
});
window.setTimeout(scheduleScan, 0);
logDebug('Captured outgoing StreamGenerate attachments.', {
attachmentCount: attachments.length,
submittedText,
});
return true;
}
function maybeCaptureXhrStreamGenerateResponse(xhr) {
const rawText = typeof xhr?.responseText === 'string' ? xhr.responseText : '';
if (!rawText || rawText.length === xhr[XHR_STREAM_CAPTURE_LENGTH]) {
return;
}
xhr[XHR_STREAM_CAPTURE_LENGTH] = rawText.length;
maybeCaptureStreamGenerate(rawText);
}
function applyPendingOverride(body) {
if (!state.pendingOverride) {
return { applied: false, body };
}
if (typeof body !== 'string') {
if (body instanceof URLSearchParams) {
body = body.toString();
} else {
return { applied: false, body };
}
}
try {
const params = new URLSearchParams(body);
const requestPayload = params.get('f.req');
if (!requestPayload) {
return { applied: false, body };
}
const outerPayload = JSON.parse(requestPayload);
if (!Array.isArray(outerPayload) || typeof outerPayload[1] !== 'string') {
return { applied: false, body };
}
const innerPayload = JSON.parse(outerPayload[1]);
if (!Array.isArray(innerPayload) || !Array.isArray(innerPayload[2]) || !Array.isArray(innerPayload[0])) {
return { applied: false, body };
}
if (state.pendingOverride.c) {
innerPayload[2][0] = state.pendingOverride.c;
}
innerPayload[2][1] = state.pendingOverride.r;
innerPayload[2][2] = state.pendingOverride.rc;
const preservedAttachmentRecords = Array.isArray(state.pendingOverride.attachments)
? state.pendingOverride.attachments.map(cloneAttachmentRecord).filter(Boolean)
: [];
const preservedAttachments = preservedAttachmentRecords
.map(buildAttachmentPayloadRecord)
.filter(Boolean);
const nativeAttachments = filterNativePayloadAttachmentsByComposerUi(
Array.isArray(innerPayload[0][3]) ? innerPayload[0][3] : [],
getTextInputField(),
);
const nativeAttachmentRecords = normalizeNativePayloadAttachments(nativeAttachments, getTextInputField());
innerPayload[0][3] = [...preservedAttachments, ...nativeAttachments];
logDebug('Patched StreamGenerate payload.', {
c: innerPayload[2][0],
r: innerPayload[2][1],
rc: innerPayload[2][2],
attachmentCount: innerPayload[0][3].length,
preservedAttachmentCount: preservedAttachments.length,
nativeAttachmentCount: nativeAttachments.length,
});
outerPayload[1] = JSON.stringify(innerPayload);
params.set('f.req', JSON.stringify(outerPayload));
return {
applied: true,
conversationId: innerPayload[2][0] || state.pendingOverride.c || getConversationIdFromLocation(),
attachments: [...preservedAttachmentRecords, ...nativeAttachmentRecords],
body: params.toString(),
};
} catch (error) {
logDebugIssue('Failed to override StreamGenerate payload.', error);
return { applied: false, body };
}
}
function patchFetch() {
if (typeof window.fetch !== 'function' || window.fetch.__geminiEditorPatched) {
return;
}
const originalFetch = window.fetch;
window.fetch = function geminiEditorFetch(input, init) {
const url = typeof input === 'string'
? input
: (input && typeof input.url === 'string' ? input.url : '');
if (isGeminiGenerateRequest(url)) {
const submittedText = getEditorText();
maybeCaptureOutgoingStreamGenerate(init?.body, {
submittedText,
targetIndex: getConversationContainers().length,
});
}
return originalFetch.call(this, input, init).then((response) => {
if (isBatchExecuteRequest(url)) {
response.clone().text().then((rawText) => {
maybeCaptureConversationLoad(rawText);
}).catch((error) => {
logDebugIssue('Failed to capture batchexecute fetch response.', error);
});
}
if (isGeminiGenerateRequest(url)) {
response.clone().text().then((rawText) => {
maybeCaptureStreamGenerate(rawText);
}).catch((error) => {
logDebugIssue('Failed to capture StreamGenerate fetch response.', error);
});
}
return response;
});
};
window.fetch.__geminiEditorPatched = true;
}
function patchXmlHttpRequest() {
const xhrPrototype = XMLHttpRequest.prototype;
if (
xhrPrototype.open.__geminiEditorPatched
|| xhrPrototype.send.__geminiEditorPatched
) {
return;
}
const originalOpen = xhrPrototype.open;
const originalSend = xhrPrototype.send;
xhrPrototype.open = function open(method, url) {
this[XHR_URL] = typeof url === 'string' ? url : String(url);
return originalOpen.apply(this, arguments);
};
xhrPrototype.send = function send(body) {
let nextBody = body;
if (!this[XHR_CAPTURE_ATTACHED] && isBatchExecuteRequest(this[XHR_URL])) {
this.addEventListener('load', () => {
try {
maybeCaptureConversationLoad(this.responseText);
} catch (error) {
logDebugIssue('Failed to capture batchexecute XHR response.', error);
}
});
this[XHR_CAPTURE_ATTACHED] = true;
}
if (!this[XHR_CAPTURE_ATTACHED] && isGeminiGenerateRequest(this[XHR_URL])) {
this.addEventListener('progress', () => {
try {
maybeCaptureXhrStreamGenerateResponse(this);
} catch (error) {
logDebugIssue('Failed to capture progressive StreamGenerate XHR response.', error);
}
});
this.addEventListener('load', () => {
try {
maybeCaptureXhrStreamGenerateResponse(this);
} catch (error) {
logDebugIssue('Failed to capture StreamGenerate XHR response.', error);
}
});
this[XHR_CAPTURE_ATTACHED] = true;
}
if (state.pendingOverride && isGeminiGenerateRequest(this[XHR_URL])) {
const targetContainer = state.editTargetContainer;
const submittedText = getEditorText();
const pendingOverride = {
...state.pendingOverride,
attachments: Array.isArray(state.pendingOverride.attachments)
? state.pendingOverride.attachments.map(cloneAttachmentRecord).filter(Boolean)
: [],
};
const targetIndex = targetContainer
? getConversationContainers().indexOf(targetContainer)
: null;
logDebug('Intercepted candidate StreamGenerate request.', {
submittedText,
pendingOverride: state.pendingOverride,
});
const result = applyPendingOverride(body);
if (result.applied) {
nextBody = result.body;
const submittedAttachments = Array.isArray(result.attachments)
? result.attachments.map(cloneAttachmentRecord).filter(Boolean)
: pendingOverride.attachments;
setAttachmentCarryover(result.conversationId, submittedAttachments, {
submittedText,
targetIndex,
});
state.pendingOverride = null;
handleOverrideSuccess(targetContainer, submittedText, submittedAttachments);
}
} else if (isGeminiGenerateRequest(this[XHR_URL])) {
maybeCaptureOutgoingStreamGenerate(nextBody, {
submittedText: getEditorText(),
targetIndex: getConversationContainers().length,
});
}
return originalSend.call(this, nextBody);
};
xhrPrototype.open.__geminiEditorPatched = true;
xhrPrototype.send.__geminiEditorPatched = true;
}
function bootstrap() {
console.log(LOG_PREFIX, 'Initializing userscript.');
patchXmlHttpRequest();
patchFetch();
startUiController();
document.addEventListener('DOMContentLoaded', scheduleScan, { once: true });
}
bootstrap();
})();