@netlify/spark-ui 0.0.46-alpha.4 → 0.0.47-alpha

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 (40) hide show
  1. package/dist/analytics/analytics.d.ts +1 -1
  2. package/dist/assets/ChatApp.css +1 -0
  3. package/dist/chunks/compat.module.BjTzMHXJ.js +296 -0
  4. package/dist/chunks/hooks.module.DeYLzLnU.js +177 -0
  5. package/dist/chunks/{useTimeout.DOkYFG64.js → useTimeout.DYuvQwFN.js} +1 -1
  6. package/dist/components/index.d.ts +1 -0
  7. package/dist/components/preact/ChatApp/ChatApp.d.ts +2 -0
  8. package/dist/components/preact/ChatApp/ChatApp.js +5346 -0
  9. package/dist/components/preact/Modal/Modal.js +1 -1
  10. package/dist/components/preact/SearchModal/components/SearchInput.js +2 -2
  11. package/dist/components/preact/SearchModal/components/SearchLink/SearchLink.js +1 -1
  12. package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +1 -1
  13. package/dist/components/preact/SearchModal/components/SearchToggle/SearchToggle.js +1 -1
  14. package/dist/components/preact/SearchModal/components/SearchWindow.js +1 -1
  15. package/dist/components/preact/SearchModal/hooks/useKeyboardNav.js +1 -1
  16. package/dist/components/preact/SearchModal/hooks/useSearch.js +2 -2
  17. package/dist/components/preact/SearchModal/hooks/useSearchHotkeys.js +1 -1
  18. package/dist/components/preact/SearchModal/hooks/useSearchLogger.d.ts +4 -4
  19. package/dist/components/preact/SearchModal/hooks/useSearchLogger.js +18 -18
  20. package/dist/components/preact/SearchModal/hooks/useSearchParams.js +1 -1
  21. package/dist/components/preact/SearchModal/hooks/useSearchToggles.js +1 -1
  22. package/dist/components/preact/SearchModal/types/search.types.d.ts +6 -0
  23. package/dist/components/preact/index.d.ts +1 -0
  24. package/dist/components/preact/index.js +7 -5
  25. package/package.json +4 -1
  26. package/packages/analytics/GoogleAnalytics.astro +16 -9
  27. package/packages/analytics/GoogleTagManager.astro +9 -7
  28. package/packages/analytics/GoogleTagManagerNoscript.astro +12 -9
  29. package/packages/analytics/analytics.ts +2 -2
  30. package/packages/components/CodeBlock.astro +208 -0
  31. package/packages/components/index.ts +1 -0
  32. package/packages/components/preact/ChatApp/ChatApp.css +142 -0
  33. package/packages/components/preact/ChatApp/ChatApp.tsx +237 -0
  34. package/packages/components/preact/SearchModal/hooks/useSearchLogger.ts +15 -18
  35. package/packages/components/preact/SearchModal/types/search.types.tsx +7 -0
  36. package/packages/components/preact/index.ts +1 -0
  37. package/packages/global.d.ts +5 -0
  38. package/packages/styles/code-block.css +249 -0
  39. package/dist/chunks/compat.module.DT16W4Wr.js +0 -175
  40. package/dist/chunks/hooks.module.CNV9mxng.js +0 -138
@@ -0,0 +1,142 @@
1
+ /* styles.css */
2
+
3
+ /* Main container */
4
+ .chatbot-container {
5
+ display: flex;
6
+ padding: 3rem;
7
+ height: 100vh;
8
+ background-color: #f7fafc;
9
+ }
10
+
11
+ .chatbot-wrapper {
12
+ display: flex;
13
+ flex-direction: column;
14
+ width: 100%;
15
+ max-width: 1024px;
16
+ margin: 0 auto;
17
+ background-color: white;
18
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
19
+ border-radius: 8px;
20
+ overflow: hidden;
21
+ }
22
+
23
+ /* Header */
24
+ .header {
25
+ background-color: #2d3748;
26
+ color: white;
27
+ padding: 1rem;
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ }
32
+
33
+ .header-left {
34
+ display: flex;
35
+ align-items: center;
36
+ }
37
+
38
+ .header-title {
39
+ font-weight: bold;
40
+ font-size: 1.25rem;
41
+ }
42
+
43
+ .new-chat-button {
44
+ display: flex;
45
+ align-items: center;
46
+ background-color: #4a5568;
47
+ border-radius: 0.375rem;
48
+ padding: 0.25rem 0.75rem;
49
+ font-size: 0.875rem;
50
+ }
51
+
52
+ .icon {
53
+ margin-right: 0.25rem;
54
+ }
55
+
56
+ /* Chat container */
57
+ .chat-container {
58
+ flex: 1;
59
+ overflow-y: auto;
60
+ padding: 1rem;
61
+ }
62
+
63
+ /* Error message */
64
+ .error-message {
65
+ background-color: #fed7d7;
66
+ border: 1px solid #fbd5d5;
67
+ color: #9b2c2c;
68
+ padding: 1rem;
69
+ border-radius: 0.375rem;
70
+ margin-bottom: 1rem;
71
+ }
72
+
73
+ .error-title {
74
+ font-weight: bold;
75
+ }
76
+
77
+ /* Message bubbles */
78
+ .message {
79
+ display: flex;
80
+ flex-direction: column;
81
+ margin-bottom: 1rem;
82
+ }
83
+
84
+ .message-right {
85
+ display: flex;
86
+ justify-content: flex-end;
87
+ }
88
+
89
+ .message-bubble {
90
+ padding: 1rem;
91
+ border-radius: 0.5rem;
92
+ max-width: 75%;
93
+ }
94
+
95
+ .user-message {
96
+ background-color: #4299e1;
97
+ color: white;
98
+ }
99
+
100
+ .bot-message {
101
+ background-color: #edf2f7;
102
+ color: #2d3748;
103
+ }
104
+
105
+ /* Input area */
106
+ .input-area {
107
+ border-top: 1px solid #e2e8f0;
108
+ padding: 1rem;
109
+ }
110
+
111
+ .input-form {
112
+ display: flex;
113
+ align-items: center;
114
+ }
115
+
116
+ .input-field {
117
+ flex: 1;
118
+ background-color: #edf2f7;
119
+ border-radius: 0.5rem;
120
+ padding: 0.5rem 1rem;
121
+ }
122
+
123
+ .input {
124
+ width: 100%;
125
+ background-color: transparent;
126
+ border: none;
127
+ outline: none;
128
+ }
129
+
130
+ .stop-button,
131
+ .send-button {
132
+ margin-left: 0.5rem;
133
+ background-color: #3182ce;
134
+ border-radius: 9999px;
135
+ padding: 0.5rem;
136
+ color: white;
137
+ }
138
+
139
+ .stop-icon {
140
+ width: 1rem;
141
+ height: 1rem;
142
+ }
@@ -0,0 +1,237 @@
1
+ import { useState, useRef, useEffect } from 'preact/hooks';
2
+ import * as marked from 'marked';
3
+ import { KapaProvider, useChat } from '@kapaai/react-sdk';
4
+
5
+ import './ChatApp.css';
6
+
7
+ function ChatApp() {
8
+ return (
9
+ <KapaProvider
10
+ integrationId="3c0789a6-c6a8-4fbc-b0cc-235f9e4a6351"
11
+ callbacks={{
12
+ askAI: {
13
+ onQuerySubmit: (data) => console.log('Question asked:', data.question),
14
+ },
15
+ }}
16
+ >
17
+ <ChatInterface />
18
+ </KapaProvider>
19
+ );
20
+ }
21
+
22
+ // Main App Component
23
+ const ChatInterface = () => {
24
+ const [inputMessage, setInputMessage] = useState('');
25
+ const [feedbackStates, setFeedbackStates] = useState({});
26
+ const [showCommentId, setShowCommentId] = useState(null);
27
+ const chatRef = useRef(null);
28
+ const {
29
+ conversation,
30
+ submitQuery,
31
+ isGeneratingAnswer,
32
+ isPreparingAnswer,
33
+ resetConversation,
34
+ stopGeneration,
35
+ addFeedback,
36
+ error,
37
+ } = useChat();
38
+
39
+ // Configure marked options for better code highlighting
40
+ useEffect(() => {
41
+ marked.setOptions({
42
+ breaks: true,
43
+ gfm: true,
44
+ headerIds: false,
45
+ });
46
+ }, []);
47
+
48
+ const handleSubmit = (e) => {
49
+ if (e) e.preventDefault();
50
+ if (inputMessage.trim() && !isGeneratingAnswer && !isPreparingAnswer) {
51
+ submitQuery(inputMessage);
52
+ setInputMessage('');
53
+ }
54
+ };
55
+
56
+ const handleClearThread = () => {
57
+ resetConversation();
58
+ setFeedbackStates({});
59
+ setShowCommentId(null);
60
+ };
61
+
62
+ const handleCopyText = (text) => {
63
+ navigator.clipboard.writeText(text);
64
+
65
+ // Show a temporary copied message
66
+ const target = event.currentTarget;
67
+ const buttons = document.querySelectorAll('.copy-active');
68
+ buttons.forEach((button) => {
69
+ button.classList.remove('copy-active');
70
+ const textEl = button.querySelector('.copy-text');
71
+ if (textEl) textEl.textContent = '';
72
+ });
73
+
74
+ // Now set the current button to show "Copied!"
75
+ target.classList.add('copy-active');
76
+ const textEl = target.querySelector('.copy-text');
77
+ if (textEl) textEl.textContent = 'Copied!';
78
+
79
+ setTimeout(() => {
80
+ target.classList.remove('copy-active');
81
+ if (textEl) textEl.textContent = '';
82
+ }, 2000);
83
+ };
84
+
85
+ const handleFeedback = (qaId, reaction) => {
86
+ addFeedback(qaId, reaction);
87
+
88
+ // Update local feedback state
89
+ setFeedbackStates((prev) => ({
90
+ ...prev,
91
+ [qaId]: reaction,
92
+ }));
93
+
94
+ // Close comment form if it's open
95
+ if (showCommentId === qaId) {
96
+ setShowCommentId(null);
97
+ }
98
+ };
99
+
100
+ const toggleComment = (qaId) => {
101
+ setShowCommentId(showCommentId === qaId ? null : qaId);
102
+ };
103
+
104
+ // Scroll to bottom when new messages arrive
105
+ useEffect(() => {
106
+ const el = chatRef.current;
107
+ if (el) el.scrollTop = el.scrollHeight;
108
+ }, [conversation]);
109
+
110
+ return (
111
+ <div className="chatbot-container">
112
+ <div className="chatbot-wrapper">
113
+ {/* Header */}
114
+ <header className="header">
115
+ <div className="header-left">
116
+ <h1 className="header-title">AI Chatbot</h1>
117
+ </div>
118
+ <button className="new-chat-button" aria-label="New chat" onClick={handleClearThread}>
119
+ New Chat
120
+ </button>
121
+ </header>
122
+
123
+ {/* Chat Container */}
124
+ <div ref={chatRef} className="chat-container">
125
+ {error && (
126
+ <div className="error-message">
127
+ <p className="error-title">Error</p>
128
+ <p>{error}</p>
129
+ </div>
130
+ )}
131
+
132
+ {conversation.length > 0 && (
133
+ <div className="clear-thread-button-container">
134
+ <button className="clear-thread-button" aria-label="Clear thread" onClick={handleClearThread}>
135
+ Clear thread
136
+ </button>
137
+ </div>
138
+ )}
139
+
140
+ {/* Messages */}
141
+ <div className="messages-container">
142
+ {conversation.length === 0 ? (
143
+ <div className="no-conversation-message">
144
+ <p>Ask a question to start a conversation!</p>
145
+ </div>
146
+ ) : (
147
+ conversation.map((qa, index) => (
148
+ <div key={qa.id || `qa-${index}`} className="message">
149
+ <div className="message-right">
150
+ <div className="message-bubble user-message">
151
+ <p>{qa.question}</p>
152
+ </div>
153
+ </div>
154
+
155
+ <div className="message-left">
156
+ <div className="message-bubble bot-message">
157
+ <div
158
+ className="message-answer"
159
+ dangerouslySetInnerHTML={{
160
+ __html: qa.answer
161
+ ? marked.parse(qa.answer)
162
+ : '<p class="text-gray-500">Generating answer...</p>',
163
+ }}
164
+ />
165
+
166
+ {/* Only show feedback controls when answer is completed (not during generation) */}
167
+ {qa.isFeedbackSubmissionEnabled && (
168
+ <div className="feedback-controls">
169
+ <div className="feedback-buttons">
170
+ <button
171
+ className={`feedback-button upvote ${
172
+ feedbackStates[qa.id] === 'upvote' ? 'upvoted' : ''
173
+ }`}
174
+ aria-label="Upvote"
175
+ onClick={() => qa.id && handleFeedback(qa.id, 'upvote')}
176
+ ></button>
177
+ <button
178
+ className={`feedback-button downvote ${
179
+ feedbackStates[qa.id] === 'downvote' ? 'downvoted' : ''
180
+ }`}
181
+ aria-label="Downvote"
182
+ onClick={() => qa.id && handleFeedback(qa.id, 'downvote')}
183
+ ></button>
184
+ <button
185
+ className="feedback-button copy"
186
+ onClick={(event) => handleCopyText(qa.answer)}
187
+ aria-label="Copy message"
188
+ >
189
+ <span className="copy-text"></span>
190
+ </button>
191
+ </div>
192
+ </div>
193
+ )}
194
+ </div>
195
+ </div>
196
+ </div>
197
+ ))
198
+ )}
199
+ {/* <div ref={chatRef} /> */}
200
+ </div>
201
+ </div>
202
+
203
+ {/* Input Area */}
204
+ <div className="input-area">
205
+ <form onSubmit={handleSubmit} className="input-form">
206
+ <div className="input-field">
207
+ <input
208
+ type="text"
209
+ placeholder="Type your message..."
210
+ className="input"
211
+ value={inputMessage}
212
+ onChange={(e) => setInputMessage(e.target.value)}
213
+ disabled={isGeneratingAnswer || isPreparingAnswer}
214
+ />
215
+ </div>
216
+ {isGeneratingAnswer ? (
217
+ <button type="button" className="stop-button" onClick={stopGeneration} aria-label="Stop generation">
218
+ <svg xmlns="http://www.w3.org/2000/svg" className="stop-icon" viewBox="0 0 20 20" fill="currentColor">
219
+ <rect x="6" y="6" width="8" height="8" />
220
+ </svg>
221
+ </button>
222
+ ) : (
223
+ <button
224
+ type="submit"
225
+ className="send-button"
226
+ disabled={!inputMessage.trim() || isPreparingAnswer}
227
+ aria-label="Send message"
228
+ ></button>
229
+ )}
230
+ </form>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ );
235
+ };
236
+
237
+ export default ChatApp;
@@ -1,4 +1,4 @@
1
- import type { SearchState } from '../types/search.types';
1
+ import type { SearchState, SearchGTMObjectProps } from '../types/search.types';
2
2
  import { useEffect, useRef, useCallback } from 'preact/hooks';
3
3
  import Analytics from '../../../../analytics/analytics';
4
4
 
@@ -6,9 +6,9 @@ import Analytics from '../../../../analytics/analytics';
6
6
  * Custom hook that logs search-related events for analytics.
7
7
  *
8
8
  * This hook tracks the following search events:
9
- * - `spark_search:query_changed`: Logs when the search query changes.
10
- * - `spark_search:no_results`: Logs when no results are found for a search.
11
- * - `spark_search:result_clicked`: Logs when a search result is clicked.
9
+ * - `search:new_query`: Logs when the search query changes.
10
+ * - `search:no_results`: Logs when no results are found for a search.
11
+ * - `search:result_selected`: Logs when a search result is clicked.
12
12
  *
13
13
  * It uses an instance of `Analytics` to track these events. The hook listens to changes in the `searchTerm` and `results` properties from the provided `SearchState`.
14
14
  *
@@ -18,7 +18,7 @@ import Analytics from '../../../../analytics/analytics';
18
18
  * @param {boolean} [debug=false] - Flag to enable debug mode. If true, additional debug information will be sent with the event payloads.
19
19
  *
20
20
  * @returns {Object} - An object with the following function:
21
- * - `handleClick`: A callback function that should be attached to search result click events. It tracks `spark_search:result_clicked` events with the clicked result's title and URL.
21
+ * - `handleClick`: A callback function that should be attached to search result click events. It tracks `search:result_selected` events with the clicked result's title and URL.
22
22
  *
23
23
  * @example
24
24
  * const { handleClick } = useSearchLogger(searchState);
@@ -31,16 +31,16 @@ export function useSearchLogger(state?: SearchState, debug?: boolean) {
31
31
  const ref = useRef<Analytics>(new Analytics());
32
32
  const analytics = ref.current;
33
33
 
34
+ const enableDebugger = (o: SearchGTMObjectProps) => o.debug_mode = true
35
+
34
36
  // Log search query
35
37
  useEffect(() => {
36
38
  if (!state?.searchTerm) return;
37
39
  const payload = { query: state?.searchTerm };
38
40
 
39
- if (debug) {
40
- payload.debug_mode = true;
41
- }
41
+ if (debug) enableDebugger(payload)
42
42
 
43
- analytics.track('spark_search:query_changed', payload);
43
+ analytics.track('search:new_query', payload);
44
44
  }, [state?.searchTerm]);
45
45
 
46
46
  // Log zero‑hit searches
@@ -49,12 +49,10 @@ export function useSearchLogger(state?: SearchState, debug?: boolean) {
49
49
 
50
50
  const payload = { query: state?.searchTerm };
51
51
 
52
- if (debug) {
53
- payload.debug_mode = true;
54
- }
52
+ if (debug) enableDebugger(payload)
55
53
 
56
54
  if (state?.searchTerm && state.results.length === 0) {
57
- analytics.track('spark_search:no_results', payload);
55
+ analytics.track('search:no_results', payload);
58
56
  }
59
57
  }, [state?.results]);
60
58
 
@@ -64,17 +62,16 @@ export function useSearchLogger(state?: SearchState, debug?: boolean) {
64
62
  if (!anchor) return;
65
63
 
66
64
  const label = anchor.querySelector('.n-search-link-label')?.innerHTML;
67
- const url = anchor.getAttribute('href');
65
+ const url = anchor.getAttribute('href') || undefined;
68
66
  const payload = {
69
67
  resultTitle: label,
70
68
  resultUrl: url,
71
69
  };
72
70
 
73
- if (debug) {
74
- payload.debug_mode = true;
75
- }
71
+ if (debug) enableDebugger(payload)
72
+
76
73
 
77
- analytics.track('spark_search:result_clicked', payload);
74
+ analytics.track('search:result_selected', payload);
78
75
  }, []);
79
76
 
80
77
  return {
@@ -27,3 +27,10 @@ export interface SearchState {
27
27
  results: SearchLinkProps[];
28
28
  isLoading: boolean;
29
29
  }
30
+
31
+ export interface SearchGTMObjectProps {
32
+ resultTitle?: string;
33
+ resultUrl?: string;
34
+ query?: string;
35
+ debug_mode?: boolean;
36
+ }
@@ -4,6 +4,7 @@ export { default as Icon } from './Icon';
4
4
  export { default as SearchLink } from './SearchModal/components/SearchLink/SearchLink';
5
5
  export { default as SearchHeading } from './SearchModal/components/SearchHeading/SearchHeading';
6
6
  export { default as SearchToggle } from './SearchModal/components/SearchToggle/SearchToggle';
7
+ export { default as ChatApp } from "./ChatApp/ChatApp"
7
8
  export { SearchModal } from './SearchModal';
8
9
 
9
10
  export { type IconName } from './Icon';
@@ -0,0 +1,5 @@
1
+ // Define Astro components module to avoid TypeScript errors
2
+ declare module 'astro:components' {
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export const Code: any;
5
+ }