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.
Files changed (51) hide show
  1. package/INTEGRATION.md +559 -0
  2. package/index.html +13 -0
  3. package/package/INTEGRATION.md +717 -0
  4. package/package/README.md +717 -0
  5. package/package/package-lock.json +3221 -0
  6. package/package/package.json +101 -0
  7. package/package/src/cli/index.ts +94 -0
  8. package/package/src/client/ChatWidget.tsx +163 -0
  9. package/package/src/client.ts +2 -0
  10. package/package/src/core/RushChatbot.ts +192 -0
  11. package/package/src/index.ts +5 -0
  12. package/package/src/server/guardrails.ts +107 -0
  13. package/package/src/server/index.ts +210 -0
  14. package/package/src/server/matcher.ts +140 -0
  15. package/package/src/shared/types.ts +58 -0
  16. package/package/src/shims-vue.d.ts +5 -0
  17. package/package/src/vanilla/index.ts +302 -0
  18. package/package/src/vanilla.ts +3 -0
  19. package/package/src/vue/ChatWidget.vue +198 -0
  20. package/package/src/vue.ts +3 -0
  21. package/package/src/vue2/ChatWidget.vue +204 -0
  22. package/package/src/vue2.ts +3 -0
  23. package/package/tsconfig.json +19 -0
  24. package/package/vite.config.ts +86 -0
  25. package/package.json +35 -0
  26. package/public/favicon.svg +1 -0
  27. package/public/icons.svg +24 -0
  28. package/server/data/analytics.json +795 -0
  29. package/server/data/chatbot.config.json +26 -0
  30. package/server/data/questions.json +238 -0
  31. package/server/data/questions.tl.json +182 -0
  32. package/server/index.ts +11 -0
  33. package/server/lib/guardrails.ts +107 -0
  34. package/server/lib/matcher.ts +140 -0
  35. package/server/routes/chat.ts +180 -0
  36. package/server/tsconfig.json +11 -0
  37. package/src/App.css +184 -0
  38. package/src/App.tsx +135 -0
  39. package/src/assets/hero.png +0 -0
  40. package/src/assets/react.svg +1 -0
  41. package/src/assets/vite.svg +1 -0
  42. package/src/components/ChatWidget.tsx +417 -0
  43. package/src/components/QuestionTree.tsx +57 -0
  44. package/src/hooks/useChatData.ts +49 -0
  45. package/src/index.css +111 -0
  46. package/src/main.tsx +10 -0
  47. package/src/types/index.ts +40 -0
  48. package/tsconfig.app.json +26 -0
  49. package/tsconfig.json +7 -0
  50. package/tsconfig.node.json +23 -0
  51. package/vite.config.ts +7 -0
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="77" height="47" fill="none" aria-labelledby="vite-logo-title" viewBox="0 0 77 47"><title id="vite-logo-title">Vite</title><style>.parenthesis{fill:#000}@media (prefers-color-scheme:dark){.parenthesis{fill:#fff}}</style><path fill="#9135ff" d="M40.151 45.71c-.663.844-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.493c-.92 0-1.457-1.04-.92-1.788l7.479-10.471c1.07-1.498 0-3.578-1.842-3.578H15.443c-.92 0-1.456-1.04-.92-1.788l9.696-13.576c.213-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.472c-1.07 1.497 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.087.89 1.83L40.153 45.712z"/><mask id="a" width="48" height="47" x="14" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M40.047 45.71c-.663.843-2.02.374-2.02-.699V34.708a2.26 2.26 0 0 0-2.262-2.262H24.389c-.92 0-1.457-1.04-.92-1.788l7.479-10.472c1.07-1.497 0-3.578-1.842-3.578H15.34c-.92 0-1.456-1.04-.92-1.788l9.696-13.575c.213-.297.556-.474.92-.474H53.93c.92 0 1.456 1.04.92 1.788L47.37 13.03c-1.07 1.498 0 3.578 1.842 3.578h11.376c.944 0 1.474 1.088.89 1.831L40.049 45.712z"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#eee6ff" rx="5.508" ry="14.704" transform="rotate(269.814 20.96 11.29)scale(-1 1)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#eee6ff" rx="10.399" ry="29.851" transform="rotate(89.814 -16.902 -8.275)scale(1 -1)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#8900ff" rx="5.508" ry="30.487" transform="rotate(89.814 -19.197 -7.127)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.928 4.177)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#8900ff" rx="5.508" ry="30.599" transform="rotate(89.814 -25.738 5.52)scale(1 -1)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#eee6ff" rx="14.072" ry="22.078" transform="rotate(93.35 31.245 55.578)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#8900ff" rx="3.47" ry="21.501" transform="rotate(89.009 35.419 55.202)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx="14.592" cy="9.743" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(39.51 14.592 9.743)"/></g><g filter="url(#k)"><ellipse cx="61.728" cy="-5.321" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 61.728 -5.32)"/></g><g filter="url(#l)"><ellipse cx="55.618" cy="7.104" fill="#00c2ff" rx="5.971" ry="9.665" transform="rotate(37.892 55.618 7.104)"/></g><g filter="url(#m)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#n)"><ellipse cx="12.326" cy="39.103" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 12.326 39.103)"/></g><g filter="url(#o)"><ellipse cx="49.857" cy="30.678" fill="#8900ff" rx="4.407" ry="29.108" transform="rotate(37.892 49.857 30.678)"/></g><g filter="url(#p)"><ellipse cx="52.623" cy="33.171" fill="#00c2ff" rx="5.971" ry="15.297" transform="rotate(37.892 52.623 33.17)"/></g></g><path d="M6.919 0c-9.198 13.166-9.252 33.575 0 46.789h6.215c-9.25-13.214-9.196-33.623 0-46.789zm62.424 0h-6.215c9.198 13.166 9.252 33.575 0 46.789h6.215c9.25-13.214 9.196-33.623 0-46.789" class="parenthesis"/><defs><filter id="b" width="60.045" height="41.654" x="-5.564" y="16.92" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-40.407" y="-6.762" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-35.435" y="2.801" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-30.84" y="20.8" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-29.307" y="21.949" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="29.961" y="-17.13" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="37.754" y="3.055" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-13.43" y="-22.082" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="34.321" y="-37.644" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="38.847" y="-10.552" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-15.081" y="6.78" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="22.45" y="-1.645" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="32.919" y="11.36" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17286" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,417 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { marked } from 'marked'
3
+ import type { Message, Question } from '../types'
4
+ import { useChatData, sendQuery, trackClick } from '../hooks/useChatData'
5
+
6
+ marked.setOptions({ breaks: true })
7
+
8
+ function MarkdownText({ text }: { text: string }) {
9
+ return (
10
+ <div
11
+ dangerouslySetInnerHTML={{ __html: marked.parse(text) as string }}
12
+ className='markdown'
13
+ style={{ lineHeight: 1.6 }}
14
+ />
15
+ )
16
+ }
17
+
18
+ export default function ChatWidget({ onClose }: { onClose?: () => void }) {
19
+ const { config, questions } = useChatData()
20
+ const [messages, setMessages] = useState<Message[]>([])
21
+ const [input, setInput] = useState('')
22
+ const [loading, setLoading] = useState(false)
23
+ const [showMainQuestions, setShowMainQuestions] = useState(true)
24
+ const bottomRef = useRef<HTMLDivElement>(null)
25
+ const scrollRef = useRef<HTMLDivElement>(null)
26
+ const [showScrollBtn, setShowScrollBtn] = useState(false)
27
+
28
+ useEffect(() => {
29
+ if (config) {
30
+ setMessages([{ id: '0', role: 'bot', text: config.chat.welcomeMessage }])
31
+ setShowMainQuestions(config.chat.showPreQuestionsOnStart)
32
+ }
33
+ }, [config])
34
+
35
+ useEffect(() => {
36
+ const el = scrollRef.current
37
+ if (!el) return
38
+ const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60
39
+ if (isAtBottom) {
40
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
41
+ setShowScrollBtn(false)
42
+ } else {
43
+ setShowScrollBtn(true)
44
+ }
45
+ }, [messages, loading])
46
+
47
+ // Check overflow when initial FAQ menu renders
48
+ useEffect(() => {
49
+ const el = scrollRef.current
50
+ if (!el || !showMainQuestions) return
51
+ setTimeout(() => {
52
+ const overflows = el.scrollHeight > el.clientHeight + 10
53
+ setShowScrollBtn(overflows)
54
+ }, 50)
55
+ }, [showMainQuestions, questions])
56
+
57
+ function handleScroll() {
58
+ const el = scrollRef.current
59
+ if (!el) return
60
+ const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60
61
+ if (isAtBottom) setShowScrollBtn(false)
62
+ }
63
+
64
+ function scrollToBottom() {
65
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
66
+ setShowScrollBtn(false)
67
+ }
68
+
69
+ if (!config) return null
70
+
71
+ const { theme, title, subtitle } = config.branding
72
+
73
+ function addMessage(msg: Omit<Message, 'id'>) {
74
+ setMessages(prev => [...prev, { ...msg, id: Date.now().toString() }])
75
+ }
76
+
77
+ async function think(ms = 1000) {
78
+ setLoading(true)
79
+ await new Promise(r => setTimeout(r, ms))
80
+ setLoading(false)
81
+ }
82
+
83
+ function followUpPhrase() {
84
+ const phrases = [
85
+ 'Is there anything else I can help you with today? 😊',
86
+ 'Happy to help further! What else would you like to know?',
87
+ 'I hope that was helpful! Feel free to ask anything else.',
88
+ 'You\'re in good hands! Is there something else on your mind?',
89
+ 'I\'m here for you — what else can I assist you with?',
90
+ ]
91
+ return phrases[Math.floor(Math.random() * phrases.length)]
92
+ }
93
+
94
+ async function handleMainQuestionClick(q: Question) {
95
+ setShowMainQuestions(false)
96
+ addMessage({ role: 'user', text: q.question })
97
+ await trackClick(q.id)
98
+
99
+ if (q.subquestions?.length > 0) {
100
+ await think()
101
+ addMessage({ role: 'bot', text: `Here are some topics under **${q.question}**:`, source: 'config', subquestions: q.subquestions } as Message)
102
+ } else {
103
+ await think()
104
+ addMessage({ role: 'bot', text: q.answer, source: 'config' })
105
+ }
106
+ }
107
+
108
+ async function handleSubQuestionClick(q: Question, siblings: Question[]) {
109
+ addMessage({ role: 'user', text: q.question })
110
+ await trackClick(q.id)
111
+ await think()
112
+ addMessage({ role: 'bot', text: q.answer, source: 'config' })
113
+ await think(800)
114
+ addMessage({ role: 'bot', text: 'Would you have another inquiry?', subquestions: siblings, isFollowUp: true })
115
+ }
116
+
117
+ async function handleSubmit(e: React.FormEvent) {
118
+ e.preventDefault()
119
+ if (!input.trim()) return
120
+ const query = input.trim()
121
+ setInput('')
122
+ setShowMainQuestions(false)
123
+ addMessage({ role: 'user', text: query })
124
+ setLoading(true)
125
+ try {
126
+ const res = await sendQuery(query)
127
+ setLoading(false)
128
+ if (res.paragraphs?.length > 0) {
129
+ for (const p of res.paragraphs) {
130
+ addMessage({ role: 'bot', text: p, source: res.source })
131
+ await think(400)
132
+ }
133
+ } else {
134
+ addMessage({ role: 'bot', text: res.answer, source: res.source })
135
+ }
136
+ } catch {
137
+ setLoading(false)
138
+ 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: [] })
139
+ setShowMainQuestions(true)
140
+ return
141
+ }
142
+ await think(800)
143
+ addMessage({ role: 'bot', text: followUpPhrase(), isFollowUp: true, subquestions: [] })
144
+ setShowMainQuestions(true)
145
+ }
146
+
147
+ return (
148
+ <div style={{
149
+ fontFamily: theme.fontFamily,
150
+ color: theme.textColor,
151
+ background: theme.backgroundColor,
152
+ width: 380,
153
+ height: 580,
154
+ borderRadius: 16,
155
+ boxShadow: '0 8px 32px rgba(0,0,0,0.15)',
156
+ display: 'flex',
157
+ flexDirection: 'column',
158
+ overflow: 'hidden',
159
+ }}>
160
+ {/* Header */}
161
+ <div style={{ background: theme.primaryColor, padding: '16px 20px', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
162
+ <div>
163
+ <div style={{ fontWeight: 700, fontSize: 16 }}>{title}</div>
164
+ <div style={{ fontSize: 12, opacity: 0.85 }}>{subtitle}</div>
165
+ </div>
166
+ {onClose && (
167
+ <button onClick={onClose} style={{
168
+ background: 'transparent', border: 'none', color: '#fff',
169
+ fontSize: 18, cursor: 'pointer', lineHeight: 1, padding: 4, opacity: 0.8,
170
+ }} aria-label="Close chat">
171
+ ✕
172
+ </button>
173
+ )}
174
+ </div>
175
+
176
+ {/* Messages */}
177
+ <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
178
+ {showScrollBtn && (
179
+ <button
180
+ onClick={scrollToBottom}
181
+ style={{
182
+ position: 'absolute', bottom: 12, right: 12, zIndex: 10,
183
+ width: 34, height: 34, borderRadius: '50%',
184
+ background: theme.primaryColor, color: '#fff',
185
+ border: 'none', cursor: 'pointer',
186
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
187
+ boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
188
+ }}
189
+ aria-label="Scroll to latest"
190
+ >
191
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
192
+ <path d="M8 3v10M3 9l5 5 5-5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
193
+ </svg>
194
+ </button>
195
+ )}
196
+ <div ref={scrollRef} onScroll={handleScroll} style={{ height: '100%', overflowY: 'auto', padding: '16px 16px 16px 16px', paddingTop: 20, display: 'flex', flexDirection: 'column', gap: 8 }}>
197
+ {messages.map((msg, idx) => (
198
+ <div key={msg.id} style={{ display: 'flex', flexDirection: 'column', alignItems: msg.role === 'user' ? 'flex-end' : 'flex-start', ...(idx === 0 ? { marginTop: 48 } : {}) }}>
199
+ <div style={{
200
+ background: msg.role === 'user' ? theme.userBubbleColor : theme.botBubbleColor,
201
+ color: msg.role === 'user' ? '#fff' : theme.textColor,
202
+ padding: '10px 14px',
203
+ borderRadius: 12,
204
+ maxWidth: '80%',
205
+ fontSize: 14,
206
+ lineHeight: 1.5,
207
+ }}>
208
+ {msg.subquestions && msg.subquestions.length > 0
209
+ ? (
210
+ <>
211
+ <div style={{ marginBottom: 8, fontSize: 14 }}><MarkdownText text={msg.text} /></div>
212
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4, animation: 'fadeSlideUp 0.35s ease-out' }}>
213
+ {msg.subquestions.map(sub => (
214
+ <button
215
+ key={sub.id}
216
+ onClick={() => handleSubQuestionClick(sub, msg.subquestions!)}
217
+ style={{
218
+ display: 'block',
219
+ width: '100%',
220
+ textAlign: 'left',
221
+ padding: '8px 12px',
222
+ borderRadius: 8,
223
+ border: `1px solid ${theme.primaryColor}`,
224
+ background: '#fff',
225
+ color: theme.primaryColor,
226
+ cursor: 'pointer',
227
+ fontSize: 14,
228
+ }}
229
+ >
230
+ {sub.question}
231
+ </button>
232
+ ))}
233
+ <button
234
+ onClick={async () => {
235
+ setShowMainQuestions(false)
236
+ addMessage({ role: 'bot', text: "Here's what I can help you with — feel free to pick a topic or type your own question! 😊" })
237
+ await think(800)
238
+ setShowMainQuestions(true)
239
+ }}
240
+ style={{
241
+ display: 'block',
242
+ width: '100%',
243
+ textAlign: 'left',
244
+ padding: '8px 12px',
245
+ borderRadius: 8,
246
+ border: `1px solid #6B7280`,
247
+ background: '#fff',
248
+ color: '#6B7280',
249
+ cursor: 'pointer',
250
+ fontSize: 14,
251
+ }}
252
+ >
253
+ ← Back to main menu
254
+ </button>
255
+ </div>
256
+ </>
257
+ )
258
+ : msg.role === 'bot' ? (
259
+ <>
260
+ <MarkdownText text={msg.text} />
261
+ {msg.isEndChat && (
262
+ <button
263
+ onClick={() => {
264
+ setMessages([{ id: '0', role: 'bot', text: config.chat.welcomeMessage }])
265
+ setShowMainQuestions(false)
266
+ setTimeout(() => setShowMainQuestions(true), 400)
267
+ }}
268
+ style={{
269
+ display: 'block',
270
+ marginTop: 10,
271
+ padding: '7px 14px',
272
+ borderRadius: 8,
273
+ border: `1px solid ${theme.primaryColor}`,
274
+ background: theme.primaryColor,
275
+ color: '#fff',
276
+ cursor: 'pointer',
277
+ fontSize: 13,
278
+ width: '100%',
279
+ }}
280
+ >
281
+ 🔄 Start a new chat
282
+ </button>
283
+ )}
284
+ </>
285
+ ) : msg.text
286
+ }
287
+ </div>
288
+ </div>
289
+ ))}
290
+
291
+ {/* Thinking animation */}
292
+ {loading && (
293
+ <div style={{ alignSelf: 'flex-start', background: theme.botBubbleColor, padding: '10px 16px', borderRadius: 12, fontSize: 14 }}>
294
+ <ThinkingDots color={theme.primaryColor} />
295
+ </div>
296
+ )}
297
+
298
+ {/* Main questions */}
299
+ {showMainQuestions && questions.length > 0 && (
300
+ <div
301
+ style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4, animation: 'fadeSlideUp 0.35s ease-out' }}>
302
+ {questions.map(q => (
303
+ <button
304
+ key={q.id}
305
+ onClick={() => handleMainQuestionClick(q)}
306
+ style={{
307
+ textAlign: 'left',
308
+ padding: '8px 12px',
309
+ borderRadius: 8,
310
+ border: `1px solid ${theme.primaryColor}`,
311
+ background: 'transparent',
312
+ color: theme.primaryColor,
313
+ cursor: 'pointer',
314
+ fontSize: 14,
315
+ }}
316
+ >
317
+ {q.question}
318
+ </button>
319
+ ))}
320
+ <button
321
+ onClick={() => {
322
+ setShowMainQuestions(false)
323
+ 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 })
324
+ }}
325
+ style={{
326
+ textAlign: 'left',
327
+ padding: '8px 12px',
328
+ borderRadius: 8,
329
+ border: '1px solid #EF4444',
330
+ background: 'transparent',
331
+ color: '#EF4444',
332
+ cursor: 'pointer',
333
+ fontSize: 14,
334
+ marginTop: 4,
335
+ }}
336
+ >
337
+ 👋 I'm done, thanks!
338
+ </button>
339
+ </div>
340
+ )}
341
+
342
+ <div ref={bottomRef} />
343
+ </div>
344
+ </div>
345
+
346
+ {/* Input */}
347
+ <form onSubmit={handleSubmit} style={{ display: 'flex', borderTop: '1px solid #e5e7eb', padding: 12, gap: 8 }}>
348
+ <input
349
+ value={input}
350
+ onChange={e => setInput(e.target.value)}
351
+ placeholder="Type a message..."
352
+ disabled={loading}
353
+ style={{
354
+ flex: 1,
355
+ padding: '8px 12px',
356
+ borderRadius: 8,
357
+ border: '1px solid #e5e7eb',
358
+ fontSize: 14,
359
+ outline: 'none',
360
+ }}
361
+ />
362
+ <button
363
+ type="submit"
364
+ disabled={loading}
365
+ style={{
366
+ background: theme.primaryColor,
367
+ color: '#fff',
368
+ border: 'none',
369
+ borderRadius: 8,
370
+ padding: '8px 16px',
371
+ cursor: loading ? 'not-allowed' : 'pointer',
372
+ fontSize: 14,
373
+ }}
374
+ >
375
+ Send
376
+ </button>
377
+ </form>
378
+ </div>
379
+ )
380
+ }
381
+
382
+ function ThinkingDots({ color }: { color: string }) {
383
+ return (
384
+ <span style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
385
+ {[0, 1, 2].map(i => (
386
+ <span key={i} style={{
387
+ width: 7,
388
+ height: 7,
389
+ borderRadius: '50%',
390
+ background: color,
391
+ display: 'inline-block',
392
+ animation: `bounce 1.2s ease-in-out ${i * 0.2}s infinite`,
393
+ }} />
394
+ ))}
395
+ <style>{`
396
+ @keyframes bounce {
397
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
398
+ 40% { transform: translateY(-6px); opacity: 1; }
399
+ }
400
+ @keyframes fadeSlideUp {
401
+ from { opacity: 0; transform: translateY(12px); }
402
+ to { opacity: 1; transform: translateY(0); }
403
+ }
404
+ .markdown p { margin: 0 0 6px 0; }
405
+ .markdown p:last-child { margin-bottom: 0; }
406
+ .markdown strong { font-weight: 600; }
407
+ .markdown ul, .markdown ol { margin: 4px 0 4px 16px; padding: 0; }
408
+ .markdown li { margin-bottom: 2px; }
409
+ .markdown a { color: inherit; text-decoration: underline; }
410
+ .markdown h1, .markdown h2, .markdown h3 { margin: 4px 0 6px 0; font-weight: 700; line-height: 1.3; }
411
+ .markdown h1 { font-size: 1.1em; }
412
+ .markdown h2 { font-size: 1em; }
413
+ .markdown h3 { font-size: 0.95em; }
414
+ `}</style>
415
+ </span>
416
+ )
417
+ }
@@ -0,0 +1,57 @@
1
+ import { useState } from 'react'
2
+ import type { Question } from '../types'
3
+
4
+ interface Props {
5
+ questions: Question[]
6
+ onSelect: (q: Question) => void
7
+ primaryColor: string
8
+ }
9
+
10
+ function QuestionItem({ q, onSelect, primaryColor, depth }: {
11
+ q: Question
12
+ onSelect: (q: Question) => void
13
+ primaryColor: string
14
+ depth: number
15
+ }) {
16
+ const [open, setOpen] = useState(false)
17
+ const hasChildren = q.subquestions?.length > 0
18
+
19
+ return (
20
+ <div style={{ marginLeft: depth * 12 }}>
21
+ <button
22
+ onClick={() => {
23
+ if (hasChildren) setOpen(o => !o)
24
+ else onSelect(q)
25
+ }}
26
+ style={{
27
+ display: 'block',
28
+ width: '100%',
29
+ textAlign: 'left',
30
+ padding: '8px 12px',
31
+ margin: '4px 0',
32
+ borderRadius: 8,
33
+ border: `1px solid ${primaryColor}`,
34
+ background: 'transparent',
35
+ color: primaryColor,
36
+ cursor: 'pointer',
37
+ fontSize: 14,
38
+ }}
39
+ >
40
+ {hasChildren ? (open ? '▾ ' : '▸ ') : ''}{q.question}
41
+ </button>
42
+ {open && hasChildren && q.subquestions.map(sub => (
43
+ <QuestionItem key={sub.id} q={sub} onSelect={onSelect} primaryColor={primaryColor} depth={depth + 1} />
44
+ ))}
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export default function QuestionTree({ questions, onSelect, primaryColor }: Props) {
50
+ return (
51
+ <div>
52
+ {questions.map(q => (
53
+ <QuestionItem key={q.id} q={q} onSelect={onSelect} primaryColor={primaryColor} depth={0} />
54
+ ))}
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,49 @@
1
+ import { useEffect, useState } from 'react'
2
+ import axios from 'axios'
3
+ import type { ChatConfig, Question } from '../types'
4
+
5
+ const API = 'http://localhost:3001/api/chat'
6
+
7
+ function generateSessionId(): string {
8
+ return 'sess_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8)
9
+ }
10
+
11
+ export function getSessionId(): string {
12
+ let sessionId = sessionStorage.getItem('finex_session_id')
13
+ if (!sessionId) {
14
+ sessionId = generateSessionId()
15
+ sessionStorage.setItem('finex_session_id', sessionId)
16
+ }
17
+ return sessionId
18
+ }
19
+
20
+ export function useChatData() {
21
+ const [config, setConfig] = useState<ChatConfig | null>(null)
22
+ const [questions, setQuestions] = useState<Question[]>([])
23
+
24
+ useEffect(() => {
25
+ axios.get(`${API}/config`).then(r => setConfig(r.data))
26
+ axios.get(`${API}/questions`).then(r => setQuestions(r.data))
27
+ }, [])
28
+
29
+ return { config, questions }
30
+ }
31
+
32
+ export async function sendQuery(query: string) {
33
+ const sessionId = getSessionId()
34
+ const { data } = await axios.get(`${API}/query`, {
35
+ params: { query, sessionId },
36
+ headers: { 'x-session-id': sessionId },
37
+ timeout: 60000,
38
+ })
39
+ return data
40
+ }
41
+
42
+ export async function trackClick(questionId: string) {
43
+ const sessionId = getSessionId()
44
+ await axios.post(
45
+ `${API}/click`,
46
+ { questionId, sessionId },
47
+ { headers: { 'x-session-id': sessionId } }
48
+ )
49
+ }
package/src/index.css ADDED
@@ -0,0 +1,111 @@
1
+ :root {
2
+ --text: #6b6375;
3
+ --text-h: #08060d;
4
+ --bg: #fff;
5
+ --border: #e5e4e7;
6
+ --code-bg: #f4f3ec;
7
+ --accent: #aa3bff;
8
+ --accent-bg: rgba(170, 59, 255, 0.1);
9
+ --accent-border: rgba(170, 59, 255, 0.5);
10
+ --social-bg: rgba(244, 243, 236, 0.5);
11
+ --shadow:
12
+ rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
13
+
14
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
15
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
16
+ --mono: ui-monospace, Consolas, monospace;
17
+
18
+ font: 18px/145% var(--sans);
19
+ letter-spacing: 0.18px;
20
+ color-scheme: light dark;
21
+ color: var(--text);
22
+ background: var(--bg);
23
+ font-synthesis: none;
24
+ text-rendering: optimizeLegibility;
25
+ -webkit-font-smoothing: antialiased;
26
+ -moz-osx-font-smoothing: grayscale;
27
+
28
+ @media (max-width: 1024px) {
29
+ font-size: 16px;
30
+ }
31
+ }
32
+
33
+ @media (prefers-color-scheme: dark) {
34
+ :root {
35
+ --text: #9ca3af;
36
+ --text-h: #f3f4f6;
37
+ --bg: #16171d;
38
+ --border: #2e303a;
39
+ --code-bg: #1f2028;
40
+ --accent: #c084fc;
41
+ --accent-bg: rgba(192, 132, 252, 0.15);
42
+ --accent-border: rgba(192, 132, 252, 0.5);
43
+ --social-bg: rgba(47, 48, 58, 0.5);
44
+ --shadow:
45
+ rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
46
+ }
47
+
48
+ #social .button-icon {
49
+ filter: invert(1) brightness(2);
50
+ }
51
+ }
52
+
53
+ #root {
54
+ width: 1126px;
55
+ max-width: 100%;
56
+ margin: 0 auto;
57
+ text-align: center;
58
+ border-inline: 1px solid var(--border);
59
+ min-height: 100svh;
60
+ display: flex;
61
+ flex-direction: column;
62
+ box-sizing: border-box;
63
+ }
64
+
65
+ body {
66
+ margin: 0;
67
+ }
68
+
69
+ h1,
70
+ h2 {
71
+ font-family: var(--heading);
72
+ font-weight: 500;
73
+ color: var(--text-h);
74
+ }
75
+
76
+ h1 {
77
+ font-size: 56px;
78
+ letter-spacing: -1.68px;
79
+ margin: 32px 0;
80
+ @media (max-width: 1024px) {
81
+ font-size: 36px;
82
+ margin: 20px 0;
83
+ }
84
+ }
85
+ h2 {
86
+ font-size: 24px;
87
+ line-height: 118%;
88
+ letter-spacing: -0.24px;
89
+ margin: 0 0 8px;
90
+ @media (max-width: 1024px) {
91
+ font-size: 20px;
92
+ }
93
+ }
94
+ p {
95
+ margin: 0;
96
+ }
97
+
98
+ code,
99
+ .counter {
100
+ font-family: var(--mono);
101
+ display: inline-flex;
102
+ border-radius: 4px;
103
+ color: var(--text-h);
104
+ }
105
+
106
+ code {
107
+ font-size: 15px;
108
+ line-height: 135%;
109
+ padding: 4px 8px;
110
+ background: var(--code-bg);
111
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>
10
+ )