textweb 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -20
- package/package.json +1 -1
- package/src/browser.js +52 -2
- package/src/cli.js +7 -6
- package/src/renderer.js +2 -40
- package/src/server.js +1 -2
- package/.env.example +0 -25
- package/src/apply.js +0 -745
- package/src/dashboard.js +0 -196
- package/src/llm.js +0 -220
- package/src/pipeline.js +0 -317
package/README.md
CHANGED
|
@@ -48,26 +48,6 @@ textweb --json https://example.com
|
|
|
48
48
|
|
|
49
49
|
~500 bytes. An LLM can read this, understand the layout, and say "click ref 9" to open the first link. No vision model needed.
|
|
50
50
|
|
|
51
|
-
## LLM Configuration
|
|
52
|
-
|
|
53
|
-
TextWeb's job application pipeline uses a local or remote LLM for freeform questions. Configure via `.env` file or environment variables:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
cp .env.example .env
|
|
57
|
-
# Edit .env with your settings
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
| Variable | Default | Description |
|
|
61
|
-
|----------|---------|-------------|
|
|
62
|
-
| `TEXTWEB_LLM_URL` | `http://localhost:1234/v1` | OpenAI-compatible API endpoint |
|
|
63
|
-
| `TEXTWEB_LLM_API_KEY` | *(empty)* | API key (optional for local LLMs) |
|
|
64
|
-
| `TEXTWEB_LLM_MODEL` | `google/gemma-3-4b` | Model name |
|
|
65
|
-
| `TEXTWEB_LLM_MAX_TOKENS` | `200` | Max response tokens |
|
|
66
|
-
| `TEXTWEB_LLM_TEMPERATURE` | `0.7` | Sampling temperature |
|
|
67
|
-
| `TEXTWEB_LLM_TIMEOUT` | `60000` | Request timeout (ms) |
|
|
68
|
-
|
|
69
|
-
Works with LM Studio, Ollama, OpenAI, or any OpenAI-compatible endpoint. No API key needed for local models.
|
|
70
|
-
|
|
71
51
|
## Integration Options
|
|
72
52
|
|
|
73
53
|
TextWeb works with any AI agent framework. Pick your integration:
|
package/package.json
CHANGED
package/src/browser.js
CHANGED
|
@@ -60,7 +60,7 @@ class AgentBrowser {
|
|
|
60
60
|
async click(ref) {
|
|
61
61
|
const el = this._getElement(ref);
|
|
62
62
|
await this.page.click(el.selector);
|
|
63
|
-
await this.
|
|
63
|
+
await this._settle();
|
|
64
64
|
return await this.snapshot();
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -103,7 +103,7 @@ class AgentBrowser {
|
|
|
103
103
|
|
|
104
104
|
async press(key) {
|
|
105
105
|
await this.page.keyboard.press(key);
|
|
106
|
-
await this.
|
|
106
|
+
await this._settle();
|
|
107
107
|
return await this.snapshot();
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -159,6 +159,56 @@ class AgentBrowser {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Get the current page URL
|
|
164
|
+
*/
|
|
165
|
+
getCurrentUrl() {
|
|
166
|
+
return this.page ? this.page.url() : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Find elements matching a CSS selector
|
|
171
|
+
* Returns array of {tag, text, selector, visible} objects
|
|
172
|
+
*/
|
|
173
|
+
async query(selector) {
|
|
174
|
+
if (!this.page) throw new Error('No page open. Call navigate() first.');
|
|
175
|
+
return await this.page.evaluate((sel) => {
|
|
176
|
+
const els = document.querySelectorAll(sel);
|
|
177
|
+
return Array.from(els).map((el, i) => ({
|
|
178
|
+
tag: el.tagName.toLowerCase(),
|
|
179
|
+
text: (el.textContent || '').trim().substring(0, 200),
|
|
180
|
+
selector: `${sel}:nth-child(${i + 1})`,
|
|
181
|
+
visible: el.offsetParent !== null,
|
|
182
|
+
href: el.href || null,
|
|
183
|
+
value: el.value || null,
|
|
184
|
+
}));
|
|
185
|
+
}, selector);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Take a screenshot (for debugging)
|
|
190
|
+
* @param {object} options - Playwright screenshot options (path, fullPage, type, etc.)
|
|
191
|
+
*/
|
|
192
|
+
async screenshot(options = {}) {
|
|
193
|
+
if (!this.page) throw new Error('No page open. Call navigate() first.');
|
|
194
|
+
return await this.page.screenshot({
|
|
195
|
+
fullPage: true,
|
|
196
|
+
type: 'png',
|
|
197
|
+
...options,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Wait for page to settle after an interaction.
|
|
203
|
+
* Races networkidle against a short timeout to avoid hanging on SPAs.
|
|
204
|
+
*/
|
|
205
|
+
async _settle() {
|
|
206
|
+
await Promise.race([
|
|
207
|
+
this.page.waitForLoadState('networkidle').catch(() => {}),
|
|
208
|
+
new Promise(r => setTimeout(r, 3000)),
|
|
209
|
+
]);
|
|
210
|
+
}
|
|
211
|
+
|
|
162
212
|
_getElement(ref) {
|
|
163
213
|
if (!this.lastResult) throw new Error('No snapshot. Navigate first.');
|
|
164
214
|
const el = this.lastResult.elements[ref];
|
package/src/cli.js
CHANGED
|
@@ -17,7 +17,6 @@ function parseArgs() {
|
|
|
17
17
|
json: false,
|
|
18
18
|
serve: false,
|
|
19
19
|
cols: 100,
|
|
20
|
-
rows: 30,
|
|
21
20
|
port: 3000,
|
|
22
21
|
help: false
|
|
23
22
|
};
|
|
@@ -48,7 +47,9 @@ function parseArgs() {
|
|
|
48
47
|
|
|
49
48
|
case '--rows':
|
|
50
49
|
case '-r':
|
|
51
|
-
|
|
50
|
+
// Deprecated: height is dynamic (grows to fit content). Ignored.
|
|
51
|
+
console.error('Warning: --rows is deprecated. Height is dynamic (grows to fit content).');
|
|
52
|
+
args[++i]; // consume the value
|
|
52
53
|
break;
|
|
53
54
|
|
|
54
55
|
case '--port':
|
|
@@ -85,7 +86,7 @@ USAGE:
|
|
|
85
86
|
|
|
86
87
|
OPTIONS:
|
|
87
88
|
--cols, -c <number> Grid width in characters (default: 100)
|
|
88
|
-
--rows, -r <number>
|
|
89
|
+
--rows, -r <number> (deprecated, height is dynamic)
|
|
89
90
|
--port, -p <number> Server port (default: 3000)
|
|
90
91
|
--interactive, -i Interactive REPL mode
|
|
91
92
|
--json, -j JSON output format
|
|
@@ -117,7 +118,7 @@ INTERACTIVE COMMANDS:
|
|
|
117
118
|
async function render(url, options) {
|
|
118
119
|
const browser = new AgentBrowser({
|
|
119
120
|
cols: options.cols,
|
|
120
|
-
|
|
121
|
+
|
|
121
122
|
headless: true
|
|
122
123
|
});
|
|
123
124
|
|
|
@@ -156,7 +157,7 @@ async function render(url, options) {
|
|
|
156
157
|
async function interactive(url, options) {
|
|
157
158
|
const browser = new AgentBrowser({
|
|
158
159
|
cols: options.cols,
|
|
159
|
-
|
|
160
|
+
|
|
160
161
|
headless: true
|
|
161
162
|
});
|
|
162
163
|
|
|
@@ -367,7 +368,7 @@ async function serve(options) {
|
|
|
367
368
|
|
|
368
369
|
const server = createServer({
|
|
369
370
|
cols: options.cols,
|
|
370
|
-
|
|
371
|
+
|
|
371
372
|
});
|
|
372
373
|
|
|
373
374
|
server.listen(options.port, () => {
|
package/src/renderer.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* Key design decisions:
|
|
8
8
|
* - Overflow > truncation (never lose information)
|
|
9
9
|
* - Measure actual font metrics from the page
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
10
|
+
* - Row-grouping layout (elements grouped by Y position)
|
|
11
|
+
* - Dynamic height (grows to fit all content)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -277,44 +277,6 @@ async function extractElements(page) {
|
|
|
277
277
|
});
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
/**
|
|
281
|
-
* Place text onto the grid, allowing overflow (never truncate).
|
|
282
|
-
* Text wraps to the next line at grid edge, continuing at the same start column.
|
|
283
|
-
*/
|
|
284
|
-
function placeText(grid, zGrid, z, row, col, text, cols, rows) {
|
|
285
|
-
let r = row;
|
|
286
|
-
let c = col;
|
|
287
|
-
|
|
288
|
-
for (let i = 0; i < text.length; i++) {
|
|
289
|
-
// Grow grid vertically if needed (overflow — don't lose data)
|
|
290
|
-
while (r >= grid.length) {
|
|
291
|
-
grid.push(Array(cols).fill(' '));
|
|
292
|
-
zGrid.push(Array(cols).fill(-1));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (c >= cols) {
|
|
296
|
-
// Wrap to next line at original column position
|
|
297
|
-
r++;
|
|
298
|
-
c = col;
|
|
299
|
-
while (r >= grid.length) {
|
|
300
|
-
grid.push(Array(cols).fill(' '));
|
|
301
|
-
zGrid.push(Array(cols).fill(-1));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const ch = text[i];
|
|
306
|
-
if (ch === '\n') { r++; c = col; continue; }
|
|
307
|
-
|
|
308
|
-
if (c >= 0 && c < cols && z >= zGrid[r][c]) {
|
|
309
|
-
grid[r][c] = ch;
|
|
310
|
-
zGrid[r][c] = z;
|
|
311
|
-
}
|
|
312
|
-
c++;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return r; // Return last row written to (useful for tracking grid growth)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
280
|
/**
|
|
319
281
|
* Detect row boundaries — groups of elements that share the same Y position
|
|
320
282
|
* This prevents text from different elements on the same visual line from overlapping
|
package/src/server.js
CHANGED
|
@@ -10,7 +10,7 @@ class TextWebServer {
|
|
|
10
10
|
constructor(options = {}) {
|
|
11
11
|
this.options = {
|
|
12
12
|
cols: options.cols || 100,
|
|
13
|
-
|
|
13
|
+
// rows is deprecated — height is dynamic
|
|
14
14
|
timeout: options.timeout || 30000,
|
|
15
15
|
...options
|
|
16
16
|
};
|
|
@@ -33,7 +33,6 @@ class TextWebServer {
|
|
|
33
33
|
if (!this.browser) {
|
|
34
34
|
this.browser = new AgentBrowser({
|
|
35
35
|
cols: this.options.cols,
|
|
36
|
-
rows: this.options.rows,
|
|
37
36
|
headless: true,
|
|
38
37
|
timeout: this.options.timeout
|
|
39
38
|
});
|
package/.env.example
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# TextWeb LLM Configuration
|
|
2
|
-
# Copy this to .env and fill in your values
|
|
3
|
-
|
|
4
|
-
# Base URL for any OpenAI-compatible API
|
|
5
|
-
# Default: http://localhost:1234/v1 (LM Studio)
|
|
6
|
-
# Examples:
|
|
7
|
-
# https://api.openai.com/v1
|
|
8
|
-
# https://api.anthropic.com/v1
|
|
9
|
-
# http://localhost:11434/v1 (Ollama)
|
|
10
|
-
TEXTWEB_LLM_URL=http://localhost:1234/v1
|
|
11
|
-
|
|
12
|
-
# API key (optional for local LLMs like LM Studio/Ollama)
|
|
13
|
-
TEXTWEB_LLM_API_KEY=
|
|
14
|
-
|
|
15
|
-
# Model name
|
|
16
|
-
TEXTWEB_LLM_MODEL=google/gemma-3-4b
|
|
17
|
-
|
|
18
|
-
# Max tokens for responses (default: 200)
|
|
19
|
-
TEXTWEB_LLM_MAX_TOKENS=200
|
|
20
|
-
|
|
21
|
-
# Temperature (default: 0.7)
|
|
22
|
-
TEXTWEB_LLM_TEMPERATURE=0.7
|
|
23
|
-
|
|
24
|
-
# Request timeout in milliseconds (default: 60000)
|
|
25
|
-
TEXTWEB_LLM_TIMEOUT=60000
|