Add controls inventory and truncate long data: URIs in dumps.
Agents hitting Outline-style SPAs need visible vs hidden control lists, and inlined base64 images were blowing up AI prompts and network copy.
This commit is contained in:
@@ -34,6 +34,7 @@ const session = new ExtractorSession(page); // attach BEFORE navigating
|
||||
await page.goto('https://example.com', { waitUntil: 'networkidle' });
|
||||
|
||||
console.log(await session.buildAiPrompt()); // or session.extractMarkdown('#main')
|
||||
console.log(await session.inventoryControls('#main')); // visible/hidden buttons & aria-labels
|
||||
console.log(session.getStore()); // { console, errors, network }
|
||||
```
|
||||
|
||||
|
||||
@@ -5,5 +5,6 @@ export {
|
||||
type ErrorEntry,
|
||||
type NetworkEntry,
|
||||
type ExtractedMarkdown,
|
||||
type InterestingControl,
|
||||
} from './session.js';
|
||||
export { readSharedJs } from './loadJs.js';
|
||||
|
||||
@@ -118,18 +118,44 @@ return extractMarkdown(el);
|
||||
`;
|
||||
}
|
||||
|
||||
function buildInventoryScript(selector: string | null | undefined): string {
|
||||
const domJs = readSharedJs('dom.js');
|
||||
const selJson = JSON.stringify(selector || '');
|
||||
return `
|
||||
(() => {
|
||||
${domJs}
|
||||
let el = null;
|
||||
const sel = ${selJson};
|
||||
if (sel) { try { el = document.querySelector(sel); } catch (_) { el = null; } }
|
||||
if (!el) el = document.body;
|
||||
return inventoryInterestingControls(el);
|
||||
})()
|
||||
`;
|
||||
}
|
||||
|
||||
export interface InterestingControl {
|
||||
tag: string;
|
||||
role: string;
|
||||
ariaLabel: string;
|
||||
text: string;
|
||||
visible: boolean;
|
||||
selector: string;
|
||||
}
|
||||
|
||||
function buildPromptScript(
|
||||
meta: Pick<ExtractedMarkdown, 'url' | 'title' | 'ts' | 'selector'>,
|
||||
markdown: string,
|
||||
store: CaptureStore,
|
||||
maxChars: number | undefined,
|
||||
controls?: InterestingControl[],
|
||||
): string {
|
||||
const promptJs = readSharedJs('prompt.js');
|
||||
const maxCharsJs = maxChars === undefined ? 'undefined' : JSON.stringify(maxChars);
|
||||
const controlsJs = JSON.stringify(controls ?? []);
|
||||
return `
|
||||
(() => {
|
||||
${promptJs}
|
||||
return buildAIPrompt(${JSON.stringify(meta)}, ${JSON.stringify(markdown)}, ${JSON.stringify(store)}, ${maxCharsJs});
|
||||
return buildAIPrompt(${JSON.stringify(meta)}, ${JSON.stringify(markdown)}, ${JSON.stringify(store)}, ${maxCharsJs}, ${controlsJs});
|
||||
})()
|
||||
`;
|
||||
}
|
||||
@@ -191,15 +217,28 @@ export class ExtractorSession {
|
||||
};
|
||||
}
|
||||
|
||||
/** Inventory buttons / menuitems / aria-labels under selector (default body). */
|
||||
async inventoryControls(selector?: string | null): Promise<InterestingControl[]> {
|
||||
const list = (await this.page.evaluate(buildInventoryScript(selector))) as InterestingControl[];
|
||||
return Array.isArray(list) ? list : [];
|
||||
}
|
||||
|
||||
async buildAiPrompt(selector?: string | null, maxChars?: number): Promise<string> {
|
||||
const extracted = await this.extractMarkdown(selector);
|
||||
const controls = await this.inventoryControls(selector);
|
||||
const meta = {
|
||||
url: extracted.url,
|
||||
title: extracted.title,
|
||||
ts: extracted.ts,
|
||||
selector: extracted.selector,
|
||||
};
|
||||
const script = buildPromptScript(meta, extracted.markdown, this.getStore(), maxChars);
|
||||
const script = buildPromptScript(
|
||||
meta,
|
||||
extracted.markdown,
|
||||
this.getStore(),
|
||||
maxChars,
|
||||
controls,
|
||||
);
|
||||
return (await this.page.evaluate(script)) as string;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ describe('ExtractorSession', () => {
|
||||
expect(full.markdown).toContain('**world**');
|
||||
expect(full.markdown).toContain('[docs](/docs)');
|
||||
expect(full.markdown).toContain('- Alpha');
|
||||
expect(full.markdown).toContain('[tiny-jpeg](data:image/jpeg;base64,…[');
|
||||
expect(full.markdown).toContain('bytes truncated])');
|
||||
expect(full.markdown).not.toMatch(/4AAQSkZJRgABAQAAAQABAAD/);
|
||||
|
||||
const scoped = await session.extractMarkdown('#main-content');
|
||||
expect(scoped.selector).toBe('#main-content');
|
||||
@@ -95,11 +98,28 @@ describe('ExtractorSession', () => {
|
||||
expect(prompt.startsWith('# Page Context')).toBe(true);
|
||||
expect(prompt).toContain('## Page Content');
|
||||
expect(prompt).toContain('Hello **world**');
|
||||
expect(prompt).toContain('## Interesting Controls');
|
||||
expect(prompt).toContain('[visible]');
|
||||
expect(prompt).toContain('Document options');
|
||||
expect(prompt).toContain('### Hidden');
|
||||
expect(/JavaScript Errors|Console Errors/.test(prompt)).toBe(true);
|
||||
expect(prompt).toContain('_Extracted by Context Extractor_');
|
||||
});
|
||||
});
|
||||
|
||||
it('inventories visible and hidden controls', async () => {
|
||||
await withPage(async (page) => {
|
||||
const session = new ExtractorSession(page);
|
||||
await page.goto(FIXTURE_URL, { waitUntil: 'domcontentloaded' });
|
||||
const controls = await session.inventoryControls('#main-content');
|
||||
const docs = controls.filter((c) => c.ariaLabel === 'Document options');
|
||||
expect(docs.length).toBe(2);
|
||||
expect(docs.some((c) => c.visible)).toBe(true);
|
||||
expect(docs.some((c) => !c.visible)).toBe(true);
|
||||
expect(controls.some((c) => c.ariaLabel === 'Upload file' && c.visible)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('clears the capture store, selectively and fully', async () => {
|
||||
await withPage(async (page) => {
|
||||
const session = new ExtractorSession(page);
|
||||
|
||||
Reference in New Issue
Block a user