From 1af678eb2cad819fbc7fefc71ecf77efb33109f1 Mon Sep 17 00:00:00 2001
From: "Huu Le (Lee)" <39040748+leehuwuj@users.noreply.github.com>
Date: Thu, 25 Jan 2024 15:20:42 +0700
Subject: [PATCH] Feat: add local pdf file option (#441)

---
 create-app.ts    |  2 +
 helpers/index.ts | 35 ++++++++++++------
 helpers/types.ts |  1 +
 index.ts         |  1 +
 questions.ts     | 96 ++++++++++++++++++++++++++++++++++++++++++------
 5 files changed, 111 insertions(+), 24 deletions(-)

diff --git a/create-app.ts b/create-app.ts
index 2725e3d9..231ace96 100644
--- a/create-app.ts
+++ b/create-app.ts
@@ -35,6 +35,7 @@ export async function createApp({
   vectorDb,
   externalPort,
   postInstallAction,
+  contextFile,
 }: InstallAppArgs): Promise<void> {
   const root = path.resolve(appPath);
 
@@ -77,6 +78,7 @@ export async function createApp({
     vectorDb,
     externalPort,
     postInstallAction,
+    contextFile,
   };
 
   if (frontend) {
diff --git a/helpers/index.ts b/helpers/index.ts
index 4c07c003..39630aa5 100644
--- a/helpers/index.ts
+++ b/helpers/index.ts
@@ -70,22 +70,32 @@ const copyTestData = async (
   engine?: TemplateEngine,
   openAiKey?: string,
   vectorDb?: TemplateVectorDB,
+  contextFile?: string,
   // eslint-disable-next-line max-params
 ) => {
   if (engine === "context") {
-    const srcPath = path.join(
-      __dirname,
-      "..",
-      "templates",
-      "components",
-      "data",
-    );
     const destPath = path.join(root, "data");
-    console.log(`\nCopying test data to ${cyan(destPath)}\n`);
-    await copy("**", destPath, {
-      parents: true,
-      cwd: srcPath,
-    });
+    if (contextFile) {
+      console.log(`\nCopying provided file to ${cyan(destPath)}\n`);
+      await fs.mkdir(destPath, { recursive: true });
+      await fs.copyFile(
+        contextFile,
+        path.join(destPath, path.basename(contextFile)),
+      );
+    } else {
+      const srcPath = path.join(
+        __dirname,
+        "..",
+        "templates",
+        "components",
+        "data",
+      );
+      console.log(`\nCopying test data to ${cyan(destPath)}\n`);
+      await copy("**", destPath, {
+        parents: true,
+        cwd: srcPath,
+      });
+    }
   }
 
   if (packageManager && engine === "context") {
@@ -168,6 +178,7 @@ export const installTemplate = async (
       props.engine,
       props.openAiKey,
       props.vectorDb,
+      props.contextFile,
     );
   } else {
     // this is a frontend for a full-stack app, create .env file with model information
diff --git a/helpers/types.ts b/helpers/types.ts
index 0f28caad..8346babf 100644
--- a/helpers/types.ts
+++ b/helpers/types.ts
@@ -15,6 +15,7 @@ export interface InstallTemplateArgs {
   template: TemplateType;
   framework: TemplateFramework;
   engine: TemplateEngine;
+  contextFile?: string;
   ui: TemplateUI;
   eslint: boolean;
   customApiPath?: string;
diff --git a/index.ts b/index.ts
index fae221f2..0da8419c 100644
--- a/index.ts
+++ b/index.ts
@@ -240,6 +240,7 @@ async function run(): Promise<void> {
     vectorDb: program.vectorDb,
     externalPort: program.externalPort,
     postInstallAction: program.postInstallAction,
+    contextFile: program.contextFile,
   });
   conf.set("preferences", preferences);
 
diff --git a/questions.ts b/questions.ts
index 90c87d42..989c9071 100644
--- a/questions.ts
+++ b/questions.ts
@@ -1,7 +1,8 @@
+import { execSync } from "child_process";
 import ciInfo from "ci-info";
 import fs from "fs";
 import path from "path";
-import { blue, green } from "picocolors";
+import { blue, green, red } from "picocolors";
 import prompts from "prompts";
 import { InstallAppArgs } from "./create-app";
 import { TemplateFramework } from "./helpers";
@@ -9,6 +10,22 @@ import { COMMUNITY_OWNER, COMMUNITY_REPO } from "./helpers/constant";
 import { getRepoRootFolders } from "./helpers/repo";
 
 export type QuestionArgs = Omit<InstallAppArgs, "appPath" | "packageManager">;
+const MACOS_FILE_SELECTION_SCRIPT = `
+osascript -l JavaScript -e '
+  a = Application.currentApplication();
+  a.includeStandardAdditions = true;
+  a.chooseFile({ withPrompt: "Please select a file to process:" }).toString()
+'`;
+
+const WINDOWS_FILE_SELECTION_SCRIPT = `
+Add-Type -AssemblyName System.Windows.Forms
+$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
+$openFileDialog.InitialDirectory = [Environment]::GetFolderPath('Desktop')
+$result = $openFileDialog.ShowDialog()
+if ($result -eq 'OK') {
+  $openFileDialog.FileName
+}
+`;
 
 const defaults: QuestionArgs = {
   template: "streaming",
@@ -55,6 +72,45 @@ const getVectorDbChoices = (framework: TemplateFramework) => {
   return displayedChoices;
 };
 
+const selectPDFFile = async () => {
+  // Popup to select a PDF file
+  try {
+    let selectedFilePath: string = "";
+    switch (process.platform) {
+      case "win32": // Windows
+        selectedFilePath = execSync(WINDOWS_FILE_SELECTION_SCRIPT, {
+          shell: "powershell.exe",
+        })
+          .toString()
+          .trim();
+        break;
+      case "darwin": // MacOS
+        selectedFilePath = execSync(MACOS_FILE_SELECTION_SCRIPT)
+          .toString()
+          .trim();
+        break;
+      default: // Unsupported OS
+        console.log(red("Unsupported OS error!"));
+        process.exit(1);
+    }
+    // Check is pdf file
+    if (!selectedFilePath.endsWith(".pdf")) {
+      console.log(
+        red("Unsupported file error! Please select a valid PDF file!"),
+      );
+      process.exit(1);
+    }
+    return selectedFilePath;
+  } catch (error) {
+    console.log(
+      red(
+        "Got error when trying to select file! Please try again or select other options.",
+      ),
+    );
+    process.exit(1);
+  }
+};
+
 export const onPromptState = (state: any) => {
   if (state.aborted) {
     // If we don't re-enable the terminal cursor before exiting
@@ -243,24 +299,40 @@ export const askQuestions = async (
     if (ciInfo.isCI) {
       program.engine = getPrefOrDefault("engine");
     } else {
-      const { engine } = await prompts(
+      let choices = [
+        {
+          title: "No data, just a simple chat",
+          value: "simple",
+        },
+        { title: "Use an example PDF", value: "exampleFile" },
+      ];
+      if (process.platform === "win32" || process.platform === "darwin") {
+        choices.push({ title: "Use a local PDF file", value: "localFile" });
+      }
+
+      const { dataSource } = await prompts(
         {
           type: "select",
-          name: "engine",
+          name: "dataSource",
           message: "Which data source would you like to use?",
-          choices: [
-            {
-              title: "No data, just a simple chat",
-              value: "simple",
-            },
-            { title: "Use an example PDF", value: "context" },
-          ],
+          choices: choices,
           initial: 1,
         },
         handlers,
       );
-      program.engine = engine;
-      preferences.engine = engine;
+      switch (dataSource) {
+        case "simple":
+          program.engine = "simple";
+          break;
+        case "exampleFile":
+          program.engine = "context";
+          break;
+        case "localFile":
+          program.engine = "context";
+          // If the user selected the "pdf" option, ask them to select a file
+          program.contextFile = await selectPDFFile();
+          break;
+      }
     }
     if (program.engine !== "simple" && !program.vectorDb) {
       if (ciInfo.isCI) {
-- 
GitLab