rushchatbot 1.6.7
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.
- package/INTEGRATION.md +559 -0
- package/index.html +13 -0
- package/package/INTEGRATION.md +717 -0
- package/package/README.md +717 -0
- package/package/package-lock.json +3221 -0
- package/package/package.json +101 -0
- package/package/src/cli/index.ts +94 -0
- package/package/src/client/ChatWidget.tsx +163 -0
- package/package/src/client.ts +2 -0
- package/package/src/core/RushChatbot.ts +192 -0
- package/package/src/index.ts +5 -0
- package/package/src/server/guardrails.ts +107 -0
- package/package/src/server/index.ts +210 -0
- package/package/src/server/matcher.ts +140 -0
- package/package/src/shared/types.ts +58 -0
- package/package/src/shims-vue.d.ts +5 -0
- package/package/src/vanilla/index.ts +302 -0
- package/package/src/vanilla.ts +3 -0
- package/package/src/vue/ChatWidget.vue +198 -0
- package/package/src/vue.ts +3 -0
- package/package/src/vue2/ChatWidget.vue +204 -0
- package/package/src/vue2.ts +3 -0
- package/package/tsconfig.json +19 -0
- package/package/vite.config.ts +86 -0
- package/package.json +35 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/server/data/analytics.json +795 -0
- package/server/data/chatbot.config.json +26 -0
- package/server/data/questions.json +238 -0
- package/server/data/questions.tl.json +182 -0
- package/server/index.ts +11 -0
- package/server/lib/guardrails.ts +107 -0
- package/server/lib/matcher.ts +140 -0
- package/server/routes/chat.ts +180 -0
- package/server/tsconfig.json +11 -0
- package/src/App.css +184 -0
- package/src/App.tsx +135 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/ChatWidget.tsx +417 -0
- package/src/components/QuestionTree.tsx +57 -0
- package/src/hooks/useChatData.ts +49 -0
- package/src/index.css +111 -0
- package/src/main.tsx +10 -0
- package/src/types/index.ts +40 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rushchatbot",
|
|
3
|
+
"version": "1.6.7",
|
|
4
|
+
"description": "Embeddable hybrid chatbot with n8n fallback, license key protection, and configurable FAQ tree",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chatbot",
|
|
7
|
+
"n8n",
|
|
8
|
+
"react",
|
|
9
|
+
"express",
|
|
10
|
+
"faq",
|
|
11
|
+
"widget"
|
|
12
|
+
],
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"require": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./server": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./client": {
|
|
28
|
+
"types": "./dist/client.d.ts",
|
|
29
|
+
"import": "./dist/client.es.js",
|
|
30
|
+
"require": "./dist/client.cjs.js"
|
|
31
|
+
},
|
|
32
|
+
"./vue": {
|
|
33
|
+
"types": "./dist/vue.d.ts",
|
|
34
|
+
"import": "./dist/vue.es.js",
|
|
35
|
+
"require": "./dist/vue.cjs.js"
|
|
36
|
+
},
|
|
37
|
+
"./vue2": {
|
|
38
|
+
"types": "./dist/vue2.d.ts",
|
|
39
|
+
"import": "./dist/vue2.es.js",
|
|
40
|
+
"require": "./dist/vue2.cjs.js"
|
|
41
|
+
},
|
|
42
|
+
"./vanilla": {
|
|
43
|
+
"types": "./dist/vanilla.d.ts",
|
|
44
|
+
"import": "./dist/vanilla.es.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"bin": {
|
|
48
|
+
"rushchatbot": "./dist/cli/index.js"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"README.md",
|
|
53
|
+
"INTEGRATION.md"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "npm run build:server && npm run build:react && npm run build:vue && npm run build:vue2 && npm run build:vanilla",
|
|
57
|
+
"build:server": "./node_modules/.bin/tsc -p tsconfig.json",
|
|
58
|
+
"build:react": "./node_modules/.bin/vite build",
|
|
59
|
+
"build:vue": "./node_modules/.bin/vite build --mode vue",
|
|
60
|
+
"build:vue2": "./node_modules/.bin/vite build --mode vue2",
|
|
61
|
+
"build:vanilla": "./node_modules/.bin/vite build --mode vanilla",
|
|
62
|
+
"prepublishOnly": "npm run build"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"axios": "^1.7.2",
|
|
66
|
+
"bad-words": "^4.1.5",
|
|
67
|
+
"cors": "^2.8.5",
|
|
68
|
+
"express": "^4.19.2",
|
|
69
|
+
"fuse.js": "^7.0.0",
|
|
70
|
+
"marked": "^18.0.13"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"react": ">=18.0.0",
|
|
74
|
+
"react-dom": ">=18.0.0",
|
|
75
|
+
"vue": ">=3.0.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"react": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"react-dom": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"vue": {
|
|
85
|
+
"optional": true
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@types/bad-words": "^3.0.3",
|
|
90
|
+
"@types/cors": "^2.8.17",
|
|
91
|
+
"@types/express": "^4.17.21",
|
|
92
|
+
"@types/node": "^22.0.0",
|
|
93
|
+
"@types/react": "^19.1.6",
|
|
94
|
+
"@types/react-dom": "^19.1.6",
|
|
95
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
96
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
97
|
+
"vue": "^3.4.0",
|
|
98
|
+
"typescript": "^5.5.3",
|
|
99
|
+
"vite": "^6.3.5"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2)
|
|
6
|
+
const command = args[0]
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
branding: {
|
|
10
|
+
title: 'My ChatBot',
|
|
11
|
+
subtitle: 'How can we help you today?',
|
|
12
|
+
theme: {
|
|
13
|
+
primaryColor: '#4F46E5',
|
|
14
|
+
backgroundColor: '#FFFFFF',
|
|
15
|
+
userBubbleColor: '#4F46E5',
|
|
16
|
+
botBubbleColor: '#F3F4F6',
|
|
17
|
+
textColor: '#111827',
|
|
18
|
+
fontFamily: 'Inter, sans-serif',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
n8n: {
|
|
22
|
+
webhookUrl: 'https://your-n8n.com/webhook/your-id',
|
|
23
|
+
timeoutMs: 55000,
|
|
24
|
+
},
|
|
25
|
+
chat: {
|
|
26
|
+
welcomeMessage: 'Hi! Choose a question below or type your own.',
|
|
27
|
+
fallbackMessage: "I'm looking into that for you...",
|
|
28
|
+
matchThreshold: 0.4,
|
|
29
|
+
showPreQuestionsOnStart: true,
|
|
30
|
+
language: 'en',
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const DEFAULT_QUESTIONS = [
|
|
35
|
+
{
|
|
36
|
+
id: 'q1',
|
|
37
|
+
question: 'What services do you offer?',
|
|
38
|
+
answer: 'We offer a wide range of services. Please contact us for more details.',
|
|
39
|
+
subquestions: [],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'q2',
|
|
43
|
+
question: 'How do I get in touch?',
|
|
44
|
+
answer: 'You can reach us via email or phone. Visit our Contact page for details.',
|
|
45
|
+
subquestions: [],
|
|
46
|
+
},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
function writeIfNotExists(filePath: string, content: unknown) {
|
|
50
|
+
if (fs.existsSync(filePath)) {
|
|
51
|
+
console.log(` [skip] ${filePath} already exists`)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
fs.writeFileSync(filePath, JSON.stringify(content, null, 2))
|
|
55
|
+
console.log(` [created] ${filePath}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (command === 'init') {
|
|
59
|
+
const targetDir = args[1] ? path.resolve(args[1]) : process.cwd()
|
|
60
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
61
|
+
|
|
62
|
+
console.log(`\nInitializing RushChatbot config in: ${targetDir}\n`)
|
|
63
|
+
writeIfNotExists(path.join(targetDir, 'chatbot.config.json'), DEFAULT_CONFIG)
|
|
64
|
+
writeIfNotExists(path.join(targetDir, 'questions.json'), DEFAULT_QUESTIONS)
|
|
65
|
+
writeIfNotExists(path.join(targetDir, 'questions.tl.json'), DEFAULT_QUESTIONS)
|
|
66
|
+
|
|
67
|
+
console.log(`
|
|
68
|
+
Done! Next steps:
|
|
69
|
+
1. Edit chatbot.config.json — set your n8n webhookUrl and branding
|
|
70
|
+
2. Edit questions.json — add your FAQ tree
|
|
71
|
+
3. In your server entry file:
|
|
72
|
+
|
|
73
|
+
import { createServer } from 'rushchatbot/server'
|
|
74
|
+
import path from 'path'
|
|
75
|
+
|
|
76
|
+
createServer({
|
|
77
|
+
configPath: path.resolve('./chatbot.config.json'),
|
|
78
|
+
questionsPath: path.resolve('./questions.json'),
|
|
79
|
+
questionsTagalogPath: path.resolve('./questions.tl.json'),
|
|
80
|
+
}).listen()
|
|
81
|
+
|
|
82
|
+
4. In your React app:
|
|
83
|
+
|
|
84
|
+
import { ChatWidget } from 'rushchatbot/client'
|
|
85
|
+
<ChatWidget apiBase="http://localhost:3001/api/chat" />
|
|
86
|
+
`)
|
|
87
|
+
} else {
|
|
88
|
+
console.log(`
|
|
89
|
+
RushChatbot CLI
|
|
90
|
+
|
|
91
|
+
Commands:
|
|
92
|
+
rushchatbot init [dir] Scaffold config and questions files
|
|
93
|
+
`)
|
|
94
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { RushChatbot, parseMarkdown } from '../core/RushChatbot'
|
|
3
|
+
import type { RushChatbotState } from '../core/RushChatbot'
|
|
4
|
+
import type { Question } from '../shared/types'
|
|
5
|
+
|
|
6
|
+
export interface ChatWidgetProps {
|
|
7
|
+
apiBase?: string
|
|
8
|
+
width?: number
|
|
9
|
+
height?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function MarkdownText({ text }: { text: string }) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
dangerouslySetInnerHTML={{ __html: parseMarkdown(text) }}
|
|
16
|
+
className='rushchatbot-md'
|
|
17
|
+
style={{ lineHeight: 1.6 }}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ThinkingDots({ color }: { color: string }) {
|
|
23
|
+
return (
|
|
24
|
+
<span style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
25
|
+
{[0, 1, 2].map(i => (
|
|
26
|
+
<span key={i} style={{
|
|
27
|
+
width: 7, height: 7, borderRadius: '50%', background: color,
|
|
28
|
+
display: 'inline-block',
|
|
29
|
+
animation: `rushchatbot-bounce 1.2s ease-in-out ${i * 0.2}s infinite`,
|
|
30
|
+
}} />
|
|
31
|
+
))}
|
|
32
|
+
<style>{`
|
|
33
|
+
@keyframes rushchatbot-bounce {
|
|
34
|
+
0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
|
|
35
|
+
40% { transform: translateY(-6px); opacity: 1; }
|
|
36
|
+
}
|
|
37
|
+
.rushchatbot-md p { margin: 0 0 6px 0; }
|
|
38
|
+
.rushchatbot-md p:last-child { margin-bottom: 0; }
|
|
39
|
+
.rushchatbot-md strong { font-weight: 600; }
|
|
40
|
+
.rushchatbot-md ul, .rushchatbot-md ol { margin: 4px 0 4px 16px; padding: 0; }
|
|
41
|
+
.rushchatbot-md li { margin-bottom: 2px; }
|
|
42
|
+
.rushchatbot-md a { color: inherit; text-decoration: underline; }
|
|
43
|
+
.rushchatbot-md h1, .rushchatbot-md h2, .rushchatbot-md h3 { margin: 4px 0 6px 0; font-weight: 700; line-height: 1.3; }
|
|
44
|
+
.rushchatbot-md h1 { font-size: 1.1em; }
|
|
45
|
+
.rushchatbot-md h2 { font-size: 1em; }
|
|
46
|
+
.rushchatbot-md h3 { font-size: 0.95em; }
|
|
47
|
+
`}</style>
|
|
48
|
+
</span>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ChatWidget({ apiBase, width = 380, height = 580 }: ChatWidgetProps) {
|
|
53
|
+
const chatRef = useRef<RushChatbot | null>(null)
|
|
54
|
+
const [state, setState] = useState<RushChatbotState | null>(null)
|
|
55
|
+
const bottomRef = useRef<HTMLDivElement>(null)
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const chat = new RushChatbot({ apiBase })
|
|
59
|
+
chatRef.current = chat
|
|
60
|
+
const unsub = chat.subscribe(setState)
|
|
61
|
+
chat.init()
|
|
62
|
+
return unsub
|
|
63
|
+
}, [apiBase])
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
67
|
+
}, [state?.messages, state?.loading])
|
|
68
|
+
|
|
69
|
+
if (!state?.config) return null
|
|
70
|
+
|
|
71
|
+
const chat = chatRef.current!
|
|
72
|
+
const { theme, title, subtitle } = state.config.branding
|
|
73
|
+
|
|
74
|
+
const bubble = (isUser: boolean) => ({
|
|
75
|
+
background: isUser ? theme.userBubbleColor : theme.botBubbleColor,
|
|
76
|
+
color: isUser ? '#fff' : theme.textColor,
|
|
77
|
+
padding: '10px 14px', borderRadius: 12, maxWidth: '80%', fontSize: 14, lineHeight: 1.5,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div style={{
|
|
82
|
+
fontFamily: theme.fontFamily, color: theme.textColor, background: theme.backgroundColor,
|
|
83
|
+
width, height, borderRadius: 16, boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
|
|
84
|
+
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
|
85
|
+
}}>
|
|
86
|
+
<div style={{ background: theme.primaryColor, padding: '16px 20px', color: '#fff' }}>
|
|
87
|
+
<div style={{ fontWeight: 700, fontSize: 16 }}>{title}</div>
|
|
88
|
+
<div style={{ fontSize: 12, opacity: 0.85 }}>{subtitle}</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div style={{ flex: 1, overflowY: 'auto', padding: 16, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
92
|
+
{state.messages.map(msg => (
|
|
93
|
+
<div key={msg.id} style={{ display: 'flex', flexDirection: 'column', alignItems: msg.role === 'user' ? 'flex-end' : 'flex-start' }}>
|
|
94
|
+
<div style={bubble(msg.role === 'user')}>
|
|
95
|
+
{msg.subquestions && msg.subquestions.length > 0 ? (
|
|
96
|
+
<>
|
|
97
|
+
{msg.isFollowUp && <div style={{ marginBottom: 8 }}><MarkdownText text={msg.text} /></div>}
|
|
98
|
+
{msg.subquestions.map((sub: Question) => (
|
|
99
|
+
<button key={sub.id} onClick={() => chat.clickSub(sub, msg.subquestions!)} style={{
|
|
100
|
+
display: 'block', width: '100%', textAlign: 'left',
|
|
101
|
+
padding: '7px 10px', margin: '4px 0', borderRadius: 8,
|
|
102
|
+
border: `1px solid ${theme.primaryColor}`, background: '#fff',
|
|
103
|
+
color: theme.primaryColor, cursor: 'pointer', fontSize: 13,
|
|
104
|
+
}}>
|
|
105
|
+
{sub.question}
|
|
106
|
+
</button>
|
|
107
|
+
))}
|
|
108
|
+
<button onClick={() => chat.backToMain()} style={{
|
|
109
|
+
display: 'block', width: '100%', textAlign: 'left',
|
|
110
|
+
padding: '7px 10px', margin: '4px 0', borderRadius: 8,
|
|
111
|
+
border: '1px solid #6B7280', background: '#fff',
|
|
112
|
+
color: '#6B7280', cursor: 'pointer', fontSize: 13,
|
|
113
|
+
}}>
|
|
114
|
+
← Back to main menu
|
|
115
|
+
</button>
|
|
116
|
+
</>
|
|
117
|
+
) : msg.role === 'bot' ? <MarkdownText text={msg.text} /> : msg.text}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
))}
|
|
121
|
+
|
|
122
|
+
{state.loading && (
|
|
123
|
+
<div style={{ alignSelf: 'flex-start', background: theme.botBubbleColor, padding: '10px 16px', borderRadius: 12, fontSize: 14 }}>
|
|
124
|
+
<ThinkingDots color={theme.primaryColor} />
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{state.showMain && state.questions.length > 0 && (
|
|
129
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 }}>
|
|
130
|
+
{state.questions.map((q: Question) => (
|
|
131
|
+
<button key={q.id} onClick={() => chat.clickMain(q)} style={{
|
|
132
|
+
textAlign: 'left', padding: '8px 12px', borderRadius: 8,
|
|
133
|
+
border: `1px solid ${theme.primaryColor}`, background: 'transparent',
|
|
134
|
+
color: theme.primaryColor, cursor: 'pointer', fontSize: 14,
|
|
135
|
+
}}>
|
|
136
|
+
{q.question}
|
|
137
|
+
</button>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<div ref={bottomRef} />
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<form onSubmit={e => { e.preventDefault(); chat.submitQuery() }} style={{ display: 'flex', borderTop: '1px solid #e5e7eb', padding: 12, gap: 8 }}>
|
|
146
|
+
<input
|
|
147
|
+
value={state.input}
|
|
148
|
+
onChange={e => chat.setInput(e.target.value)}
|
|
149
|
+
placeholder="Type a message..."
|
|
150
|
+
disabled={state.loading}
|
|
151
|
+
style={{ flex: 1, padding: '8px 12px', borderRadius: 8, border: '1px solid #e5e7eb', fontSize: 14, outline: 'none' }}
|
|
152
|
+
/>
|
|
153
|
+
<button type="submit" disabled={state.loading} style={{
|
|
154
|
+
background: theme.primaryColor, color: '#fff', border: 'none',
|
|
155
|
+
borderRadius: 8, padding: '8px 16px',
|
|
156
|
+
cursor: state.loading ? 'not-allowed' : 'pointer', fontSize: 14,
|
|
157
|
+
}}>
|
|
158
|
+
Send
|
|
159
|
+
</button>
|
|
160
|
+
</form>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { marked } from 'marked'
|
|
2
|
+
import type { Message, Question, WidgetSettings } from '../shared/types'
|
|
3
|
+
|
|
4
|
+
marked.setOptions({ breaks: true })
|
|
5
|
+
|
|
6
|
+
export interface RushChatbotConfig {
|
|
7
|
+
apiBase?: string
|
|
8
|
+
settings?: WidgetSettings
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RushChatbotState {
|
|
12
|
+
messages: Message[]
|
|
13
|
+
questions: Question[]
|
|
14
|
+
input: string
|
|
15
|
+
loading: boolean
|
|
16
|
+
showMain: boolean
|
|
17
|
+
config: { branding: any; chat: any } | null
|
|
18
|
+
settings: WidgetSettings
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Listener = (state: RushChatbotState) => void
|
|
22
|
+
|
|
23
|
+
function generateSessionId(): string {
|
|
24
|
+
return 'sess_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getSessionId(): string {
|
|
28
|
+
let id = sessionStorage.getItem('rushchatbot_session')
|
|
29
|
+
if (!id) { id = generateSessionId(); sessionStorage.setItem('rushchatbot_session', id) }
|
|
30
|
+
return id
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseMarkdown(text: string): string {
|
|
34
|
+
return marked.parse(text) as string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type { WidgetSettings } from '../shared/types'
|
|
38
|
+
|
|
39
|
+
export class RushChatbot {
|
|
40
|
+
private base: string
|
|
41
|
+
private listeners: Listener[] = []
|
|
42
|
+
private state: RushChatbotState = {
|
|
43
|
+
messages: [],
|
|
44
|
+
questions: [],
|
|
45
|
+
input: '',
|
|
46
|
+
loading: false,
|
|
47
|
+
showMain: true,
|
|
48
|
+
config: null,
|
|
49
|
+
settings: {},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
constructor(options: RushChatbotConfig = {}) {
|
|
53
|
+
this.base = options.apiBase ?? 'http://localhost:3001/api/chat'
|
|
54
|
+
if (options.settings) this.state.settings = options.settings
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getState(): RushChatbotState {
|
|
58
|
+
return { ...this.state }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
subscribe(listener: Listener): () => void {
|
|
62
|
+
this.listeners.push(listener)
|
|
63
|
+
return () => { this.listeners = this.listeners.filter(l => l !== listener) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private setState(patch: Partial<RushChatbotState>) {
|
|
67
|
+
this.state = { ...this.state, ...patch }
|
|
68
|
+
this.listeners.forEach(l => l(this.getState()))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private addMessage(msg: Omit<Message, 'id'>) {
|
|
72
|
+
const messages = [...this.state.messages, { ...msg, id: Date.now().toString() + Math.random() }]
|
|
73
|
+
this.setState({ messages })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async think(ms = 1000) {
|
|
77
|
+
this.setState({ loading: true })
|
|
78
|
+
await new Promise(r => setTimeout(r, ms))
|
|
79
|
+
this.setState({ loading: false })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private followUp(): string {
|
|
83
|
+
const phrases = [
|
|
84
|
+
'Is there anything else I can help you with today? 😊',
|
|
85
|
+
'Happy to help further! What else would you like to know?',
|
|
86
|
+
'I hope that was helpful! Feel free to ask anything else.',
|
|
87
|
+
"You're in good hands! Is there something else on your mind?",
|
|
88
|
+
"I'm here for you — what else can I assist you with?",
|
|
89
|
+
]
|
|
90
|
+
return phrases[Math.floor(Math.random() * phrases.length)]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async init() {
|
|
94
|
+
const [cfg, qs] = await Promise.all([
|
|
95
|
+
fetch(`${this.base}/config`).then(r => r.json()),
|
|
96
|
+
fetch(`${this.base}/questions`).then(r => r.json()),
|
|
97
|
+
])
|
|
98
|
+
this.setState({
|
|
99
|
+
config: cfg,
|
|
100
|
+
questions: qs,
|
|
101
|
+
messages: [{ id: '0', role: 'bot', text: cfg.chat.welcomeMessage }],
|
|
102
|
+
showMain: cfg.chat.showPreQuestionsOnStart ?? true,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setInput(value: string) {
|
|
107
|
+
this.setState({ input: value })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async clickMain(q: Question) {
|
|
111
|
+
this.setState({ showMain: false })
|
|
112
|
+
this.addMessage({ role: 'user', text: q.question })
|
|
113
|
+
await fetch(`${this.base}/click`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json', 'x-session-id': getSessionId() },
|
|
116
|
+
body: JSON.stringify({ questionId: q.id, sessionId: getSessionId() }),
|
|
117
|
+
})
|
|
118
|
+
await this.think()
|
|
119
|
+
if (q.subquestions?.length > 0) {
|
|
120
|
+
this.addMessage({ role: 'bot', text: `Here are some topics under **${q.question}**:`, source: 'config', subquestions: q.subquestions })
|
|
121
|
+
} else {
|
|
122
|
+
this.addMessage({ role: 'bot', text: q.answer, source: 'config' })
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async clickSub(q: Question, siblings: Question[]) {
|
|
127
|
+
this.addMessage({ role: 'user', text: q.question })
|
|
128
|
+
await fetch(`${this.base}/click`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Content-Type': 'application/json', 'x-session-id': getSessionId() },
|
|
131
|
+
body: JSON.stringify({ questionId: q.id, sessionId: getSessionId() }),
|
|
132
|
+
})
|
|
133
|
+
await this.think()
|
|
134
|
+
this.addMessage({ role: 'bot', text: q.answer, source: 'config' })
|
|
135
|
+
await this.think(800)
|
|
136
|
+
this.addMessage({ role: 'bot', text: 'Would you have another inquiry?', subquestions: siblings, isFollowUp: true })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async backToMain() {
|
|
140
|
+
this.setState({ showMain: false })
|
|
141
|
+
this.addMessage({ role: 'bot', text: "Here's what I can help you with — feel free to pick a topic or type your own question! 😊" })
|
|
142
|
+
await this.think(800)
|
|
143
|
+
this.setState({ showMain: true })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
endChat() {
|
|
147
|
+
this.setState({ showMain: false })
|
|
148
|
+
this.addMessage({ role: 'bot', text: 'Thank you for chatting with us! Have a wonderful day. Feel free to come back anytime you need help. 👋😊', isEndChat: true })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async startNewChat() {
|
|
152
|
+
const welcome = this.state.config?.chat.welcomeMessage ?? ''
|
|
153
|
+
this.setState({ messages: [{ id: '0', role: 'bot', text: welcome }], showMain: false })
|
|
154
|
+
await this.think(400)
|
|
155
|
+
this.setState({ showMain: true })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async submitQuery() {
|
|
159
|
+
const query = this.state.input.trim()
|
|
160
|
+
if (!query) return
|
|
161
|
+
this.setState({ input: '', showMain: false })
|
|
162
|
+
this.addMessage({ role: 'user', text: query })
|
|
163
|
+
this.setState({ loading: true })
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const sid = getSessionId()
|
|
167
|
+
const res = await fetch(`${this.base}/query?query=${encodeURIComponent(query)}&sessionId=${sid}`, {
|
|
168
|
+
headers: { 'x-session-id': sid },
|
|
169
|
+
}).then(r => r.json())
|
|
170
|
+
|
|
171
|
+
this.setState({ loading: false })
|
|
172
|
+
|
|
173
|
+
if (res.paragraphs?.length > 0) {
|
|
174
|
+
for (const p of res.paragraphs) {
|
|
175
|
+
this.addMessage({ role: 'bot', text: p, source: res.source })
|
|
176
|
+
await this.think(400)
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
this.addMessage({ role: 'bot', text: res.answer, source: res.source })
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
this.setState({ loading: false })
|
|
183
|
+
this.addMessage({ role: 'bot', text: "Oops! It seems like I ran into a little hiccup. 😅 Please try again in a moment, or feel free to explore the topics below — I'm happy to help!", isFollowUp: true, subquestions: [] })
|
|
184
|
+
this.setState({ showMain: true })
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await this.think(800)
|
|
189
|
+
this.addMessage({ role: 'bot', text: this.followUp(), isFollowUp: true, subquestions: [] })
|
|
190
|
+
this.setState({ showMain: true })
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createServer } from './server/index'
|
|
2
|
+
export type { RushChatbotServerOptions } from './server/index'
|
|
3
|
+
export type { ChatbotConfig, Question, Message } from './shared/types'
|
|
4
|
+
export { RushChatbot, parseMarkdown } from './core/RushChatbot'
|
|
5
|
+
export type { RushChatbotConfig, RushChatbotState } from './core/RushChatbot'
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Filter } from 'bad-words'
|
|
2
|
+
|
|
3
|
+
const filter = new Filter()
|
|
4
|
+
|
|
5
|
+
// Additional rude/aggressive phrases beyond profanity
|
|
6
|
+
const RUDE_PATTERNS = [
|
|
7
|
+
/\bstupid\b/i,
|
|
8
|
+
/\bidiot\b/i,
|
|
9
|
+
/\bmoron\b/i,
|
|
10
|
+
/\bdumb\b/i,
|
|
11
|
+
/\buseless\b/i,
|
|
12
|
+
/\bworthless\b/i,
|
|
13
|
+
/\bterrible\b/i,
|
|
14
|
+
/\bawful\b/i,
|
|
15
|
+
/\bhate\s+you\b/i,
|
|
16
|
+
/\bshut\s+up\b/i,
|
|
17
|
+
/\bgo\s+away\b/i,
|
|
18
|
+
/\byou\s+suck\b/i,
|
|
19
|
+
/\bwaste\s+of\s+time\b/i,
|
|
20
|
+
/\bscam\b/i,
|
|
21
|
+
/\bfraud\b/i,
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const GUARDRAIL_RESPONSES = [
|
|
25
|
+
"We truly value your time and want to make sure we're helping you in the best way possible. Let's keep our conversation respectful so we can assist you better. 😊 How can we help you today?",
|
|
26
|
+
"We understand that things can be frustrating sometimes, and we're here to help! Let's work together to find the answers you need. What can we assist you with?",
|
|
27
|
+
"Your experience matters to us, and we want to make sure you get the best support possible. Let's start fresh — what would you like to know?",
|
|
28
|
+
"We're committed to providing you with a positive and helpful experience. Let's keep things friendly so we can serve you better! What can we help you with today?",
|
|
29
|
+
"We're here to help and we want to make this a great experience for you! Let's take a step back and see how we can best assist you. 😊",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const INVALID_INPUT_RESPONSES = [
|
|
33
|
+
"Hmm, it seems like that message didn't quite come through. Could you rephrase your question? We'd love to help! 😊",
|
|
34
|
+
"We want to make sure we understand you correctly! Could you try asking your question in a different way?",
|
|
35
|
+
"It looks like we didn't catch that. Could you provide a bit more detail so we can assist you better?",
|
|
36
|
+
"We're here to help, but we need a little more to go on! Could you try rephrasing that for us?",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
// Minimum valid word length and count
|
|
40
|
+
const MIN_WORD_LENGTH = 2
|
|
41
|
+
const MIN_VALID_WORDS = 1
|
|
42
|
+
const MAX_INPUT_LENGTH = 500
|
|
43
|
+
|
|
44
|
+
// Detect if input is gibberish — too many consecutive consonants or random chars
|
|
45
|
+
function isGibberish(word: string): boolean {
|
|
46
|
+
if (word.length < 2) return false
|
|
47
|
+
const consonantRun = /[bcdfghjklmnpqrstvwxyz]{5,}/i
|
|
48
|
+
const randomChars = /[^a-z0-9\s]{3,}/i
|
|
49
|
+
const repeatingChars = /(.)\1{4,}/i
|
|
50
|
+
return consonantRun.test(word) || randomChars.test(word) || repeatingChars.test(word)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ValidationResult {
|
|
54
|
+
valid: boolean
|
|
55
|
+
response?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateInput(text: string): ValidationResult {
|
|
59
|
+
const trimmed = text.trim()
|
|
60
|
+
|
|
61
|
+
// Empty or too short
|
|
62
|
+
if (!trimmed || trimmed.length < MIN_WORD_LENGTH) {
|
|
63
|
+
return { valid: false, response: "Please type a message so we can assist you! 😊" }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Too long
|
|
67
|
+
if (trimmed.length > MAX_INPUT_LENGTH) {
|
|
68
|
+
return { valid: false, response: "Your message is a bit too long. Could you summarize your question so we can help you better?" }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Only numbers or special characters
|
|
72
|
+
if (/^[^a-zA-Z]+$/.test(trimmed)) {
|
|
73
|
+
return { valid: false, response: INVALID_INPUT_RESPONSES[Math.floor(Math.random() * INVALID_INPUT_RESPONSES.length)] }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for gibberish words
|
|
77
|
+
const words = trimmed.split(/\s+/).filter(w => w.length >= MIN_WORD_LENGTH)
|
|
78
|
+
const gibberishWords = words.filter(isGibberish)
|
|
79
|
+
if (words.length > 0 && gibberishWords.length / words.length > 0.6) {
|
|
80
|
+
return { valid: false, response: INVALID_INPUT_RESPONSES[Math.floor(Math.random() * INVALID_INPUT_RESPONSES.length)] }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Not enough valid words
|
|
84
|
+
const validWords = words.filter(w => /[a-zA-Z]/.test(w))
|
|
85
|
+
if (validWords.length < MIN_VALID_WORDS) {
|
|
86
|
+
return { valid: false, response: INVALID_INPUT_RESPONSES[Math.floor(Math.random() * INVALID_INPUT_RESPONSES.length)] }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { valid: true }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface GuardrailResult {
|
|
93
|
+
flagged: boolean
|
|
94
|
+
response?: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function checkGuardrails(text: string): GuardrailResult {
|
|
98
|
+
const hasProfanity = filter.isProfane(text)
|
|
99
|
+
const hasRudeness = RUDE_PATTERNS.some(pattern => pattern.test(text))
|
|
100
|
+
|
|
101
|
+
if (hasProfanity || hasRudeness) {
|
|
102
|
+
const response = GUARDRAIL_RESPONSES[Math.floor(Math.random() * GUARDRAIL_RESPONSES.length)]
|
|
103
|
+
return { flagged: true, response }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { flagged: false }
|
|
107
|
+
}
|