@nlweb-ai/chat-app 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/nlweb-chat.mjs +133 -0
  2. package/package.json +27 -17
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Standalone server for @nlweb-ai/chat-app.
4
+ // Serves the built SPA and optionally proxies API routes to ASK_API_URL.
5
+ //
6
+ // Usage:
7
+ // ASK_API_URL=http://localhost:8081 npx @nlweb-ai/chat-app
8
+ // PORT=4000 ASK_API_URL=https://my-nlweb.example.com npx @nlweb-ai/chat-app
9
+
10
+ import { createServer } from "node:http";
11
+ import { request as httpRequest } from "node:http";
12
+ import { request as httpsRequest } from "node:https";
13
+ import { createReadStream, existsSync, statSync } from "node:fs";
14
+ import { join, extname, dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const DIST_DIR = join(__dirname, "..", "dist");
19
+ const PORT = parseInt(process.env.PORT || "8080", 10);
20
+ const ASK_API_URL = process.env.ASK_API_URL;
21
+
22
+ const PROXY_PATHS = [
23
+ "/ask",
24
+ "/health",
25
+ "/config",
26
+ "/mcp",
27
+ "/mcp-sse",
28
+ "/a2a",
29
+ "/a2a-sse",
30
+ "/site-configs",
31
+ ];
32
+
33
+ const MIME_TYPES = {
34
+ ".html": "text/html; charset=utf-8",
35
+ ".js": "application/javascript; charset=utf-8",
36
+ ".css": "text/css; charset=utf-8",
37
+ ".json": "application/json; charset=utf-8",
38
+ ".svg": "image/svg+xml",
39
+ ".png": "image/png",
40
+ ".ico": "image/x-icon",
41
+ ".map": "application/json",
42
+ };
43
+
44
+ // ── Proxy ──────────────────────────────────────────────────────────────
45
+
46
+ function shouldProxy(url) {
47
+ const path = url.split("?")[0];
48
+ return PROXY_PATHS.some(
49
+ (prefix) => path === prefix || path.startsWith(prefix + "/"),
50
+ );
51
+ }
52
+
53
+ function proxy(clientReq, clientRes) {
54
+ const upstream = new URL(ASK_API_URL);
55
+ const reqFn = upstream.protocol === "https:" ? httpsRequest : httpRequest;
56
+
57
+ const options = {
58
+ hostname: upstream.hostname,
59
+ port: upstream.port || (upstream.protocol === "https:" ? 443 : 80),
60
+ path: clientReq.url,
61
+ method: clientReq.method,
62
+ headers: { ...clientReq.headers, host: upstream.host },
63
+ };
64
+
65
+ const proxyReq = reqFn(options, (proxyRes) => {
66
+ clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
67
+ proxyRes.pipe(clientRes, { end: true });
68
+ });
69
+
70
+ proxyReq.on("error", (err) => {
71
+ console.error(`Proxy error: ${err.message}`);
72
+ if (!clientRes.headersSent) {
73
+ clientRes.writeHead(502, { "Content-Type": "text/plain" });
74
+ }
75
+ clientRes.end(`Bad Gateway: ${err.message}`);
76
+ });
77
+
78
+ clientReq.pipe(proxyReq, { end: true });
79
+ }
80
+
81
+ // ── Static file server ─────────────────────────────────────────────────
82
+
83
+ function serveStatic(req, res) {
84
+ const urlPath = req.url.split("?")[0];
85
+ let filePath = join(DIST_DIR, urlPath === "/" ? "index.html" : urlPath);
86
+
87
+ if (!filePath.startsWith(DIST_DIR)) {
88
+ res.writeHead(403);
89
+ res.end("Forbidden");
90
+ return;
91
+ }
92
+
93
+ // SPA fallback: non-existent paths serve index.html
94
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
95
+ filePath = join(DIST_DIR, "index.html");
96
+ }
97
+
98
+ const ext = extname(filePath);
99
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
100
+ const stat = statSync(filePath);
101
+
102
+ res.writeHead(200, {
103
+ "Content-Type": contentType,
104
+ "Content-Length": stat.size,
105
+ });
106
+ createReadStream(filePath).pipe(res);
107
+ }
108
+
109
+ // ── Server ─────────────────────────────────────────────────────────────
110
+
111
+ if (!ASK_API_URL) {
112
+ console.error("Error: ASK_API_URL is required. Example:");
113
+ console.error(" ASK_API_URL=http://localhost:8081 npx @nlweb-ai/chat-app");
114
+ process.exit(1);
115
+ }
116
+
117
+ if (!existsSync(join(DIST_DIR, "index.html"))) {
118
+ console.error("Error: dist/index.html not found. Run 'pnpm build' first.");
119
+ process.exit(1);
120
+ }
121
+
122
+ const server = createServer((req, res) => {
123
+ if (ASK_API_URL && shouldProxy(req.url)) {
124
+ proxy(req, res);
125
+ } else {
126
+ serveStatic(req, res);
127
+ }
128
+ });
129
+
130
+ server.listen(PORT, () => {
131
+ console.log(`NLWeb Chat App: http://localhost:${PORT}`);
132
+ console.log(`Proxying API requests to: ${ASK_API_URL}`);
133
+ });
package/package.json CHANGED
@@ -1,12 +1,23 @@
1
1
  {
2
2
  "name": "@nlweb-ai/chat-app",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "NLWeb AI chat application built with React and @nlweb-ai/search-components",
6
6
  "license": "MIT",
7
7
  "author": "nlweb-ai",
8
- "keywords": ["nlweb", "chat", "search", "react"],
9
- "files": ["dist"],
8
+ "keywords": [
9
+ "nlweb",
10
+ "chat",
11
+ "search",
12
+ "react"
13
+ ],
14
+ "bin": {
15
+ "nlweb-chat": "./bin/nlweb-chat.mjs"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "bin"
20
+ ],
10
21
  "repository": {
11
22
  "type": "git",
12
23
  "url": "https://github.com/nlweb-ai/nlweb-ask-agent.git",
@@ -16,23 +27,12 @@
16
27
  "registry": "https://registry.npmjs.org",
17
28
  "access": "public"
18
29
  },
19
- "scripts": {
20
- "dev": "vite",
21
- "build": "tsc -b && vite build",
22
- "start": "serve dist -s -l 3000",
23
- "lint": "eslint .",
24
- "format": "prettier --write \"src/**/*.{ts,tsx,json,css}\"",
25
- "format:check": "prettier --check \"src/**/*.{ts,tsx,json,css}\"",
26
- "typecheck": "tsc -b --noEmit",
27
- "preview": "vite preview",
28
- "prepublishOnly": "pnpm run build"
29
- },
30
30
  "dependencies": {
31
31
  "@headlessui/react": "^2.2.9",
32
32
  "@heroicons/react": "^2.2.0",
33
- "@nlweb-ai/search-components": "workspace:*",
34
33
  "react": "^19.2.0",
35
- "react-dom": "^19.2.0"
34
+ "react-dom": "^19.2.0",
35
+ "@nlweb-ai/search-components": "0.1.10"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@eslint/js": "^9.39.1",
@@ -50,5 +50,15 @@
50
50
  "typescript": "~5.9.3",
51
51
  "typescript-eslint": "^8.46.4",
52
52
  "vite": "npm:rolldown-vite@7.2.5"
53
+ },
54
+ "scripts": {
55
+ "dev": "vite",
56
+ "build": "tsc -b && vite build",
57
+ "start": "node bin/nlweb-chat.mjs",
58
+ "lint": "eslint .",
59
+ "format": "prettier --write \"src/**/*.{ts,tsx,json,css}\"",
60
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,json,css}\"",
61
+ "typecheck": "tsc -b --noEmit",
62
+ "preview": "vite preview"
53
63
  }
54
- }
64
+ }