Skip to content

Commit 4bd78ff

Browse files
author
DavertMik
committed
added HTML util methods
1 parent 53fbe77 commit 4bd78ff

19 files changed

+8026
-8
lines changed

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ After big changes run linter: `npm run lint:fix`
3737
**Never use NodeJS**
3838
This application is only Bun
3939

40+
41+
4042
## Core Architecture
4143

4244
Explorbot uses layered architecture with AI-driven automation:
@@ -86,6 +88,12 @@ Application is built via React Ink with interactive TUI
8688
]
8789
```
8890

91+
DO NEVER REMOVE FROM COMPONENTS:
92+
93+
```
94+
import React from 'react';
95+
```
96+
8997
### User Input in TUI
9098

9199
There are application commands available in TUI

explorbot.config.example.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,35 @@ interface AIConfig {
6161
retryDelay: number;
6262
}
6363

64+
interface HtmlConfig {
65+
minimal?: {
66+
include?: string[];
67+
exclude?: string[];
68+
};
69+
combined?: {
70+
include?: string[];
71+
exclude?: string[];
72+
};
73+
text?: {
74+
include?: string[];
75+
exclude?: string[];
76+
};
77+
}
78+
79+
interface DirsConfig {
80+
knowledge: string;
81+
experience: string;
82+
output: string;
83+
}
84+
6485
interface ExplorbotConfig {
6586
playwright: PlaywrightConfig;
6687
app: AppConfig;
6788
output: OutputConfig;
6889
test: TestConfig;
6990
ai: AIConfig;
91+
html?: HtmlConfig;
92+
dirs?: DirsConfig;
7093
}
7194

7295
const config: ExplorbotConfig = {
@@ -155,6 +178,72 @@ const config: ExplorbotConfig = {
155178
retryAttempts: 3,
156179
retryDelay: 1000,
157180
},
181+
182+
// Optional HTML parsing configuration
183+
// Use CSS selectors to customize which elements are included in snapshots
184+
html: {
185+
// Minimal UI snapshot - keeps only interactive elements
186+
minimal: {
187+
include: [
188+
// Include elements with test IDs (not included by default)
189+
'[data-testid]',
190+
'[data-cy]',
191+
// Include custom interactive components
192+
'[role="toolbar"]',
193+
// Include elements with specific data attributes
194+
'div[data-id]',
195+
],
196+
exclude: [
197+
// Exclude cookie banners
198+
'#onetrust-consent-sdk',
199+
// Exclude notification toasts
200+
'.toast',
201+
'.notification',
202+
// Exclude loading spinners
203+
'.spinner',
204+
'.loading',
205+
],
206+
},
207+
// Combined snapshot - keeps interactive elements + meaningful text
208+
combined: {
209+
include: [
210+
// Include content areas with specific classes
211+
'.content',
212+
'.main-content',
213+
'[data-content]',
214+
// Include article content
215+
'article',
216+
'.article',
217+
],
218+
exclude: [
219+
// Exclude navigation menus
220+
'.nav-menu',
221+
'.navigation',
222+
'nav',
223+
// Exclude metadata
224+
'.metadata',
225+
'.meta',
226+
'time',
227+
],
228+
},
229+
// Text snapshot - converts to markdown text
230+
text: {
231+
include: [
232+
// Include specific text containers
233+
'.prose',
234+
'.markdown',
235+
'[data-markdown]',
236+
],
237+
exclude: [
238+
// Exclude code blocks (if not needed)
239+
'code',
240+
'pre',
241+
// Exclude small text
242+
'small',
243+
'.fine-print',
244+
],
245+
},
246+
},
158247
};
159248

160249
export default config;
@@ -165,4 +254,6 @@ export type {
165254
OutputConfig,
166255
TestConfig,
167256
AIConfig,
257+
HtmlConfig,
258+
DirsConfig,
168259
};

src/ai/researcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { z } from 'zod';
1414
const debugLog = createDebug('explorbot:researcher');
1515

1616
export class Research {
17-
expandDOMCalled: boolean = false;
17+
expandDOMCalled = false;
1818
}
1919

2020
export class Researcher {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
import { useState, useEffect } from 'react';
3+
import { Box, Text } from 'ink';
4+
import {
5+
addActivityListener,
6+
removeActivityListener,
7+
type ActivityEntry,
8+
} from '../activity.ts';
9+
10+
const ActivityPane: React.FC = () => {
11+
const [activity, setActivity] = useState<ActivityEntry | null>(null);
12+
const [animationState, setAnimationState] = useState(0);
13+
14+
useEffect(() => {
15+
const listener = (newActivity: ActivityEntry | null) => {
16+
setActivity(newActivity);
17+
};
18+
19+
addActivityListener(listener);
20+
21+
return () => {
22+
removeActivityListener(listener);
23+
};
24+
}, []);
25+
26+
useEffect(() => {
27+
if (!activity) return;
28+
29+
const interval = setInterval(() => {
30+
setAnimationState((prev) => (prev + 1) % 4);
31+
}, 500);
32+
33+
return () => clearInterval(interval);
34+
}, [activity]);
35+
36+
if (!activity) {
37+
return (
38+
<Box height={1} paddingX={1}>
39+
<Text dimColor>Done. Press [ESC] to enable input</Text>
40+
</Box>
41+
);
42+
}
43+
44+
const getActivityColor = (type: ActivityEntry['type']) => {
45+
switch (type) {
46+
case 'ai':
47+
return 'cyan';
48+
case 'action':
49+
return 'green';
50+
case 'navigation':
51+
return 'blue';
52+
default:
53+
return 'yellow';
54+
}
55+
};
56+
57+
const getDots = () => {
58+
return '.'.repeat(animationState);
59+
};
60+
61+
const isDimmed = animationState % 2 === 0;
62+
63+
return (
64+
<Box height={1} marginY={1} paddingX={1}>
65+
<Text color={getActivityColor(activity.type)} dimColor={isDimmed}>
66+
{activity.message}
67+
{getDots()}
68+
</Text>
69+
</Box>
70+
);
71+
};
72+
73+
export default ActivityPane;

src/components/LogPane.tsx.old

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React, { useState, useCallback, useEffect } from 'react';
2+
import dedent from 'dedent';
3+
import { marked } from 'marked';
4+
import { markedTerminal } from 'marked-terminal';
5+
6+
marked.use(markedTerminal());
7+
8+
import { Box, Text } from 'ink';
9+
import type { TaggedLogEntry, LogType } from '../utils/logger.js';
10+
import {
11+
registerLogPane,
12+
setVerboseMode,
13+
unregisterLogPane,
14+
} from '../utils/logger.js';
15+
16+
// marked.use(new markedTerminal());
17+
18+
type LogEntry = TaggedLogEntry;
19+
20+
interface LogPaneProps {
21+
verboseMode: boolean;
22+
}
23+
24+
const LogPane: React.FC<LogPaneProps> = ({ verboseMode }) => {
25+
const [logs, setLogs] = useState<TaggedLogEntry[]>([]);
26+
27+
const addLog = useCallback((logEntry: TaggedLogEntry) => {
28+
setLogs((prevLogs: TaggedLogEntry[]) => {
29+
// Skip duplicate consecutive logs
30+
if (prevLogs.length === 0) return [logEntry];
31+
32+
const lastLog = prevLogs[prevLogs.length - 1];
33+
if (
34+
lastLog.type === logEntry.type &&
35+
lastLog.content === logEntry.content &&
36+
// Check if it's within 1 second to avoid legitimate duplicates
37+
Math.abs(
38+
(lastLog.timestamp?.getTime() || 0) -
39+
(logEntry.timestamp?.getTime() || 0)
40+
) < 1000
41+
) {
42+
return prevLogs;
43+
}
44+
45+
return [...prevLogs, logEntry];
46+
});
47+
}, []);
48+
49+
useEffect(() => {
50+
registerLogPane(addLog);
51+
52+
return () => {
53+
unregisterLogPane(addLog);
54+
};
55+
}, []); // Empty dependency array to ensure this only runs once
56+
const getLogStyles = (type: LogType) => {
57+
switch (type) {
58+
case 'success':
59+
return { color: 'green' as const };
60+
case 'error':
61+
return { color: 'red' as const };
62+
case 'warning':
63+
return { color: 'yellow' as const };
64+
case 'debug':
65+
return { color: 'gray' as const, dimColor: true };
66+
case 'substep':
67+
return { color: 'gray' as const, dimColor: true };
68+
case 'step':
69+
return { color: 'cyan' as const, dimColor: true };
70+
case 'multiline':
71+
return { color: 'gray' as const, dimColor: true };
72+
default:
73+
return {};
74+
}
75+
};
76+
77+
const processLogContent = (content: string): string[] => {
78+
return content.split('\n').filter((line) => line.length > 0);
79+
};
80+
81+
const renderLogEntry = (log: TaggedLogEntry, index: number) => {
82+
// Skip debug logs when not in verbose mode AND DEBUG env var is not set
83+
const shouldShowDebug =
84+
verboseMode || Boolean(process.env.DEBUG?.includes('explorbot:'));
85+
if (log.type === 'debug' && !shouldShowDebug) {
86+
return null;
87+
}
88+
const styles = getLogStyles(log.type);
89+
90+
if (log.type === 'multiline') {
91+
return (
92+
<Box
93+
key={index}
94+
borderStyle="classic"
95+
marginY={1}
96+
borderColor="dim"
97+
height={15}
98+
overflow="hidden"
99+
>
100+
<Text>{dedent(marked.parse(String(log.content)).toString())}</Text>
101+
</Box>
102+
);
103+
}
104+
105+
const lines = processLogContent(String(log.content));
106+
107+
if (log.type === 'substep') {
108+
return (
109+
<Box key={index} flexDirection="column">
110+
{lines.map((line, lineIndex) => (
111+
<Text key={`${index}-${lineIndex}`} {...styles}>
112+
{lineIndex === 0 ? `> ${line}` : ` ${line}`}
113+
</Text>
114+
))}
115+
</Box>
116+
);
117+
}
118+
119+
if (log.type === 'step') {
120+
return (
121+
<Box key={index} flexDirection="column" paddingLeft={2}>
122+
{lines.map((line, lineIndex) => (
123+
<Text key={`${index}-${lineIndex}`} {...styles}>
124+
{line}
125+
</Text>
126+
))}
127+
</Box>
128+
);
129+
}
130+
131+
return (
132+
<Box key={index} flexDirection="column">
133+
{lines.map((line, lineIndex) => (
134+
<Text key={`${index}-${lineIndex}`} {...styles}>
135+
{line}
136+
</Text>
137+
))}
138+
</Box>
139+
);
140+
};
141+
142+
return (
143+
<Box flexDirection="column">
144+
{logs.map((log, index) => renderLogEntry(log, index)).filter(Boolean)}
145+
</Box>
146+
);
147+
};
148+
149+
export default LogPane;

0 commit comments

Comments
 (0)