cli html renderer
This commit is contained in:
32
package-lock.json
generated
32
package-lock.json
generated
@@ -10,6 +10,8 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@openrouter/sdk": "^0.11.2",
|
||||
"chalk": "^5.6.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"exa-js": "^2.11.0",
|
||||
"express": "^5.2.1"
|
||||
}
|
||||
@@ -99,6 +101,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||
@@ -175,9 +189,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"version": "17.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz",
|
||||
"integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -273,6 +287,18 @@
|
||||
"zod-to-json-schema": "^3.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exa-js/node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@openrouter/sdk": "^0.11.2",
|
||||
"chalk": "^5.6.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"exa-js": "^2.11.0",
|
||||
"express": "^5.2.1"
|
||||
|
||||
@@ -2,6 +2,7 @@ import dotenv from 'dotenv';
|
||||
import { createClients } from './src/clients.js';
|
||||
import { getConfig, validateConfig } from './src/config/env.js';
|
||||
import { createSearchService } from './src/services/searchService.js';
|
||||
import { renderHTML } from './src/utils/htmlConsoleRenderer.js';
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
@@ -49,13 +50,11 @@ async function runCLI() {
|
||||
|
||||
const result = await searchService.search(question);
|
||||
|
||||
|
||||
console.log(result);
|
||||
console.log('');
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log('FINAL ANSWER:');
|
||||
console.log('═══════════════════════════════════════════════════════════');
|
||||
console.log(result.fullAnswerHTMLSnippet || result.answer || 'No answer generated');
|
||||
console.log(renderHTML(result.fullAnswerHTMLSnippet) || 'No answer generated');
|
||||
console.log('');
|
||||
|
||||
if (result.mostRelevantSources && result.mostRelevantSources.length > 0) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { logOpenRouterCall } from './openRouterLogger.js';
|
||||
|
||||
function parseResponse(response) {
|
||||
return { cost: response.usage.cost, data: JSON.parse(response.choices[0].message.content) };
|
||||
if(!response?.usage?.cost) console.log(response);
|
||||
|
||||
return { cost: response?.usage?.cost, data: JSON.parse(response.choices[0].message.content) };
|
||||
}
|
||||
|
||||
export async function summarizeSources({ openrouter, text, question }) {
|
||||
@@ -71,12 +73,12 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) {
|
||||
`;
|
||||
|
||||
const params = {
|
||||
model: 'openai/gpt-5.4-mini',
|
||||
model: 'openai/gpt-oss-120b:nitro',
|
||||
messages: [
|
||||
{ role: 'system', content: prompt },
|
||||
{ role: 'user', content: text },
|
||||
],
|
||||
reasoning: { effort: 'none' },
|
||||
reasoning: { effort: 'low' },
|
||||
response_format: {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
|
||||
@@ -48,7 +48,7 @@ async function fetchDetailedContents({ exa, question, sources, broadcast }) {
|
||||
);
|
||||
|
||||
const content = await exa.getContents([source.url], EXA_CONTENT_OPTIONS(question));
|
||||
return { url: source.url, content };
|
||||
return { url: source.url, content, cost: content.costDollars.total };
|
||||
} catch (error) {
|
||||
broadcast(`⚠️ Could not fetch content for ${source.url}: ${error.message}`, 'warning');
|
||||
return { url: source.url, content: null };
|
||||
@@ -82,7 +82,8 @@ export function createSearchService({ exa, openrouter, broadcast }) {
|
||||
broadcast(`✅ Found ${result.results.length} results`, 'success', {
|
||||
count: result.results.length,
|
||||
});
|
||||
let cost = result.costDollars.total;
|
||||
const cost=[];
|
||||
cost.push({type:'exa_search', amount: result.costDollars.total});
|
||||
|
||||
const extractedContent = extractContent(result);
|
||||
|
||||
@@ -104,7 +105,7 @@ export function createSearchService({ exa, openrouter, broadcast }) {
|
||||
question,
|
||||
});
|
||||
summary = summaryResult.data;
|
||||
cost += summaryResult.cost;
|
||||
cost.push({type:'openrouter_summarize', amount: summaryResult.cost});
|
||||
} catch (error) {
|
||||
throw new SearchServiceError('Failed to generate summary', 500, error);
|
||||
}
|
||||
@@ -127,6 +128,11 @@ export function createSearchService({ exa, openrouter, broadcast }) {
|
||||
|
||||
broadcast('🔧 Enhancing summaries with detailed content...', 'info');
|
||||
enrichSourcesWithDetails(summary.sources, detailedContents);
|
||||
for (const detailed of detailedContents) {
|
||||
if (detailed.cost) {
|
||||
cost.push({type:'exa_get_content', amount: detailed.cost});
|
||||
}
|
||||
}
|
||||
|
||||
broadcast('📊 Generating final summary...', 'info');
|
||||
|
||||
@@ -138,7 +144,7 @@ export function createSearchService({ exa, openrouter, broadcast }) {
|
||||
question,
|
||||
});
|
||||
finalSummary = finalSummaryResult.data;
|
||||
cost += finalSummaryResult.cost;
|
||||
cost.push({type:'openrouter_final_summary', amount: finalSummaryResult.cost});
|
||||
} catch (error) {
|
||||
throw new SearchServiceError('Failed to generate final summary', 500, error);
|
||||
}
|
||||
@@ -151,7 +157,6 @@ export function createSearchService({ exa, openrouter, broadcast }) {
|
||||
answerLength: finalSummary.fullAnswerHTMLSnippet?.length || 0,
|
||||
sourcesCount: finalSummary.mostRelevantSources?.length || 0,
|
||||
});
|
||||
console.log(`Total cost of API calls: $${cost.toFixed(6)}`);
|
||||
|
||||
return finalSummary;
|
||||
},
|
||||
|
||||
80
src/utils/htmlConsoleRenderer.js
Normal file
80
src/utils/htmlConsoleRenderer.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
/**
|
||||
* Simple HTML to console renderer using chalk for styling.
|
||||
* Supports: <b>, <i>, <u>, <ul>, <li>, <span style="color:...">, <p>, <div>, <hr/>
|
||||
*/
|
||||
|
||||
// Color mapping for span styles
|
||||
const colorMap = {
|
||||
red: chalk.red,
|
||||
blue: chalk.blue,
|
||||
green: chalk.green,
|
||||
yellow: chalk.yellow,
|
||||
cyan: chalk.cyan,
|
||||
magenta: chalk.magenta,
|
||||
white: chalk.white,
|
||||
gray: chalk.gray,
|
||||
grey: chalk.gray,
|
||||
black: chalk.black,
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse HTML string and convert to styled console output
|
||||
* @param {string} html - HTML string to render
|
||||
* @returns {string} - Formatted string with chalk styling
|
||||
*/
|
||||
export function renderHTML(html) {
|
||||
if (!html) return '';
|
||||
|
||||
let text = html;
|
||||
|
||||
// Handle <hr/> as a line
|
||||
text = text.replace(/<hr\s*\/?>/gi, '\n' + '─'.repeat(60) + '\n');
|
||||
|
||||
// Handle <span style="color:...">content</span>
|
||||
text = text.replace(/<span\s+style\s*=\s*["']color:\s*(\w+)["']\s*>([\s\S]*?)<\/span>/gi, (match, color, content) => {
|
||||
const colorFn = colorMap[color.toLowerCase()];
|
||||
return colorFn ? colorFn(content) : content;
|
||||
});
|
||||
|
||||
// Handle <b>content</b> - bold
|
||||
text = text.replace(/<b>([\s\S]*?)<\/b>/gi, (match, content) => chalk.bold(content));
|
||||
|
||||
// Handle <i>content</i> - italic (dimmed as terminal alternative)
|
||||
text = text.replace(/<i>([\s\S]*?)<\/i>/gi, (match, content) => chalk.italic(content));
|
||||
|
||||
// Handle <u>content</u> - underline
|
||||
text = text.replace(/<u>([\s\S]*?)<\/u>/gi, (match, content) => chalk.underline(content));
|
||||
|
||||
// Handle <li>content</li> - list item with bullet
|
||||
text = text.replace(/<li>([\s\S]*?)<\/li>/gi, (match, content) => ` • ${content.trim()}\n`);
|
||||
|
||||
// Handle <ul>content</ul> - unordered list (just preserve content, items will be formatted)
|
||||
text = text.replace(/<ul>([\s\S]*?)<\/ul>/gi, (match, content) => `${content}\n`);
|
||||
|
||||
// Handle <p>content</p> - paragraph with spacing
|
||||
text = text.replace(/<p>([\s\S]*?)<\/p>/gi, (match, content) => `${content.trim()}\n\n`);
|
||||
|
||||
// Handle <div>content</div> - block with newline
|
||||
text = text.replace(/<div>([\s\S]*?)<\/div>/gi, (match, content) => `${content.trim()}\n\n`);
|
||||
|
||||
// Clean up any remaining HTML tags that weren't processed
|
||||
text = text.replace(/<[^>]+>/g, '');
|
||||
|
||||
// Clean up excessive blank lines (more than 2 consecutive newlines)
|
||||
text = text.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
// Trim extra whitespace at start and end
|
||||
text = text.trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML and print to console
|
||||
* @param {string} html - HTML string to render
|
||||
*/
|
||||
export function printHTML(html) {
|
||||
console.log(renderHTML(html));
|
||||
}
|
||||
Reference in New Issue
Block a user