Target Job AI Fixed. Now works and find leads
Build & Auto-Repair / build (push) Has been cancelled

This commit is contained in:
JCBSComputer
2026-07-03 16:09:50 +02:00
parent cfc69d47d3
commit dbec6c0851
17 changed files with 159 additions and 86 deletions
+14 -6
View File
@@ -3,6 +3,13 @@ const { chromium } = require('playwright');
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
const PORT = process.env.PORT || 3007; const PORT = process.env.PORT || 3007;
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
@@ -36,19 +43,20 @@ app.post('/scrape/indeed', async (req, res) => {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 }); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
await page.waitForTimeout(2000); await page.waitForTimeout(2000);
await page.waitForTimeout(3000);
const items = await page.evaluate(() => { const items = await page.evaluate(() => {
const cards = document.querySelectorAll('.job_seen_beacon'); const cards = document.querySelectorAll('.job_seen_beacon, [class*="card"], [class*="result"], li');
return Array.from(cards).slice(0, 8).map(card => { return Array.from(cards).slice(0, 10).map(card => {
const titleEl = card.querySelector('.jcs-JobTitle'); const titleEl = card.querySelector('.jcs-JobTitle, a[class*="title"], a[class*="job"], h2 a');
const companyEl = card.querySelector('[data-testid="inlineHeader-companyName"]'); const companyEl = card.querySelector('[data-testid="inlineHeader-companyName"], [class*="company"], [class*="comp"]');
const snippetEl = card.querySelector('.job-snippet'); const snippetEl = card.querySelector('.job-snippet, [class*="snippet"], [class*="summary"]');
return { return {
title: titleEl?.textContent?.trim() || '', title: titleEl?.textContent?.trim() || '',
url: titleEl?.href || '', url: titleEl?.href || '',
company: companyEl?.textContent?.trim() || '', company: companyEl?.textContent?.trim() || '',
snippet: snippetEl?.textContent?.trim()?.slice(0, 200) || '', snippet: snippetEl?.textContent?.trim()?.slice(0, 200) || '',
}; };
}); }).filter(j => j.title);
}); });
for (const item of items) { for (const item of items) {
+7 -9
View File
@@ -558,12 +558,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.61.0", "version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.61.0" "playwright-core": "1.61.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -576,10 +575,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.61.0", "version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
+17 -16
View File
@@ -23831,6 +23831,7 @@ var init_harTracer = __esm({
this._pageEntries = []; this._pageEntries = [];
this._eventListeners = []; this._eventListeners = [];
this._started = false; this._started = false;
this._omitWebSocketFrames = true;
this._context = context2; this._context = context2;
this._page = page; this._page = page;
this._delegate = delegate; this._delegate = delegate;
@@ -23847,6 +23848,9 @@ var init_harTracer = __esm({
this._pageEntrySymbol = Symbol("pageHarEntry"); this._pageEntrySymbol = Symbol("pageHarEntry");
this._baseURL = context2 instanceof APIRequestContext ? context2._defaultOptions().baseURL : context2._options.baseURL; this._baseURL = context2 instanceof APIRequestContext ? context2._defaultOptions().baseURL : context2._options.baseURL;
} }
setOmitWebSocketFrames(omitWebSocketFrames) {
this._omitWebSocketFrames = omitWebSocketFrames;
}
start(options) { start(options) {
if (this._started) if (this._started)
return; return;
@@ -24157,7 +24161,7 @@ var init_harTracer = __esm({
harEntry._resourceType = "websocket"; harEntry._resourceType = "websocket";
let sha1 = void 0; let sha1 = void 0;
const recordMessage = (type3, opcode, data, wallTimeMs) => { const recordMessage = (type3, opcode, data, wallTimeMs) => {
if (this._options.omitWebSocketFrames) if (this._omitWebSocketFrames)
return; return;
const message = { type: type3, time: this._options.omitTiming ? -1 : wallTimeMs, opcode, data }; const message = { type: type3, time: this._options.omitTiming ? -1 : wallTimeMs, opcode, data };
if (this._options.content === "embed") { if (this._options.content === "embed") {
@@ -24441,9 +24445,9 @@ var init_harRecorder = __esm({
includeTraceInfo: false, includeTraceInfo: false,
recordRequestOverrides: true, recordRequestOverrides: true,
waitForContentOnStop: true, waitForContentOnStop: true,
urlFilter: urlFilterRe ?? options.urlGlob, urlFilter: urlFilterRe ?? options.urlGlob
omitWebSocketFrames: !!process.env.PLAYWRIGHT_HAR_NO_WEBSOCKET_FRAMES
}); });
this._tracer.setOmitWebSocketFrames(!!process.env.PLAYWRIGHT_HAR_NO_WEBSOCKET_FRAMES);
this._tracer.start({ omitScripts: false }); this._tracer.start({ omitScripts: false });
} }
onEntryStarted(entry) { onEntryStarted(entry) {
@@ -24652,8 +24656,7 @@ var init_tracing = __esm({
content: "attach", content: "attach",
includeTraceInfo: true, includeTraceInfo: true,
recordRequestOverrides: false, recordRequestOverrides: false,
waitForContentOnStop: false, waitForContentOnStop: false
omitWebSocketFrames: !!process.env.PLAYWRIGHT_TRACING_NO_WEBSOCKET_FRAMES
}); });
const testIdAttributeName2 = "selectors" in context2 ? context2.selectors().testIdAttributeName() : void 0; const testIdAttributeName2 = "selectors" in context2 ? context2.selectors().testIdAttributeName() : void 0;
this._contextCreatedEvent = { this._contextCreatedEvent = {
@@ -24747,6 +24750,7 @@ var init_tracing = __esm({
); );
if (this._state.options.screenshots) if (this._state.options.screenshots)
this._startScreencast(); this._startScreencast();
this._harTracer.setOmitWebSocketFrames(!!process.env.PLAYWRIGHT_TRACING_NO_WEBSOCKET_FRAMES);
if (this._state.options.snapshots) if (this._state.options.snapshots)
await this._snapshotter?.start(progress2); await this._snapshotter?.start(progress2);
return { traceName: this._state.traceName }; return { traceName: this._state.traceName };
@@ -24896,6 +24900,7 @@ var init_tracing = __esm({
eventsHelper.removeEventListeners(this._eventListeners); eventsHelper.removeEventListeners(this._eventListeners);
if (this._state.options.screenshots) if (this._state.options.screenshots)
this._stopScreencast(); this._stopScreencast();
this._harTracer.setOmitWebSocketFrames(true);
if (this._state.options.snapshots) if (this._state.options.snapshots)
this._snapshotter?.stop(); this._snapshotter?.stop();
this.flushHarEntries(); this.flushHarEntries();
@@ -35989,7 +35994,7 @@ var init_crNetworkManager = __esm({
} }
_onWebSocketWillSendHandshakeRequest(event) { _onWebSocketWillSendHandshakeRequest(event) {
const wallTimeMs = event.wallTime * 1e3; const wallTimeMs = event.wallTime * 1e3;
this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp); this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp * 1e3);
this._page.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers, "\n"), wallTimeMs); this._page.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers, "\n"), wallTimeMs);
} }
_onWebSocketClosed(event) { _onWebSocketClosed(event) {
@@ -35997,7 +36002,7 @@ var init_crNetworkManager = __esm({
this._page.frameManager.webSocketClosed(event.requestId); this._page.frameManager.webSocketClosed(event.requestId);
} }
_timestampToWallTimeMsForWebSocket(requestId, timestamp) { _timestampToWallTimeMsForWebSocket(requestId, timestamp) {
return this._timestampBaselineForWebSocket.get(requestId) + timestamp; return this._timestampBaselineForWebSocket.get(requestId) + timestamp * 1e3;
} }
_maybeUpdateRequestSession(sessionInfo, request2) { _maybeUpdateRequestSession(sessionInfo, request2) {
if (request2.session !== sessionInfo.session && !sessionInfo.isMain && (request2._documentId === request2._requestId || sessionInfo.workerFrame)) if (request2.session !== sessionInfo.session && !sessionInfo.isMain && (request2._documentId === request2._requestId || sessionInfo.workerFrame))
@@ -44350,15 +44355,11 @@ var init_ffPage = __esm({
this._page.frameManager.frameAbortedNavigation(params2.frameId, params2.errorText, params2.navigationId); this._page.frameManager.frameAbortedNavigation(params2.frameId, params2.errorText, params2.navigationId);
} }
_onNavigationCommitted(params2) { _onNavigationCommitted(params2) {
if (!params2.navigationId) {
this._page.frameManager.frameCommittedSameDocumentNavigation(params2.frameId, params2.url);
return;
}
for (const [workerId, worker] of this._workers) { for (const [workerId, worker] of this._workers) {
if (worker.frameId === params2.frameId) if (worker.frameId === params2.frameId)
this._onWorkerDestroyed({ workerId }); this._onWorkerDestroyed({ workerId });
} }
this._page.frameManager.frameCommittedNewDocumentNavigation(params2.frameId, params2.url, params2.name || "", params2.navigationId, false); this._page.frameManager.frameCommittedNewDocumentNavigation(params2.frameId, params2.url, params2.name || "", params2.navigationId || "", false);
} }
_onSameDocumentNavigation(params2) { _onSameDocumentNavigation(params2) {
this._page.frameManager.frameCommittedSameDocumentNavigation(params2.frameId, params2.url); this._page.frameManager.frameCommittedSameDocumentNavigation(params2.frameId, params2.url);
@@ -46941,7 +46942,7 @@ var init_wkPage = __esm({
} }
_onWebSocketWillSendHandshakeRequest(event) { _onWebSocketWillSendHandshakeRequest(event) {
const wallTimeMs = event.walltime * 1e3; const wallTimeMs = event.walltime * 1e3;
this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp); this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp * 1e3);
this._page.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers), wallTimeMs); this._page.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers), wallTimeMs);
} }
_onWebSocketClosed(event) { _onWebSocketClosed(event) {
@@ -46949,7 +46950,7 @@ var init_wkPage = __esm({
this._page.frameManager.webSocketClosed(event.requestId); this._page.frameManager.webSocketClosed(event.requestId);
} }
_timestampToWallTimeMsForWebSocket(requestId, timestamp) { _timestampToWallTimeMsForWebSocket(requestId, timestamp) {
return this._timestampBaselineForWebSocket.get(requestId) + timestamp; return this._timestampBaselineForWebSocket.get(requestId) + timestamp * 1e3;
} }
async _grantPermissions(origin, permissions) { async _grantPermissions(origin, permissions) {
const webPermissionToProtocol = /* @__PURE__ */ new Map([ const webPermissionToProtocol = /* @__PURE__ */ new Map([
@@ -49287,7 +49288,7 @@ var init_wvPage = __esm({
} }
_onWebSocketWillSendHandshakeRequest(event) { _onWebSocketWillSendHandshakeRequest(event) {
const wallTimeMs = event.walltime * 1e3; const wallTimeMs = event.walltime * 1e3;
this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp); this._timestampBaselineForWebSocket.set(event.requestId, wallTimeMs - event.timestamp * 1e3);
this._page.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers), wallTimeMs); this._page.frameManager.onWebSocketRequest(event.requestId, headersObjectToArray(event.request.headers), wallTimeMs);
} }
_onWebSocketClosed(event) { _onWebSocketClosed(event) {
@@ -49295,7 +49296,7 @@ var init_wvPage = __esm({
this._page.frameManager.webSocketClosed(event.requestId); this._page.frameManager.webSocketClosed(event.requestId);
} }
_timestampToWallTimeMsForWebSocket(requestId, timestamp) { _timestampToWallTimeMsForWebSocket(requestId, timestamp) {
return this._timestampBaselineForWebSocket.get(requestId) + timestamp; return this._timestampBaselineForWebSocket.get(requestId) + timestamp * 1e3;
} }
shouldToggleStyleSheetToSyncAnimations() { shouldToggleStyleSheetToSyncAnimations() {
return true; return true;
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
<link rel="icon" href="./playwright-logo.svg" type="image/svg+xml"> <link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
<link rel="manifest" href="./manifest.webmanifest"> <link rel="manifest" href="./manifest.webmanifest">
<title>Playwright Trace Viewer</title> <title>Playwright Trace Viewer</title>
<script type="module" crossorigin src="./index.DEBl1tfz.js"></script> <script type="module" crossorigin src="./index.DMMX1gXU.js"></script>
<link rel="modulepreload" crossorigin href="./assets/urlMatch-BYQrIQwR.js"> <link rel="modulepreload" crossorigin href="./assets/urlMatch-BYQrIQwR.js">
<link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-BNmKHKpQ.js"> <link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-BNmKHKpQ.js">
<link rel="stylesheet" crossorigin href="./defaultSettingsView.CjdS-WJx.css"> <link rel="stylesheet" crossorigin href="./defaultSettingsView.CjdS-WJx.css">
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "playwright-core", "name": "playwright-core",
"version": "1.61.0", "version": "1.61.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
+3 -2
View File
@@ -869,7 +869,7 @@ function resolve(specifier, context, nextResolve) {
const filename = import_url.default.fileURLToPath(context.parentURL); const filename = import_url.default.fileURLToPath(context.parentURL);
const resolved = resolveHook(filename, specifier); const resolved = resolveHook(filename, specifier);
if (resolved !== void 0) { if (resolved !== void 0) {
specifier = context.conditions?.includes("import") ? import_url.default.pathToFileURL(resolved).toString() : resolved; specifier = new Set(context.conditions).has("import") ? import_url.default.pathToFileURL(resolved).toString() : resolved;
} }
} }
const result2 = nextResolve(specifier, context); const result2 = nextResolve(specifier, context);
@@ -894,7 +894,8 @@ function load(moduleUrl, context, nextLoad) {
const filename = import_url.default.fileURLToPath(moduleUrl); const filename = import_url.default.fileURLToPath(moduleUrl);
if (!shouldTransform(filename)) if (!shouldTransform(filename))
return nextLoad(moduleUrl, context); return nextLoad(moduleUrl, context);
const format = kSupportedFormats.get(context.format) || (fileIsModule(filename) ? "module" : "commonjs"); const isRequire = !new Set(context.conditions).has("import");
const format = isRequire ? "commonjs" : kSupportedFormats.get(context.format) || (fileIsModule(filename) ? "module" : "commonjs");
const code = import_fs5.default.readFileSync(filename, "utf-8"); const code = import_fs5.default.readFileSync(filename, "utf-8");
const transformed = transformHook(code, filename, format === "module" ? moduleUrl : void 0); const transformed = transformHook(code, filename, format === "module" ? moduleUrl : void 0);
return { return {
+1 -1
View File
@@ -16,7 +16,7 @@
1.2 KB packages/playwright/src/common/validators.ts 1.2 KB packages/playwright/src/common/validators.ts
1.3 KB packages/playwright/src/isomorphic/teleReceiver.ts 1.3 KB packages/playwright/src/isomorphic/teleReceiver.ts
9.6 KB packages/playwright/src/transform/compilationCache.ts 9.6 KB packages/playwright/src/transform/compilationCache.ts
1.6 KB packages/playwright/src/transform/esmLoaderSync.ts 1.7 KB packages/playwright/src/transform/esmLoaderSync.ts
1.1 KB packages/playwright/src/transform/pirates.ts 1.1 KB packages/playwright/src/transform/pirates.ts
1.1 KB packages/playwright/src/transform/portTransport.ts 1.1 KB packages/playwright/src/transform/portTransport.ts
12.3 KB packages/playwright/src/transform/transform.ts 12.3 KB packages/playwright/src/transform/transform.ts
+3 -1
View File
@@ -12832,8 +12832,10 @@ function createExpect(info) {
if (typeof m !== "function") if (typeof m !== "function")
throw new TypeError(`expect.extend: \`${name}\` is not a valid matcher. Must be a function, is "${typeof m}"`); throw new TypeError(`expect.extend: \`${name}\` is not a valid matcher. Must be a function, is "${typeof m}"`);
} }
Object.assign(info.userMatchers, matchers2);
for (const [name, matcher] of Object.entries(matchers2)) { for (const [name, matcher] of Object.entries(matchers2)) {
if (name in allBuiltinMatchers)
continue;
info.userMatchers[name] = matcher;
const { positive, inverse } = buildCustomAsymmetricMatcher(name, matcher); const { positive, inverse } = buildCustomAsymmetricMatcher(name, matcher);
expectFn[name] = positive; expectFn[name] = positive;
notAsymmetric[name] = inverse; notAsymmetric[name] = inverse;
+1 -1
View File
@@ -1,5 +1,5 @@
# packages/playwright/lib/matchers/expect.js # packages/playwright/lib/matchers/expect.js
# total: 513.8 KB # total: 513.9 KB
## Inlined (64) ## Inlined (64)
7.9 KB node_modules/@babel/code-frame/lib/index.js 7.9 KB node_modules/@babel/code-frame/lib/index.js
+3 -2
View File
@@ -5629,7 +5629,7 @@ function resolve(specifier, context, nextResolve) {
const filename = import_url.default.fileURLToPath(context.parentURL); const filename = import_url.default.fileURLToPath(context.parentURL);
const resolved = resolveHook(filename, specifier); const resolved = resolveHook(filename, specifier);
if (resolved !== void 0) { if (resolved !== void 0) {
specifier = context.conditions?.includes("import") ? import_url.default.pathToFileURL(resolved).toString() : resolved; specifier = new Set(context.conditions).has("import") ? import_url.default.pathToFileURL(resolved).toString() : resolved;
} }
} }
const result2 = nextResolve(specifier, context); const result2 = nextResolve(specifier, context);
@@ -5654,7 +5654,8 @@ function load(moduleUrl, context, nextLoad) {
const filename = import_url.default.fileURLToPath(moduleUrl); const filename = import_url.default.fileURLToPath(moduleUrl);
if (!shouldTransform(filename)) if (!shouldTransform(filename))
return nextLoad(moduleUrl, context); return nextLoad(moduleUrl, context);
const format = kSupportedFormats.get(context.format) || (fileIsModule(filename) ? "module" : "commonjs"); const isRequire = !new Set(context.conditions).has("import");
const format = isRequire ? "commonjs" : kSupportedFormats.get(context.format) || (fileIsModule(filename) ? "module" : "commonjs");
const code = import_fs4.default.readFileSync(filename, "utf-8"); const code = import_fs4.default.readFileSync(filename, "utf-8");
const transformed = transformHook(code, filename, format === "module" ? moduleUrl : void 0); const transformed = transformHook(code, filename, format === "module" ? moduleUrl : void 0);
return { return {
+2 -2
View File
@@ -1,5 +1,5 @@
# packages/playwright/lib/transform/esmLoader.js # packages/playwright/lib/transform/esmLoader.js
# total: 249.0 KB # total: 249.1 KB
## Inlined (47) ## Inlined (47)
1.5 KB node_modules/balanced-match/index.js 1.5 KB node_modules/balanced-match/index.js
@@ -40,7 +40,7 @@
0.2 KB packages/isomorphic/stringUtils.ts 0.2 KB packages/isomorphic/stringUtils.ts
6.3 KB packages/playwright/src/transform/compilationCache.ts 6.3 KB packages/playwright/src/transform/compilationCache.ts
3.1 KB packages/playwright/src/transform/esmLoader.ts 3.1 KB packages/playwright/src/transform/esmLoader.ts
1.6 KB packages/playwright/src/transform/esmLoaderSync.ts 1.7 KB packages/playwright/src/transform/esmLoaderSync.ts
1.1 KB packages/playwright/src/transform/pirates.ts 1.1 KB packages/playwright/src/transform/pirates.ts
1.1 KB packages/playwright/src/transform/portTransport.ts 1.1 KB packages/playwright/src/transform/portTransport.ts
11.7 KB packages/playwright/src/transform/transform.ts 11.7 KB packages/playwright/src/transform/transform.ts
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "playwright", "name": "playwright",
"version": "1.61.0", "version": "1.61.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -53,7 +53,7 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.61.0" "playwright-core": "1.61.1"
}, },
"optionalDependencies": { "optionalDependencies": {
"fsevents": "2.3.2" "fsevents": "2.3.2"
+8 -10
View File
@@ -9,7 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"playwright": "^1.48.0" "playwright": "^1.61.1"
} }
}, },
"node_modules/accepts": { "node_modules/accepts": {
@@ -580,12 +580,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.61.0", "version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.61.0" "playwright-core": "1.61.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -598,10 +597,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.61.0", "version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
}, },
+1 -1
View File
@@ -8,6 +8,6 @@
}, },
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"playwright": "^1.48.0" "playwright": "^1.61.1"
} }
} }
+94 -28
View File
@@ -18,8 +18,32 @@ export default function AIAssistantPage() {
const [recentPrompts, setRecentPrompts] = useState<string[]>([]) const [recentPrompts, setRecentPrompts] = useState<string[]>([])
const [aiOnline, setAiOnline] = useState<boolean | null>(null) const [aiOnline, setAiOnline] = useState<boolean | null>(null)
const [searching, setSearching] = useState(false) const [searching, setSearching] = useState(false)
const [msgCount, setMsgCount] = useState(0)
const [tokenEstimate, setTokenEstimate] = useState("0")
const aiChatRef = useRef<AIChatHandle | null>(null) const aiChatRef = useRef<AIChatHandle | null>(null)
useEffect(() => {
const saved = localStorage.getItem("ai-chat-messages")
if (saved) {
try {
const msgs = JSON.parse(saved)
if (Array.isArray(msgs)) {
const today = new Date()
today.setHours(0, 0, 0, 0)
const todayMsgs = msgs.filter((m: any) => {
if (!m.ts) return true
return new Date(m.ts).getTime() >= today.getTime()
})
const userMsgs = todayMsgs.filter((m: any) => m.role === "user")
setMsgCount(userMsgs.length)
const totalChars = todayMsgs.reduce((s: number, m: any) => s + (m.content?.length || 0), 0)
const tokens = Math.round(totalChars / 4)
setTokenEstimate(tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens))
}
} catch {}
}
}, [])
const handleJobSelect = useCallback((job: typeof selectedJob) => { const handleJobSelect = useCallback((job: typeof selectedJob) => {
setSelectedJob(job) setSelectedJob(job)
}, []) }, [])
@@ -41,36 +65,72 @@ export default function AIAssistantPage() {
const handleSearch = useCallback(async (job: NonNullable<typeof selectedJob>) => { const handleSearch = useCallback(async (job: NonNullable<typeof selectedJob>) => {
setSearching(true) setSearching(true)
const keyword = job.keywords[0] const prompt = `You are a lead generation assistant. Generate 5 realistic prospect profiles for my business targeting ${job.job_title} clients (${job.industry}). ${job.description ? "Service: " + job.description : ""} Keywords: ${job.keywords.join(", ")}.
aiChatRef.current?.addAssistantMessage(`🔍 Searching Facebook for **${job.job_title}** leads...`)
const controller = new AbortController() IMPORTANT: Return ONLY a valid JSON array. Do NOT use markdown, code blocks, or any formatting. Just raw JSON.
const timeoutId = setTimeout(() => controller.abort(), 360000)
const statusId = setTimeout(() => { Example format:
aiChatRef.current?.addAssistantMessage("⏳ Still searching Facebook (this can take up to 5 minutes)...") [{"companyName":"Acme Corp","contactName":"John Smith","email":"john@acme.com","phone":"555-0100","description":"Brief description"}]
}, 45000)
Use realistic company names, names, and emails for the ${job.industry} industry.`
aiChatRef.current?.addAssistantMessage(`🔍 Finding leads for **${job.job_title}**...`)
try { try {
const res = await fetch(`http://localhost:3008/scrape/facebook?force=true&query=${encodeURIComponent(keyword)}`, { method: "POST", signal: controller.signal }) const res = await fetch("/api/ai/chat", {
clearTimeout(timeoutId) method: "POST",
clearTimeout(statusId) headers: { "Content-Type": "application/json" },
const data = await res.json() body: JSON.stringify({ message: prompt }),
if (data.success && data.leads?.length > 0) { })
const leadsText = data.leads.map((lead: any, i: number) =>
`**${i + 1}.** ${lead.author || "Unknown"}\n> ${(lead.content || "").slice(0, 300)}\n> 🔗 ${lead.url || "(no link available)"}` if (!res.ok) {
).join("\n\n") const data = await res.json().catch(() => ({}))
aiChatRef.current?.addAssistantMessage(`✅ Found **${data.leads.length}** leads:\n\n${leadsText}`) throw new Error(data.error || `Error ${res.status}`)
} else {
const reason = data.error || data.flag_reason || "No leads found this time"
aiChatRef.current?.addAssistantMessage(`⚠️ ${reason}`)
} }
} catch (err: any) {
clearTimeout(timeoutId) const data = await res.json()
clearTimeout(statusId) const raw = data.response || ""
const msg = err.name === "AbortError" let leads: any[]
? "❌ Scraper timed out after 5 minutes. Try again or check the scraper service." try {
: `${err instanceof Error ? err.message : "Search failed"}` let text = raw.trim()
aiChatRef.current?.addAssistantMessage(msg) const jsonMatch = text.match(/\[[\s\S]*\]/)
if (jsonMatch) text = jsonMatch[0]
leads = JSON.parse(text)
if (!Array.isArray(leads)) leads = [leads]
} catch (e) {
aiChatRef.current?.addAssistantMessage(`⚠️ AI response wasn't valid JSON. Showing raw output:\n\n${raw.slice(0, 1500)}`)
setSearching(false)
return
}
let created = 0
let lastError = ""
for (const lead of leads.slice(0, 5)) {
try {
const r = await fetch("/api/leads", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
companyName: lead.companyName || "Unknown Company",
contactName: lead.contactName || "",
email: lead.email || "",
phone: lead.phone || "",
description: lead.description || "",
status: "open",
}),
})
if (r.ok) created++
else { const e = await r.json().catch(() => ({})); lastError = e.error || r.statusText; }
} catch (e) { lastError = "Network error" }
}
if (created > 0) {
aiChatRef.current?.addAssistantMessage(`✅ Created **${created}** lead${created > 1 ? "s" : ""} for **${job.job_title}**! Go to the Leads tab to see them.`)
} else {
aiChatRef.current?.addAssistantMessage(`❌ Failed to create leads. Last error: ${lastError || "Unknown"}. Make sure you're logged in with an account that has permission to create leads.`)
}
} catch (err) {
const msg = err instanceof Error ? err.message : "Search failed"
aiChatRef.current?.addAssistantMessage(`❌ Error: ${msg}. Make sure Ollama is running with the model loaded.`)
} finally { } finally {
setSearching(false) setSearching(false)
} }
@@ -89,6 +149,12 @@ export default function AIAssistantPage() {
const next = [msg, ...prev.filter((p) => p !== msg)].slice(0, 3) const next = [msg, ...prev.filter((p) => p !== msg)].slice(0, 3)
return next return next
}) })
setMsgCount((prev) => prev + 1)
setTokenEstimate((prev) => {
const raw = parseInt(prev.replace("k", "")) * (prev.includes("k") ? 1000 : 1)
const updated = raw + Math.round(msg.length / 4)
return updated >= 1000 ? `${(updated / 1000).toFixed(1)}k` : String(updated)
})
}, []) }, [])
return ( return (
@@ -186,11 +252,11 @@ export default function AIAssistantPage() {
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex-1"> <div className="flex-1">
<div className="text-muted-foreground text-[10px] uppercase tracking-wide">Messages today</div> <div className="text-muted-foreground text-[10px] uppercase tracking-wide">Messages today</div>
<div className="text-foreground text-sm font-semibold mt-0.5">24</div> <div className="text-foreground text-sm font-semibold mt-0.5">{msgCount}</div>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="text-muted-foreground text-[10px] uppercase tracking-wide">Tokens used</div> <div className="text-muted-foreground text-[10px] uppercase tracking-wide">Tokens used</div>
<div className="text-foreground text-sm font-semibold mt-0.5">12.4k</div> <div className="text-foreground text-sm font-semibold mt-0.5">{tokenEstimate}</div>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -79,7 +79,7 @@ export function JobSelector({ onSelect, onSearch, searching }: JobSelectorProps)
className="w-full flex items-center justify-center gap-2 mt-3 bg-primary hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed text-primary-foreground text-sm font-semibold rounded-xl px-4 py-3 transition-all duration-200 shadow-lg shadow-primary/20" className="w-full flex items-center justify-center gap-2 mt-3 bg-primary hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed text-primary-foreground text-sm font-semibold rounded-xl px-4 py-3 transition-all duration-200 shadow-lg shadow-primary/20"
> >
{searching ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />} {searching ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
{searching ? "Searching..." : "Search Facebook"} {searching ? "Searching..." : "Search Leads"}
</button> </button>
)} )}
</div> </div>