diff --git a/create-app.ts b/create-app.ts index 1e0663be160abdae8165302ea64c9bdce971ee5d..34d51b7a9e50521ec724118cba1d6842739281ef 100644 --- a/create-app.ts +++ b/create-app.ts @@ -1,19 +1,13 @@ /* eslint-disable import/no-extraneous-dependencies */ import path from "path"; import { green } from "picocolors"; -import type { PackageManager } from "./helpers/get-pkg-manager"; import { tryGitInit } from "./helpers/git"; import { isFolderEmpty } from "./helpers/is-folder-empty"; import { getOnline } from "./helpers/is-online"; import { isWriteable } from "./helpers/is-writeable"; import { makeDir } from "./helpers/make-dir"; -import type { - TemplateEngine, - TemplateFramework, - TemplateType, - TemplateUI, -} from "./templates"; +import type { InstallTemplateArgs } from "./templates"; import { installPythonTemplate, installTemplate } from "./templates"; export async function createApp({ @@ -24,14 +18,9 @@ export async function createApp({ appPath, packageManager, eslint, -}: { - template: TemplateType; - framework: TemplateFramework; - engine: TemplateEngine; - ui: TemplateUI; + customApiPath, +}: Omit<InstallTemplateArgs, "appName" | "root" | "isOnline"> & { appPath: string; - packageManager: PackageManager; - eslint: boolean; }): Promise<void> { const root = path.resolve(appPath); @@ -73,6 +62,7 @@ export async function createApp({ packageManager, isOnline, eslint, + customApiPath, }); } diff --git a/helpers/is-url.ts b/helpers/is-url.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb87b975252f721bf596f98b628f4b6394a63516 --- /dev/null +++ b/helpers/is-url.ts @@ -0,0 +1,8 @@ +export function isUrl(url: string): boolean { + try { + new URL(url); + return true; + } catch (error) { + return false; + } +} diff --git a/index.ts b/index.ts index a287cdfef763db6252fe6808ad1d4a2e6579bd64..18ee5f5ea27e486c2ee244481385b2c371ef96b3 100644 --- a/index.ts +++ b/index.ts @@ -11,6 +11,7 @@ import checkForUpdate from "update-check"; import { createApp } from "./create-app"; import { getPkgManager } from "./helpers/get-pkg-manager"; import { isFolderEmpty } from "./helpers/is-folder-empty"; +import { isUrl } from "./helpers/is-url"; import { validateNpmName } from "./helpers/validate-pkg"; import packageJson from "./package.json"; @@ -43,13 +44,6 @@ const program = new Commander.Command(packageJson.name) ` Initialize with eslint config. -`, - ) - .option( - "--import-alias <alias-to-configure>", - ` - - Specify import alias to use (default "@/*"). `, ) .option( @@ -182,10 +176,18 @@ async function run(): Promise<void> { engine: "simple", ui: "html", eslint: true, + customApiPath: "http://localhost:8000/api/chat", }; const getPrefOrDefault = (field: string) => preferences[field] ?? defaults[field]; + const handlers = { + onCancel: () => { + console.error("Exiting."); + process.exit(1); + }, + }; + if (!program.template) { if (ciInfo.isCI) { program.template = getPrefOrDefault("template"); @@ -201,12 +203,7 @@ async function run(): Promise<void> { ], initial: 1, }, - { - onCancel: () => { - console.error("Exiting."); - process.exit(1); - }, - }, + handlers, ); program.template = template; preferences.template = template; @@ -229,12 +226,7 @@ async function run(): Promise<void> { ], initial: 0, }, - { - onCancel: () => { - console.error("Exiting."); - process.exit(1); - }, - }, + handlers, ); program.framework = framework; preferences.framework = framework; @@ -257,12 +249,7 @@ async function run(): Promise<void> { ], initial: 0, }, - { - onCancel: () => { - console.error("Exiting."); - process.exit(1); - }, - }, + handlers, ); program.ui = ui; preferences.ui = ui; @@ -275,6 +262,15 @@ async function run(): Promise<void> { if (ciInfo.isCI) { program.engine = getPrefOrDefault("engine"); } else { + const external = + program.framework === "nextjs" + ? [ + { + title: "External chat engine (e.g. FastAPI)", + value: "external", + }, + ] + : []; const { engine } = await prompts( { type: "select", @@ -283,20 +279,39 @@ async function run(): Promise<void> { choices: [ { title: "SimpleChatEngine", value: "simple" }, { title: "ContextChatEngine", value: "context" }, + ...external, ], initial: 0, }, - { - onCancel: () => { - console.error("Exiting."); - process.exit(1); - }, - }, + handlers, ); program.engine = engine; preferences.engine = engine; } } + if ( + program.framework === "nextjs" && + program.engine === "external" && + !program.customApiPath + ) { + if (ciInfo.isCI) { + program.customApiPath = getPrefOrDefault("customApiPath"); + } else { + const { customApiPath } = await prompts( + { + type: "text", + name: "customApiPath", + message: + "URL path of your external chat engine (used for development)?", + validate: (url) => (isUrl(url) ? true : "Please enter a valid URL"), + initial: getPrefOrDefault("customApiPath"), + }, + handlers, + ); + program.customApiPath = customApiPath; + preferences.customApiPath = customApiPath; + } + } } if ( @@ -330,6 +345,7 @@ async function run(): Promise<void> { appPath: resolvedProjectPath, packageManager, eslint: program.eslint, + customApiPath: program.customApiPath, }); conf.set("preferences", preferences); } diff --git a/templates/index.ts b/templates/index.ts index d2bf52688e5b85361d6dd6babf254f7da7f1594e..90d0b808f6054b5b67e9b8489f2f93e2a56d1c0e 100644 --- a/templates/index.ts +++ b/templates/index.ts @@ -22,6 +22,7 @@ export const installTemplate = async ({ engine, ui, eslint, + customApiPath, }: InstallTemplateArgs) => { console.log(bold(`Using ${packageManager}.`)); @@ -117,6 +118,20 @@ export const installTemplate = async ({ llamaindex: version, }; + if (engine === "external" && customApiPath) { + console.log( + "\nUsing external API with custom API path:", + customApiPath, + "\n", + ); + const apiPath = path.join(root, "app", "api"); + await fs.rmdir(apiPath, { recursive: true }); + packageJson.scripts = { + ...packageJson.scripts, + dev: `NEXT_PUBLIC_CHAT_API=${customApiPath} next dev`, + }; + } + if (engine === "context" && relativeEngineDestPath) { // add generate script if using context engine packageJson.scripts = { diff --git a/templates/simple/nextjs/app/components/chat-section.tsx b/templates/simple/nextjs/app/components/chat-section.tsx index 0a3ea5b3d5135354745289fccd3e16f0db2179ae..d4527654177b36fd7bc0d46e2a38677b10bb10c9 100644 --- a/templates/simple/nextjs/app/components/chat-section.tsx +++ b/templates/simple/nextjs/app/components/chat-section.tsx @@ -10,7 +10,7 @@ export default function ChatSection() { const [input, setInput] = useState(""); const getAssistantMessage = async (messages: Message[]) => { - const response = await fetch("/api/chat", { + const response = await fetch(process.env.NEXT_PUBLIC_CHAT_API ?? "/api/chat", { method: "POST", headers: { "Content-Type": "application/json", diff --git a/templates/streaming/nextjs/app/components/chat-section.tsx b/templates/streaming/nextjs/app/components/chat-section.tsx index 0a5fcef939a304d351194b268bb84201f7865072..e1dae74fe0f6acf0b104947202e93b8c59ce5d25 100644 --- a/templates/streaming/nextjs/app/components/chat-section.tsx +++ b/templates/streaming/nextjs/app/components/chat-section.tsx @@ -5,7 +5,7 @@ import { ChatInput, ChatMessages, Message } from "../../../../ui/html/chat"; export default function ChatSection() { const { messages, input, isLoading, handleSubmit, handleInputChange } = - useChat(); + useChat({ api: process.env.NEXT_PUBLIC_CHAT_API }); return ( <> diff --git a/templates/types.ts b/templates/types.ts index 75d4d70eedf0745e9270fa4849c5ca922e22abe9..d4c032f781a19957e1006cabeddf5100fccd8662 100644 --- a/templates/types.ts +++ b/templates/types.ts @@ -2,7 +2,7 @@ import { PackageManager } from "../helpers/get-pkg-manager"; export type TemplateType = "simple" | "streaming"; export type TemplateFramework = "nextjs" | "express" | "fastapi"; -export type TemplateEngine = "simple" | "context"; +export type TemplateEngine = "simple" | "context" | "external"; export type TemplateUI = "html" | "shadcn"; export interface InstallPythonTemplateArgs { @@ -22,4 +22,5 @@ export interface InstallTemplateArgs { engine: TemplateEngine; ui: TemplateUI; eslint: boolean; + customApiPath: string; }