0

Add images to results for history embeddings with answers enabled

If a history embeddings search result's url is known to sync, try to
get an image for the result through the PageImageService using the
same client ID as history clusters. If there is no image, the History
page falls back to a SVG and the side panel falls back to the
favicon. Update some styling for side panel.

History page: http://screen/A4eXugpVh2z9eNY
History page: http://screen/7ZiUuXRiQiuyVVg
Side panel: http://screen/6Vh8qSuvZz4tpR3

Bug: 369210842
Change-Id: Ie93539e01687e38dde2e6a474b5857a2ae98ef99
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6133460
Reviewed-by: manuk hovanesian <manukh@chromium.org>
Commit-Queue: John Lee <johntlee@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1401643}
This commit is contained in:
John Lee
2025-01-02 16:20:16 -08:00
committed by Chromium LUCI CQ
parent 5e816e57f6
commit 603e84f127
9 changed files with 177 additions and 6 deletions
chrome/test/data/webui/cr_components/history_embeddings
ui/webui/resources/cr_components/history_embeddings

@ -15,6 +15,7 @@ build_webui_tests("build") {
ts_deps = [
"//ui/webui/resources/cr_components/history_embeddings:build_ts",
"//ui/webui/resources/cr_components/page_image_service:build_ts",
"//ui/webui/resources/cr_elements:build_ts",
"//ui/webui/resources/js:build_ts",
]

@ -916,7 +916,7 @@ import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';
}
const favicons = element.shadowRoot!.querySelectorAll<HTMLElement>(
'.result-item .favicon');
'.result-url-and-favicon .favicon');
assertEquals(2, favicons.length);
assertEquals(
getFaviconForPageURL(mockResults[0]!.url.url, true),

@ -2,16 +2,44 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://history/strings.m.js';
import 'chrome://resources/cr_components/history_embeddings/result_image.js';
import type {SearchResultItem} from 'chrome://resources/cr_components/history_embeddings/history_embeddings.mojom-webui.js';
import type {HistoryEmbeddingsResultImageElement} from 'chrome://resources/cr_components/history_embeddings/result_image.js';
import {assertTrue} from 'chrome://webui-test/chai_assert.js';
import {PageImageServiceBrowserProxy} from 'chrome://resources/cr_components/page_image_service/browser_proxy.js';
import {ClientId as PageImageServiceClientId, PageImageServiceHandlerRemote} from 'chrome://resources/cr_components/page_image_service/page_image_service.mojom-webui.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {TestMock} from 'chrome://webui-test/test_mock.js';
suite('cr-history-embeddings-result-image', () => {
let element: HistoryEmbeddingsResultImageElement;
let imageServiceHandler: TestMock<PageImageServiceHandlerRemote>&
PageImageServiceHandlerRemote;
function generateResult(): SearchResultItem {
return {
title: 'Google',
url: {url: 'http://google.com'},
urlForDisplay: 'google.com',
relativeTime: '2 hours ago',
shortDateTime: 'Sept 2, 2022',
sourcePassage: 'Google description',
lastUrlVisitTimestamp: 1000,
answerData: null,
isUrlKnownToSync: false,
};
}
setup(() => {
document.body.innerHTML = window.trustedTypes!.emptyHTML;
imageServiceHandler = TestMock.fromClass(PageImageServiceHandlerRemote);
PageImageServiceBrowserProxy.setInstance(
new PageImageServiceBrowserProxy(imageServiceHandler));
loadTimeData.overrideValues({enableHistoryEmbeddingsImages: true});
element = document.createElement('cr-history-embeddings-result-image');
document.body.appendChild(element);
});
@ -20,4 +48,57 @@ suite('cr-history-embeddings-result-image', () => {
const svg = element.shadowRoot!.querySelector('svg');
assertTrue(!!svg);
});
test('RequestsImagesIfKnownToSync', async () => {
// By default, results are not known to sync so they should not request
// images.
element.searchResult = generateResult();
await element.updateComplete;
assertEquals(0, imageServiceHandler.getCallCount('getPageImageUrl'));
// Set a new result that is known to sync and verify that an image is
// requested with the correct arguments.
element.searchResult =
Object.assign(generateResult(), {isUrlKnownToSync: true});
await element.updateComplete;
const requestArgs = await imageServiceHandler.whenCalled('getPageImageUrl');
assertEquals(1, imageServiceHandler.getCallCount('getPageImageUrl'));
assertEquals(PageImageServiceClientId.Journeys, requestArgs[0]);
assertDeepEquals({url: 'http://google.com'}, requestArgs[1]);
assertDeepEquals(
{suggestImages: true, optimizationGuideImages: true}, requestArgs[2]);
});
test('ShowsImages', async () => {
// Make the handler return an invalid response and verify that the image
// is not set and is hidden.
imageServiceHandler.setResultFor(
'getPageImageUrl', Promise.resolve({result: undefined}));
element.searchResult =
Object.assign(generateResult(), {isUrlKnownToSync: true});
await element.updateComplete;
await imageServiceHandler.whenCalled('getPageImageUrl');
let imageElement = element.shadowRoot!.querySelector('#image')!;
assertTrue(imageElement.hasAttribute('hidden'));
assertFalse(element.hasAttribute('has-image'));
// Make the handler now return a valid image URL.
imageServiceHandler.reset();
const imageUrl = 'https://some-server/some-image.png';
imageServiceHandler.setResultFor(
'getPageImageUrl',
Promise.resolve({result: {imageUrl: {url: imageUrl}}}));
element.searchResult =
Object.assign(generateResult(), {isUrlKnownToSync: true});
await element.updateComplete;
await imageServiceHandler.whenCalled('getPageImageUrl');
await element.updateComplete;
// Verify images are shown and set to the correct url.
imageElement = element.shadowRoot!.querySelector('#image')!;
assertFalse(imageElement.hasAttribute('hidden'));
assertEquals(imageUrl, imageElement.getAttribute('auto-src'));
assertTrue(element.hasAttribute('has-image'));
});
});

@ -38,6 +38,7 @@ build_webui("build") {
"$root_gen_dir/ui/webui/resources/tsc/cr_components/history_embeddings"
ts_composite = true
ts_deps = [
"../page_image_service:build_ts",
"//third_party/lit/v3_0:build_ts",
"//third_party/polymer/v3_0:library",
"//ui/webui/resources/cr_components/history:build_ts",

@ -144,16 +144,42 @@ cr-url-list-item:hover + hr {
text-decoration: none;
}
:host([enable-answers_][in-side-panel]) .result-item {
padding: 4px 16px;
}
:host([enable-answers_]) .result-image {
align-items: center;
background: var(--color-history-embeddings-image-background,
var(--cr-fallback-color-neutral-container));
border-radius: 8px;
display: flex;
flex-shrink: 0;
justify-content: center;
height: 58px;
overflow: hidden;
width: 104px;
}
:host([enable-answers_][in-side-panel]) .result-image {
height: 40px;
width: 40px;
}
:host([enable-answers_][in-side-panel])
cr-history-embeddings-result-image:not([has-image]) {
display: none;
}
:host([enable-answers_]:not([in-side-panel])) .result-image .favicon {
display: none;
}
:host([enable-answers_][in-side-panel])
cr-history-embeddings-result-image[has-image] ~ .favicon {
display: none;
}
:host([enable-answers_]) .result-metadata {
display: flex;
flex: 1;
@ -195,6 +221,10 @@ cr-url-list-item:hover + hr {
width: 16px;
}
:host([enable-answers_][in-side-panel]) .result-url-and-favicon .favicon {
display: none;
}
.time {
margin-inline-start: 16px;
}
@ -366,8 +396,12 @@ cr-url-list-item:hover + hr {
on-click="onResultClick_" on-auxclick="onResultClick_"
on-contextmenu="onResultContextMenu_">
<div class="result-image">
<cr-history-embeddings-result-image search-result="[[item]]">
<cr-history-embeddings-result-image
in-side-panel="[[inSidePanel]]"
search-result="[[item]]">
</cr-history-embeddings-result-image>
<div class="favicon"
style$="background-image: [[getFavicon_(item)]]"></div>
</div>
<div class="result-metadata">
<div class="result-title">[[item.title]]</div>

@ -128,7 +128,7 @@ export class HistoryEmbeddingsElement extends HistoryEmbeddingsElementBase {
},
showRelativeTimes: {type: Boolean, value: false},
otherHistoryResultClicked: {type: Boolean, value: false},
inSidePanel: {type: Boolean, value: false},
inSidePanel: {type: Boolean, value: false, reflectToAttribute: true},
};
}

@ -23,6 +23,7 @@
height: 100%;
justify-content: center;
overflow: hidden;
position: relative;
width: 100%;
}
@ -30,6 +31,14 @@
fill: var(--illustration-color_);
}
#image {
height: 100%;
inset: 0;
object-fit: cover;
position: absolute;
width: 100%;
}
@media (prefers-color-scheme: dark) {
:host {
--gradient-end-color_: var(

@ -8,12 +8,14 @@ import type {HistoryEmbeddingsResultImageElement} from './result_image.ts';
export function getHtml(this: HistoryEmbeddingsResultImageElement) {
return html`
<svg aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
width="42" height="42" viewBox="0 0 42 42">
<path id="illustrationPath"
d="M15.1906 0.931046C6.16922 -2.98707 -2.98707 6.16923 0.931046 15.1906L1.57886 16.6822C2.77508 19.4364 2.77508 22.5636 1.57885 25.3178L0.931044 26.8094C-2.98707 35.8308 6.16923 44.9871 15.1906 41.069L16.6822 40.4211C19.4364 39.2249 22.5636 39.2249 25.3178 40.4211L26.8094 41.069C35.8308 44.9871 44.9871 35.8308 41.0689 26.8094L40.4211 25.3178C39.2249 22.5636 39.2249 19.4364 40.4211 16.6822L41.069 15.1906C44.9871 6.16922 35.8308 -2.98706 26.8094 0.931049L25.3178 1.57886C22.5635 2.77508 19.4364 2.77508 16.6822 1.57886L15.1906 0.931046Z">
</path>
</svg>
<img id="image" is="cr-auto-img" auto-src="${this.imageUrl_}"
.hidden="${!this.hasImage}" alt=""></img>
`;
}

@ -2,7 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '//resources/cr_elements/cr_auto_img/cr_auto_img.js';
import {PageImageServiceBrowserProxy} from '//resources/cr_components/page_image_service/browser_proxy.js';
import {ClientId as PageImageServiceClientId} from '//resources/cr_components/page_image_service/page_image_service.mojom-webui.js';
import {loadTimeData} from '//resources/js/load_time_data.js';
import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js';
import type {PropertyValues} from '//resources/lit/v3_0/lit.rollup.js';
import type {SearchResultItem} from './history_embeddings.mojom-webui.js';
import {getCss} from './result_image.css.js';
@ -23,11 +29,48 @@ export class HistoryEmbeddingsResultImageElement extends CrLitElement {
static override get properties() {
return {
hasImage: {type: Boolean, reflect: true},
imageUrl_: {type: String},
inSidePanel: {type: Boolean, reflect: true},
searchResult: {type: Object},
};
}
hasImage: boolean;
protected imageUrl_: string|null;
inSidePanel: boolean;
searchResult: SearchResultItem;
override willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (!loadTimeData.getBoolean('enableHistoryEmbeddingsImages')) {
return;
}
if (changedProperties.has('searchResult')) {
// Reset image state while it is being fetched.
this.imageUrl_ = null;
this.hasImage = false;
if (this.searchResult.isUrlKnownToSync) {
this.fetchImageForSearchResult_();
}
}
}
private async fetchImageForSearchResult_() {
const searchResultUrl = this.searchResult.url;
const {result} =
await PageImageServiceBrowserProxy.getInstance()
.handler.getPageImageUrl(
PageImageServiceClientId.Journeys, searchResultUrl,
{suggestImages: true, optimizationGuideImages: true});
if (result && searchResultUrl === this.searchResult.url) {
this.imageUrl_ = result.imageUrl.url;
this.hasImage = true;
}
}
}
declare global {