Skip to content

Commit 9cdcc21

Browse files
Page context follow ups (#1936)
* Only run in the sidebar * Add to the windows bundle * Add duck-ai-listener to windows * Add replaceState listener * Move to non-isolated so we can listen to URL changes * Detect duck.ai better * Ignore URL listeners for frames and duckai * Ignore duck:// urls for collection * Remove error * Move logging * lint fix * Add visibilitychange listener * Remove collect subscription * Clean up mutation listener to stop after cache is gone, simplify cache logic * Convert text content to simple markdown representation * Add remote config wrappers * Truncate page context at the node collection and add flag Add truncation in page warning. * Add prompt explict markers * Clean up * Ensure mutation observer is initialized correctly * Rename message bridge logger * Remove arg * Change prompt a little * Lint fixes * Fix unit test not to be hardcoded
1 parent 3f1a9b8 commit 9cdcc21

File tree

8 files changed

+331
-217
lines changed

8 files changed

+331
-217
lines changed

injected/src/content-feature.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,40 @@ export default class ContentFeature extends ConfigFeature {
6262
return this.args?.debug || false;
6363
}
6464

65+
get shouldLog() {
66+
return this.isDebug;
67+
}
68+
69+
/**
70+
* Logging utility for this feature (Stolen some inspo from DuckPlayer logger, will unify in the future)
71+
*/
72+
get log() {
73+
const shouldLog = this.shouldLog;
74+
const prefix = `${this.name.padEnd(20, ' ')} |`;
75+
76+
return {
77+
// These are getters to have the call site be the reported line number.
78+
get info() {
79+
if (!shouldLog) {
80+
return () => {};
81+
}
82+
return console.log.bind(console, prefix);
83+
},
84+
get warn() {
85+
if (!shouldLog) {
86+
return () => {};
87+
}
88+
return console.warn.bind(console, prefix);
89+
},
90+
get error() {
91+
if (!shouldLog) {
92+
return () => {};
93+
}
94+
return console.error.bind(console, prefix);
95+
},
96+
};
97+
}
98+
6599
get desktopModeEnabled() {
66100
return this.args?.desktopModeEnabled || false;
67101
}

injected/src/features.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const otherFeatures = /** @type {const} */ ([
3737
/** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */
3838
/** @type {Record<string, FeatureName[]>} */
3939
export const platformSupport = {
40-
apple: ['webCompat', 'duckPlayerNative', ...baseFeatures, 'duckAiListener'],
40+
apple: ['webCompat', 'duckPlayerNative', ...baseFeatures, 'duckAiListener', 'pageContext'],
4141
'apple-isolated': [
4242
'duckPlayer',
4343
'duckPlayerNative',
@@ -46,7 +46,6 @@ export const platformSupport = {
4646
'clickToLoad',
4747
'messageBridge',
4848
'favicon',
49-
'pageContext',
5049
],
5150
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge'],
5251
'android-broker-protection': ['brokerProtection'],
@@ -72,6 +71,8 @@ export const platformSupport = {
7271
'breakageReporting',
7372
'messageBridge',
7473
'webCompat',
74+
'pageContext',
75+
'duckAiListener',
7576
],
7677
firefox: ['cookie', ...baseFeatures, 'clickToLoad'],
7778
chrome: ['cookie', ...baseFeatures, 'clickToLoad'],

injected/src/features/duck-ai-listener.js

Lines changed: 58 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ContentFeature from '../content-feature.js';
2-
import { isBeingFramed, isDuckAi } from '../utils.js';
2+
import { isBeingFramed, isDuckAiSidebar } from '../utils.js';
33

44
/**
55
* Duck AI Listener Feature
@@ -39,37 +39,6 @@ export default class DuckAiListener extends ContentFeature {
3939
/** @type {HTMLButtonElement | null} */
4040
sendButton = null;
4141

42-
get shouldLog() {
43-
return this.isDebug;
44-
}
45-
46-
/**
47-
* Logging utility for this feature
48-
*/
49-
get log() {
50-
const shouldLog = this.shouldLog;
51-
return {
52-
get info() {
53-
if (!shouldLog) {
54-
return () => {};
55-
}
56-
return console.log;
57-
},
58-
get warn() {
59-
if (!shouldLog) {
60-
return () => {};
61-
}
62-
return console.warn;
63-
},
64-
get error() {
65-
if (!shouldLog) {
66-
return () => {};
67-
}
68-
return console.error;
69-
},
70-
};
71-
}
72-
7342
init() {
7443
// Only activate on duckduckgo.com
7544
if (!this.shouldActivate()) {
@@ -97,7 +66,7 @@ export default class DuckAiListener extends ContentFeature {
9766
if (isBeingFramed()) {
9867
return false;
9968
}
100-
return isDuckAi();
69+
return isDuckAiSidebar();
10170
}
10271

10372
/**
@@ -410,7 +379,7 @@ export default class DuckAiListener extends ContentFeature {
410379
font-size: 12px;
411380
color: rgb(102, 102, 102);
412381
`;
413-
subtitle.textContent = 'Page Content';
382+
subtitle.textContent = this.pageData.truncated ? 'Page Content (Truncated)' : 'Page Content';
414383

415384
contentInfo.appendChild(title);
416385
contentInfo.appendChild(subtitle);
@@ -429,6 +398,24 @@ export default class DuckAiListener extends ContentFeature {
429398
cursor: pointer;
430399
`;
431400

401+
// Add warning icon if content is truncated
402+
const warningIcon = document.createElement('div');
403+
if (this.pageData.truncated) {
404+
warningIcon.innerHTML = `
405+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
406+
<path d="M8 1.5L15 14H1L8 1.5Z" stroke="#ff6b35" stroke-width="1.5" fill="none"/>
407+
<path d="M8 6V9M8 11H8.01" stroke="#ff6b35" stroke-width="1.5" stroke-linecap="round"/>
408+
</svg>
409+
`;
410+
warningIcon.style.cssText = `
411+
flex-shrink: 0;
412+
color: #ff6b35;
413+
cursor: pointer;
414+
margin-left: 4px;
415+
`;
416+
warningIcon.title = 'Content has been truncated due to size limits';
417+
}
418+
432419
// Add dark mode support
433420
if (this.isDarkMode()) {
434421
this.contextChip.style.background = 'rgba(255, 255, 255, 0.1)';
@@ -443,6 +430,9 @@ export default class DuckAiListener extends ContentFeature {
443430
this.contextChip.appendChild(icon);
444431
this.contextChip.appendChild(contentInfo);
445432
this.contextChip.appendChild(infoIcon);
433+
if (this.pageData.truncated) {
434+
this.contextChip.appendChild(warningIcon);
435+
}
446436

447437
this.log.info('Context chip assembled, about to insert into DOM');
448438

@@ -594,6 +584,11 @@ export default class DuckAiListener extends ContentFeature {
594584
this.pageData = pageDataParsed;
595585
this.globalPageContext = pageDataParsed.content;
596586

587+
// Check for truncated content and warn user
588+
if (pageDataParsed.truncated) {
589+
this.log.warn('Page content has been truncated due to size limits');
590+
}
591+
597592
this.createContextChip();
598593
this.setupMessageInterception();
599594
}
@@ -741,6 +736,7 @@ export default class DuckAiListener extends ContentFeature {
741736
setupValuePropertyDescriptor(textarea) {
742737
// Store the original value property descriptor
743738
const originalDescriptor = Object.getOwnPropertyDescriptor(textarea, 'value');
739+
this.randomNumber = window.crypto?.randomUUID?.() || Math.floor(Math.random() * 1000);
744740

745741
// Override the value property using arrow functions to capture this context
746742
Object.defineProperty(textarea, 'value', {
@@ -749,9 +745,36 @@ export default class DuckAiListener extends ContentFeature {
749745
if (originalDescriptor && originalDescriptor.get) {
750746
const currentValue = originalDescriptor.get.call(textarea) || '';
751747
const pageContext = this.globalPageContext || '';
748+
const randomNumber = this.randomNumber;
749+
const instructions =
750+
this.getFeatureSetting('instructions') ||
751+
`
752+
You are a helpful assistant that can answer questions and help with tasks.
753+
Do not include prompt, page-title, page-context, or instructions tags in your response.
754+
Answer the prompt using the page-title, and page-context ONLY if it's relevant to answering the prompt.`;
752755

753756
if (pageContext && currentValue) {
754-
return `${currentValue}\n\n---\n\nPage Context:\n${pageContext}`;
757+
const truncatedWarning = this.pageData?.truncated ? ' (Content was truncated due to size limits)\n' : '\n';
758+
return `Prompt:
759+
<prompt-${randomNumber}>
760+
${currentValue}
761+
</prompt-${randomNumber}>
762+
763+
Instructions:
764+
<instructions-${randomNumber}>
765+
${instructions}
766+
</instructions-${randomNumber}>
767+
768+
Page Title:
769+
<page-title-${randomNumber}>
770+
${this.pageData.title}
771+
</page-title-${randomNumber}>
772+
773+
Page Context:
774+
<page-context-${randomNumber}>
775+
${pageContext}
776+
${truncatedWarning}
777+
</page-context-${randomNumber}>`;
755778
}
756779

757780
return currentValue;
@@ -766,42 +789,4 @@ export default class DuckAiListener extends ContentFeature {
766789
configurable: true,
767790
});
768791
}
769-
770-
/**
771-
* Set textarea value in a React-compatible way
772-
* Based on the approach from broker-protection/actions/fill-form.js
773-
* @param {HTMLTextAreaElement} textarea - The textarea element
774-
* @param {string} value - The value to set
775-
*/
776-
setReactTextAreaValue(textarea, value) {
777-
try {
778-
// Access the original setter to bypass React's controlled component behavior
779-
const originalSet = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
780-
781-
if (!originalSet || typeof originalSet.call !== 'function') {
782-
this.log.warn('Cannot access original value setter, falling back to direct assignment');
783-
textarea.value = value;
784-
return;
785-
}
786-
787-
// Set the textarea value using the original setter and trigger React events
788-
textarea.dispatchEvent(new Event('keydown', { bubbles: true }));
789-
originalSet.call(textarea, value);
790-
791-
const events = [
792-
new Event('input', { bubbles: true }),
793-
new Event('keyup', { bubbles: true }),
794-
new Event('change', { bubbles: true }),
795-
];
796-
797-
// Dispatch events twice to ensure React picks up the change
798-
events.forEach((ev) => textarea.dispatchEvent(ev));
799-
originalSet.call(textarea, value);
800-
events.forEach((ev) => textarea.dispatchEvent(ev));
801-
} catch (error) {
802-
this.log.error('Error setting React textarea value:', error);
803-
// Fallback to direct assignment
804-
textarea.value = value;
805-
}
806-
}
807792
}

injected/src/features/message-bridge.js

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class MessageBridge extends ContentFeature {
6969
* @param {{name: string; id: string} & Record<string, any>} incoming
7070
*/
7171
const reply = (incoming) => {
72-
if (!args.messageSecret) return this.log('ignoring because args.messageSecret was absent');
72+
if (!args.messageSecret) return this.log.info('ignoring because args.messageSecret was absent');
7373
const eventName = appendToken(incoming.name + '-' + incoming.id);
7474
const event = new captured.CustomEvent(eventName, { detail: incoming });
7575
captured.dispatchEvent(event);
@@ -82,20 +82,20 @@ export class MessageBridge extends ContentFeature {
8282
*/
8383
const accept = (ClassType, callback) => {
8484
captured.addEventListener(appendToken(ClassType.NAME), (/** @type {CustomEvent<unknown>} */ e) => {
85-
this.log(`${ClassType.NAME}`, JSON.stringify(e.detail));
85+
this.log.info(`${ClassType.NAME}`, JSON.stringify(e.detail));
8686
const instance = ClassType.create(e.detail);
8787
if (instance) {
8888
callback(instance);
8989
} else {
90-
this.log('Failed to create an instance');
90+
this.log.info('Failed to create an instance');
9191
}
9292
});
9393
};
9494

9595
/**
9696
* These are all the messages we accept from the page-world.
9797
*/
98-
this.log(`bridge is installing...`);
98+
this.log.info(`bridge is installing...`);
9999
accept(InstallProxy, (install) => {
100100
this.installProxyFor(install, args.messagingConfig, reply);
101101
});
@@ -115,17 +115,17 @@ export class MessageBridge extends ContentFeature {
115115
*/
116116
installProxyFor(install, config, reply) {
117117
const { id, featureName } = install;
118-
if (this.proxies.has(featureName)) return this.log('ignoring `installProxyFor` because it exists', featureName);
118+
if (this.proxies.has(featureName)) return this.log.info('ignoring `installProxyFor` because it exists', featureName);
119119
const allowed = this.getFeatureSettingEnabled(featureName);
120120
if (!allowed) {
121-
return this.log('not installing proxy, because', featureName, 'was not enabled');
121+
return this.log.info('not installing proxy, because', featureName, 'was not enabled');
122122
}
123123

124124
const ctx = { ...this.messaging.messagingContext, featureName };
125125
const messaging = new Messaging(ctx, config);
126126
this.proxies.set(featureName, messaging);
127127

128-
this.log('did install proxy for ', featureName);
128+
this.log.info('did install proxy for ', featureName);
129129
reply(new DidInstall({ id }));
130130
}
131131

@@ -137,9 +137,9 @@ export class MessageBridge extends ContentFeature {
137137
const { id, featureName, method, params } = request;
138138

139139
const proxy = this.proxies.get(featureName);
140-
if (!proxy) return this.log('proxy was not installed for ', featureName);
140+
if (!proxy) return this.log.info('proxy was not installed for ', featureName);
141141

142-
this.log('will proxy', request);
142+
this.log.info('will proxy', request);
143143

144144
try {
145145
const result = await proxy.request(method, params);
@@ -168,9 +168,9 @@ export class MessageBridge extends ContentFeature {
168168
proxySubscription(subscription, reply) {
169169
const { id, featureName, subscriptionName } = subscription;
170170
const proxy = this.proxies.get(subscription.featureName);
171-
if (!proxy) return this.log('proxy was not installed for', featureName);
171+
if (!proxy) return this.log.info('proxy was not installed for', featureName);
172172

173-
this.log('will setup subscription', subscription);
173+
this.log.info('will setup subscription', subscription);
174174

175175
// cleanup existing subscriptions first
176176
const prev = this.subscriptions.get(id);
@@ -196,7 +196,7 @@ export class MessageBridge extends ContentFeature {
196196
*/
197197
removeSubscription(id) {
198198
const unsubscribe = this.subscriptions.get(id);
199-
this.log(`will remove subscription`, id);
199+
this.log.info(`will remove subscription`, id);
200200
unsubscribe?.();
201201
this.subscriptions.delete(id);
202202
}
@@ -206,21 +206,12 @@ export class MessageBridge extends ContentFeature {
206206
*/
207207
proxyNotification(notification) {
208208
const proxy = this.proxies.get(notification.featureName);
209-
if (!proxy) return this.log('proxy was not installed for', notification.featureName);
209+
if (!proxy) return this.log.info('proxy was not installed for', notification.featureName);
210210

211-
this.log('will proxy notification', notification);
211+
this.log.info('will proxy notification', notification);
212212
proxy.notify(notification.method, notification.params);
213213
}
214214

215-
/**
216-
* @param {Parameters<console['log']>} args
217-
*/
218-
log(...args) {
219-
if (this.isDebug) {
220-
console.log('[isolated]', ...args);
221-
}
222-
}
223-
224215
load(_args) {}
225216
}
226217

0 commit comments

Comments
 (0)