research-cli
Version:
AI-powered research assistant with web search capabilities and beautiful terminal UI
129 lines • 5.45 kB
JavaScript
/**
* Markdown renderer for TTY-friendly streaming output
*/
import { createCitationsPanel, createErrorPanel, createStatusPanel, createToolPanel, createUsageStatsPanel, } from "../ui/panels.js";
import { ProgressManager } from "../ui/progress-manager.js";
export class MarkdownRenderer {
isFirstChunk = true;
citations = [];
progressManager;
// private citationCounter = 1; // Unused for now, will be used in M2
constructor(options = {}) {
this.progressManager = new ProgressManager(options);
}
async renderStream(eventStream) {
try {
for await (const event of eventStream) {
await this.renderEvent(event);
}
// Render citations at the end if any
if (this.citations.length > 0) {
this.renderCitations();
}
// Ensure we end with a newline for clean terminal output
process.stdout.write("\n");
}
catch (error) {
// eslint-disable-next-line no-console
console.error("\nError rendering stream:", error instanceof Error ? error.message : "Unknown error");
}
}
async renderEvent(event) {
// Handle progress updates
this.progressManager.handleProviderEvent(event);
switch (event.type) {
case "start":
if (event.data &&
typeof event.data === "object" &&
"provider" in event.data &&
"model" in event.data) {
const panel = createStatusPanel(String(event.data.provider), String(event.data.model));
// eslint-disable-next-line no-console
console.error(`\n${panel}`);
}
break;
case "tool_call":
if (event.data &&
typeof event.data === "object" &&
"name" in event.data) {
const panel = createToolPanel(String(event.data.name), "calling");
// eslint-disable-next-line no-console
console.error(`${panel}\n`);
}
break;
case "tool_result":
if (event.data &&
typeof event.data === "object" &&
"name" in event.data &&
"results" in event.data) {
const panel = createToolPanel(String(event.data.name), "completed");
// eslint-disable-next-line no-console
console.error(`${panel}\n`);
}
break;
case "chunk":
if (event.data &&
typeof event.data === "object" &&
"text" in event.data &&
typeof event.data.text === "string") {
// Stop progress bar when first chunk arrives
if (this.isFirstChunk) {
// Give a moment for progress bar to show completion
await new Promise((resolve) => setTimeout(resolve, 100));
process.stdout.write("\n");
process.stdout.write(event.data.text);
this.isFirstChunk = false;
}
else {
process.stdout.write(event.data.text);
}
}
break;
case "final":
if (event.data &&
typeof event.data === "object" &&
"citations" in event.data) {
// Store citations for rendering at the end
if (Array.isArray(event.data.citations)) {
this.citations = event.data.citations.map((citation) => ({
url: String(citation.url || ""),
title: String(citation.title || ""),
date: citation.date ? String(citation.date) : undefined,
}));
}
// Add final newline after content
process.stdout.write("\n");
// Display usage stats if available
if ("usage" in event.data && event.data.usage) {
this.renderUsageStats(event.data.usage);
}
}
break;
case "error":
if (event.data &&
typeof event.data === "object" &&
"message" in event.data) {
const panel = createErrorPanel(String(event.data.message), "code" in event.data ? String(event.data.code) : undefined);
// eslint-disable-next-line no-console
console.error(`\n${panel}`);
}
break;
default:
// Ignore unknown event types
break;
}
}
renderUsageStats(usage) {
const panel = createUsageStatsPanel(usage);
// eslint-disable-next-line no-console
console.error(`\n${panel}`);
}
renderCitations() {
if (this.citations.length === 0)
return;
const panel = createCitationsPanel(this.citations);
// eslint-disable-next-line no-console
console.log(`\n${panel}`);
}
}
//# sourceMappingURL=markdown.js.map