UNPKG

triostack-document-sdk

Version:

Generate professional proposals and legal agreements using OpenAI API with support for PDF and DOC formats

675 lines (601 loc) 21.7 kB
import OpenAI from "openai"; import { jsPDF } from "jspdf"; import { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, } from "docx"; /** * Generate a professional sales proposal using OpenAI API * @param {string} apiKey - OpenAI API key * @param {string} projectDetails - Detailed project information * @param {string} projectName - Name of the project * @param {string} pricing - Pricing information * @param {string} extraDetails - Additional context/details for the AI * @returns {Promise<string>} Generated proposal content */ export async function generateProposal( apiKey, projectDetails, projectName, pricing, extraDetails = "" ) { try { const client = new OpenAI({ apiKey: apiKey, }); const prompt = ` Create a professional sales proposal (not an email) formatted with clear headings, based on the following project information: Project Name: ${projectName} Project Details: ${projectDetails} Pricing: ${pricing} ${extraDetails ? `Additional Details: ${extraDetails}` : ""} The proposal should include these sections with clear headings: - EXECUTIVE SUMMARY - PROJECT OVERVIEW - OUR APPROACH - DELIVERABLES - TIMELINE - INVESTMENT - NEXT STEPS Use clear, professional headings in ALL CAPS for each section. Make the content compelling and tailored to the specific project. `; const response = await client.chat.completions.create({ model: "gpt-5-nano", messages: [ { role: "system", content: "You are a professional business proposal writer. Create compelling, well-structured sales proposals that convert leads to customers.", }, { role: "user", content: prompt }, ], stream: false, }); return response.choices[0]?.message?.content || "No proposal generated."; } catch (error) { throw new Error(`Failed to generate proposal: ${error.message}`); } } /** * Generate a legal agreement between two parties using OpenAI API * @param {string} apiKey - OpenAI API key * @param {string} projectDetails - Detailed project information * @param {string} projectName - Name of the project * @param {string} pricing - Pricing information * @param {string} partyA - Information about Party A (name, details, etc.) * @param {string} partyB - Information about Party B (name, details, etc.) * @param {string} extraDetails - Additional context/details for the AI * @returns {Promise<string>} Generated agreement content */ export async function generateAgreement( apiKey, projectDetails, projectName, pricing, partyA, partyB, extraDetails = "" ) { try { const client = new OpenAI({ apiKey: apiKey, }); const prompt = ` Create a professional legal agreement between two parties, formatted with clear headings, based on the following information: Project Name: ${projectName} Project Details: ${projectDetails} Pricing: ${pricing} Party A: ${partyA} Party B: ${partyB} ${extraDetails ? `Additional Details: ${extraDetails}` : ""} The agreement should include: - Clear identification of both parties - Project scope and deliverables - Terms and conditions - Payment terms and schedule - Timeline and milestones - Intellectual property rights - Confidentiality clauses - Termination conditions - Legal jurisdiction - Professional legal formatting `; const response = await client.chat.completions.create({ model: "gpt-5-nano", messages: [ { role: "system", content: "You are a legal document writer. Create comprehensive, professional legal agreements that protect both parties' interests while being clear and enforceable.", }, { role: "user", content: prompt }, ], stream: false, }); return response.choices[0]?.message?.content || "No agreement generated."; } catch (error) { throw new Error(`Failed to generate agreement: ${error.message}`); } } /** * Convert HTML content to PDF using jsPDF with deep black headings * @param {string} htmlContent - HTML content to convert * @param {string} filename - Name of the PDF file (without .pdf extension) * @returns {Promise<Uint8Array>} PDF as Uint8Array */ export async function generatePDF(htmlContent, filename = "document") { try { // Create PDF directly with jsPDF const pdf = new jsPDF("p", "mm", "a4"); // Set initial font and styling pdf.setFont("helvetica"); pdf.setFontSize(10); let yPosition = 20; const lineHeight = 7; // Add company name at the top (centered) pdf.setFontSize(16); pdf.setTextColor(0, 0, 0); // Deep black color pdf.setFont("helvetica", "bold"); const companyName = "TRIOSTACK TECHNOLOGIES PRIVATE LIMITED"; const companyNameWidth = pdf.getTextWidth(companyName); const centerX = (210 - companyNameWidth) / 2; // Center on A4 page (210mm width) pdf.text(companyName, centerX, yPosition); yPosition += lineHeight + 8; // Extra spacing after company name // Add company details below (centered with separators) // Set consistent font size for all company details pdf.setFontSize(8); pdf.setFont("helvetica", "normal"); // Combined company details with separators (labels bold, content normal) const detailsParts = [ { label: "CIN:", content: "U62012UP2025PTC226106" }, { label: "Phone:", content: "+91 9211941924" }, { label: "Website:", content: "www.triostack.in" }, { label: "Email:", content: "info@triostack.in" }, { label: "Address:", content: "IIMT LBF, Plot No. 19, 20, near IIMT Group of Colleges, Knowledge Park III, Greater Noida, Uttar Pradesh 201310", }, ]; // Line 1: CIN, Phone, Website (centered) pdf.setFont("helvetica", "bold"); const cinLabelWidth = pdf.getTextWidth("CIN: "); pdf.setFont("helvetica", "normal"); const cinContentWidth = pdf.getTextWidth("U62012UP2025PTC226106"); pdf.setFont("helvetica", "bold"); const phoneLabelWidth = pdf.getTextWidth("Phone: "); pdf.setFont("helvetica", "normal"); const phoneContentWidth = pdf.getTextWidth("+91 9211941924"); pdf.setFont("helvetica", "bold"); const websiteLabelWidth = pdf.getTextWidth("Website: "); pdf.setFont("helvetica", "normal"); const websiteContentWidth = pdf.getTextWidth("www.triostack.in"); const separatorWidth = pdf.getTextWidth(" | "); const totalFirstLineWidth = cinLabelWidth + cinContentWidth + separatorWidth + phoneLabelWidth + phoneContentWidth + separatorWidth + websiteLabelWidth + websiteContentWidth; // Center the first line const firstLineStartX = (210 - totalFirstLineWidth) / 2; let currentX = firstLineStartX; // Draw CIN pdf.setFont("helvetica", "bold"); pdf.text("CIN: ", currentX, yPosition); currentX += cinLabelWidth; pdf.setFont("helvetica", "normal"); pdf.text("U62012UP2025PTC226106", currentX, yPosition); currentX += cinContentWidth; // Add separator pdf.text(" | ", currentX, yPosition); currentX += separatorWidth; // Draw Phone pdf.setFont("helvetica", "bold"); pdf.text("Phone: ", currentX, yPosition); currentX += phoneLabelWidth; pdf.setFont("helvetica", "normal"); pdf.text("+91 9211941924", currentX, yPosition); currentX += phoneContentWidth; // Add separator pdf.text(" | ", currentX, yPosition); currentX += separatorWidth; // Draw Website pdf.setFont("helvetica", "bold"); pdf.text("Website: ", currentX, yPosition); currentX += websiteLabelWidth; pdf.setFont("helvetica", "normal"); pdf.text("www.triostack.in", currentX, yPosition); // Line 2: Email (centered) yPosition += lineHeight; pdf.setFont("helvetica", "bold"); const emailLabelWidth = pdf.getTextWidth("Email: "); pdf.setFont("helvetica", "normal"); const emailContentWidth = pdf.getTextWidth("info@triostack.in"); const totalSecondLineWidth = emailLabelWidth + emailContentWidth; const secondLineStartX = (210 - totalSecondLineWidth) / 2; pdf.setFont("helvetica", "bold"); pdf.text("Email: ", secondLineStartX, yPosition); pdf.setFont("helvetica", "normal"); pdf.text( "info@triostack.in", secondLineStartX + emailLabelWidth, yPosition ); // Line 3: Address (centered) - ULTIMATE FIX yPosition += lineHeight; // Use a much smaller max width to ensure no clipping const maxAddressWidth = 110; // Further reduced from 130 to 110 for much better right margin const fullAddress = "IIMT LBF, Plot No. 19, 20, near IIMT Group of Colleges, Knowledge Park III, Greater Noida, Uttar Pradesh 201310"; // Split the address content only (without the label) const addressLines = pdf.splitTextToSize(fullAddress, maxAddressWidth); // Calculate total height needed for address const addressHeight = addressLines.length * lineHeight; // Center the entire address block const addressBlockStartY = yPosition; for (let i = 0; i < addressLines.length; i++) { const line = addressLines[i]; const lineWidth = pdf.getTextWidth(line); const lineStartX = (210 - lineWidth) / 2; // Center each line if (i === 0) { // First line: Draw label and content with proper spacing pdf.setFont("helvetica", "bold"); const labelWidth = pdf.getTextWidth("Address: "); const labelStartX = lineStartX - labelWidth; pdf.text("Address: ", labelStartX, yPosition); pdf.setFont("helvetica", "normal"); pdf.text(line, lineStartX, yPosition); } else { // Continuation lines: just the content, centered pdf.setFont("helvetica", "normal"); pdf.text(line, lineStartX, yPosition); } yPosition += lineHeight; } yPosition += lineHeight; yPosition += 5; // Extra spacing after address // Add a line separator with proper margins pdf.setDrawColor(0, 0, 0); pdf.line(25, yPosition, 185, yPosition); // Further reduced from 20-190 to 25-185 for even better margins yPosition += 15; // Space after line // Split content into lines that fit the page width with proper margins const pageWidth = 140; // Further reduced from 150 to 140 for even more right margin/padding const lines = pdf.splitTextToSize(htmlContent, pageWidth); // Add content to PDF with deep black headings for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Check if we need a new page if (yPosition > 270) { pdf.addPage(); yPosition = 20; } // Check if this line is a heading (starts with # or is in ALL CAPS) const isHeading = line.trim().startsWith("#") || (line.trim().length > 3 && line.trim() === line.trim().toUpperCase() && line.trim().length < 50); if (isHeading) { // Set heading styling with deep black color pdf.setFontSize(14); pdf.setTextColor(0, 0, 0); // Deep black color for headings pdf.setFont("helvetica", "bold"); // Remove # symbols if present const headingText = line.replace(/^#+\s*/, "").trim(); pdf.text(headingText, 25, yPosition); // Further increased from 20 to 25 for even better left margin // Reset styling for next line pdf.setFontSize(10); pdf.setTextColor(0, 0, 0); // Black color pdf.setFont("helvetica", "normal"); yPosition += lineHeight + 3; // Extra spacing after headings } else { // Regular text pdf.setTextColor(0, 0, 0); // Black color pdf.setFontSize(10); pdf.setFont("helvetica", "normal"); pdf.text(line, 25, yPosition); // Further increased from 20 to 25 for even better left margin yPosition += lineHeight; } } return pdf.output("arraybuffer"); } catch (error) { throw new Error(`Failed to generate PDF: ${error.message}`); } } /** * Convert content to DOC format using docx library * @param {string} content - Content to convert * @param {string} filename - Name of the DOC file (without .docx extension) * @returns {Promise<Uint8Array>} DOC as Uint8Array */ export async function generateDOC(content, filename = "document") { try { // Split content into paragraphs const paragraphs = content.split("\n\n").filter((p) => p.trim()); // Create document structure const doc = new Document({ sections: [ { properties: {}, children: [ // Company name at the top new Paragraph({ children: [ new TextRun({ text: "TRIOSTACK TECHNOLOGIES PRIVATE LIMITED", size: 36, bold: true, }), ], alignment: AlignmentType.CENTER, spacing: { after: 300 }, }), // Company details with separators (bold labels) - Line 1 new Paragraph({ children: [ new TextRun({ text: "CIN: ", size: 18, bold: true, }), new TextRun({ text: "U62012UP2025PTC226106", size: 18, bold: false, }), new TextRun({ text: " | Phone: ", size: 18, bold: true, }), new TextRun({ text: "+91 9211941924", size: 18, bold: false, }), new TextRun({ text: " | Website: ", size: 18, bold: true, }), new TextRun({ text: "www.triostack.in", size: 18, bold: false, }), ], alignment: AlignmentType.CENTER, spacing: { after: 200 }, }), // Company details - Line 2 new Paragraph({ children: [ new TextRun({ text: "Email: ", size: 18, bold: true, }), new TextRun({ text: "info@triostack.in", size: 18, bold: false, }), new TextRun({ text: " | Address: ", size: 18, bold: true, }), new TextRun({ text: "IIMT LBF, Plot No. 19, 20, near IIMT Group of Colleges, Knowledge Park III, Greater Noida, Uttar Pradesh 201310", size: 18, bold: false, }), ], alignment: AlignmentType.CENTER, spacing: { after: 400 }, }), // Document title new Paragraph({ text: filename.toUpperCase(), heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER, spacing: { after: 400 }, }), ...paragraphs.map( (paragraph) => new Paragraph({ children: [ new TextRun({ text: paragraph.trim(), size: 24, }), ], spacing: { after: 200 }, }) ), ], }, ], }); // Generate DOC file const buffer = await Packer.toBuffer(doc); return buffer; } catch (error) { throw new Error(`Failed to generate DOC: ${error.message}`); } } /** * Generate proposal and convert to PDF * @param {string} apiKey - OpenAI API key * @param {string} projectDetails - Detailed project information * @param {string} projectName - Name of the project * @param {string} pricing - Pricing information * @param {string} extraDetails - Additional context/details for the AI * @param {string} filename - Name of the PDF file (without .pdf extension) * @returns {Promise<Uint8Array>} PDF as Uint8Array */ export async function generateProposalPDF( apiKey, projectDetails, projectName, pricing, extraDetails = "", filename = "proposal" ) { try { // Generate proposal content const proposalContent = await generateProposal( apiKey, projectDetails, projectName, pricing, extraDetails ); // Create formatted content for PDF const formattedContent = `${projectName}\n\n${proposalContent}`; // Generate PDF return await generatePDF(formattedContent, filename); } catch (error) { throw new Error(`Failed to generate proposal PDF: ${error.message}`); } } /** * Generate agreement and convert to PDF * @param {string} apiKey - OpenAI API key * @param {string} projectDetails - Detailed project information * @param {string} projectName - Name of the project * @param {string} pricing - Pricing information * @param {string} partyA - Information about Party A * @param {string} partyB - Information about Party B * @param {string} extraDetails - Additional context/details for the AI * @param {string} filename - Name of the PDF file (without .pdf extension) * @returns {Promise<Uint8Array>} PDF as Uint8Array */ export async function generateAgreementPDF( apiKey, projectDetails, projectName, pricing, partyA, partyB, extraDetails = "", filename = "agreement" ) { try { // Generate agreement content const agreementContent = await generateAgreement( apiKey, projectDetails, projectName, pricing, partyA, partyB, extraDetails ); // Create formatted content for PDF const formattedContent = `${projectName}\n\n${agreementContent}`; // Generate PDF return await generatePDF(formattedContent, filename); } catch (error) { throw new Error(`Failed to generate agreement PDF: ${error.message}`); } } /** * Generate proposal and convert to DOC * @param {string} apiKey - OpenAI API key * @param {string} projectDetails - Detailed project information * @param {string} projectName - Name of the project * @param {string} pricing - Pricing information * @param {string} extraDetails - Additional context/details for the AI * @param {string} filename - Name of the DOC file (without .docx extension) * @returns {Promise<Uint8Array>} DOC as Uint8Array */ export async function generateProposalDOC( apiKey, projectDetails, projectName, pricing, extraDetails = "", filename = "proposal" ) { try { // Generate proposal content const proposalContent = await generateProposal( apiKey, projectDetails, projectName, pricing, extraDetails ); // Create formatted content for DOC const formattedContent = `${projectName}\n\n${proposalContent}`; // Generate DOC return await generateDOC(formattedContent, filename); } catch (error) { throw new Error(`Failed to generate proposal DOC: ${error.message}`); } } /** * Generate agreement and convert to DOC * @param {string} apiKey - OpenAI API key * @param {string} projectDetails - Detailed project information * @param {string} projectName - Name of the project * @param {string} pricing - Pricing information * @param {string} partyA - Information about Party A * @param {string} partyB - Information about Party B * @param {string} extraDetails - Additional context/details for the AI * @param {string} filename - Name of the DOC file (without .docx extension) * @returns {Promise<Uint8Array>} DOC as Uint8Array */ export async function generateAgreementDOC( apiKey, projectDetails, projectName, pricing, partyA, partyB, extraDetails = "", filename = "agreement" ) { try { // Generate agreement content const agreementContent = await generateAgreement( apiKey, projectDetails, projectName, pricing, partyA, partyB, extraDetails ); // Create formatted content for DOC const formattedContent = `${projectName}\n\n${agreementContent}`; // Generate DOC return await generateDOC(formattedContent, filename); } catch (error) { throw new Error(`Failed to generate agreement DOC: ${error.message}`); } } // Default export for backward compatibility export default { generateProposal, generateAgreement, generatePDF, generateDOC, generateProposalPDF, generateProposalDOC, generateAgreementPDF, generateAgreementDOC, };