@netbirdio/explain 0.1.0
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/README.md +402 -0
- package/dist/client/AIAssistantProvider.d.ts +21 -0
- package/dist/client/AIAssistantProvider.d.ts.map +1 -0
- package/dist/client/AIAssistantProvider.js +198 -0
- package/dist/client/AIAssistantProvider.js.map +1 -0
- package/dist/client/AIChatBot.d.ts +10 -0
- package/dist/client/AIChatBot.d.ts.map +1 -0
- package/dist/client/AIChatBot.js +139 -0
- package/dist/client/AIChatBot.js.map +1 -0
- package/dist/client/AIFloatingButton.d.ts +7 -0
- package/dist/client/AIFloatingButton.d.ts.map +1 -0
- package/dist/client/AIFloatingButton.js +16 -0
- package/dist/client/AIFloatingButton.js.map +1 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.d.ts +51 -0
- package/dist/client/styles.d.ts.map +1 -0
- package/dist/client/styles.js +317 -0
- package/dist/client/styles.js.map +1 -0
- package/dist/server/handler.d.ts +38 -0
- package/dist/server/handler.d.ts.map +1 -0
- package/dist/server/handler.js +117 -0
- package/dist/server/handler.js.map +1 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/providers/anthropic.d.ts +9 -0
- package/dist/server/providers/anthropic.d.ts.map +1 -0
- package/dist/server/providers/anthropic.js +40 -0
- package/dist/server/providers/anthropic.js.map +1 -0
- package/dist/server/providers/openai.d.ts +9 -0
- package/dist/server/providers/openai.d.ts.map +1 -0
- package/dist/server/providers/openai.js +46 -0
- package/dist/server/providers/openai.js.map +1 -0
- package/dist/server/providers/types.d.ts +9 -0
- package/dist/server/providers/types.d.ts.map +1 -0
- package/dist/server/providers/types.js +2 -0
- package/dist/server/providers/types.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
- package/src/client/AIAssistantProvider.tsx +288 -0
- package/src/client/AIChatBot.tsx +309 -0
- package/src/client/AIFloatingButton.tsx +34 -0
- package/src/client/index.ts +4 -0
- package/src/client/styles.ts +353 -0
- package/src/server/handler.ts +158 -0
- package/src/server/index.ts +5 -0
- package/src/server/providers/anthropic.ts +53 -0
- package/src/server/providers/openai.ts +55 -0
- package/src/server/providers/types.ts +10 -0
- package/src/types.ts +11 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Bot,
|
|
5
|
+
MessageCircleQuestion,
|
|
6
|
+
Send,
|
|
7
|
+
Sparkles,
|
|
8
|
+
User,
|
|
9
|
+
X,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
12
|
+
import type { Message } from "../types";
|
|
13
|
+
import * as S from "./styles";
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
initialQuery: string;
|
|
19
|
+
endpoint: string;
|
|
20
|
+
apiKey?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function fetchAIResponse(
|
|
24
|
+
endpoint: string,
|
|
25
|
+
apiKey: string | undefined,
|
|
26
|
+
messages: Message[],
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
const headers: Record<string, string> = {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
};
|
|
31
|
+
if (apiKey) {
|
|
32
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const response = await fetch(endpoint, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers,
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
messages: messages.map((m) => ({
|
|
40
|
+
role: m.role,
|
|
41
|
+
content: m.content,
|
|
42
|
+
})),
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const text = await response.text();
|
|
48
|
+
throw new Error(`${response.status}: ${text}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const data = await response.json();
|
|
52
|
+
return data.reply;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function AIChatBot({
|
|
56
|
+
open,
|
|
57
|
+
onClose,
|
|
58
|
+
initialQuery,
|
|
59
|
+
endpoint,
|
|
60
|
+
apiKey,
|
|
61
|
+
}: Props) {
|
|
62
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
63
|
+
const [input, setInput] = useState("");
|
|
64
|
+
const [isTyping, setIsTyping] = useState(false);
|
|
65
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
66
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
67
|
+
const hasProcessedInitialQuery = useRef(false);
|
|
68
|
+
|
|
69
|
+
const scrollToBottom = useCallback(() => {
|
|
70
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
scrollToBottom();
|
|
75
|
+
}, [messages, isTyping, scrollToBottom]);
|
|
76
|
+
|
|
77
|
+
const getAIResponse = useCallback(
|
|
78
|
+
async (allMessages: Message[]) => {
|
|
79
|
+
setIsTyping(true);
|
|
80
|
+
try {
|
|
81
|
+
const reply = await fetchAIResponse(endpoint, apiKey, allMessages);
|
|
82
|
+
const response: Message = {
|
|
83
|
+
id: Date.now().toString(),
|
|
84
|
+
role: "assistant",
|
|
85
|
+
content: reply,
|
|
86
|
+
};
|
|
87
|
+
setMessages((prev) => [...prev, response]);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const errorMsg =
|
|
90
|
+
err instanceof Error ? err.message : "Unknown error";
|
|
91
|
+
const response: Message = {
|
|
92
|
+
id: Date.now().toString(),
|
|
93
|
+
role: "assistant",
|
|
94
|
+
content: `Sorry, I couldn't get a response. Error: ${errorMsg}`,
|
|
95
|
+
};
|
|
96
|
+
setMessages((prev) => [...prev, response]);
|
|
97
|
+
} finally {
|
|
98
|
+
setIsTyping(false);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[endpoint, apiKey],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Handle initial query from explain mode
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (open && initialQuery && !hasProcessedInitialQuery.current) {
|
|
107
|
+
hasProcessedInitialQuery.current = true;
|
|
108
|
+
|
|
109
|
+
const lines = initialQuery.split("\n");
|
|
110
|
+
const userMessage = lines[0];
|
|
111
|
+
const docsLine = lines.find((l) => l.startsWith("Docs: "));
|
|
112
|
+
|
|
113
|
+
const msgs: Message[] = [];
|
|
114
|
+
|
|
115
|
+
if (docsLine) {
|
|
116
|
+
msgs.push({
|
|
117
|
+
id: Date.now().toString() + "-ctx",
|
|
118
|
+
role: "context",
|
|
119
|
+
content: docsLine,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
msgs.push({
|
|
124
|
+
id: Date.now().toString(),
|
|
125
|
+
role: "user",
|
|
126
|
+
content: userMessage,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
setMessages(msgs);
|
|
130
|
+
getAIResponse(msgs);
|
|
131
|
+
}
|
|
132
|
+
}, [open, initialQuery, getAIResponse]);
|
|
133
|
+
|
|
134
|
+
// Reset when closed
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!open) {
|
|
137
|
+
hasProcessedInitialQuery.current = false;
|
|
138
|
+
setMessages([]);
|
|
139
|
+
setInput("");
|
|
140
|
+
setIsTyping(false);
|
|
141
|
+
}
|
|
142
|
+
}, [open]);
|
|
143
|
+
|
|
144
|
+
// Focus input when opened
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (open) {
|
|
147
|
+
setTimeout(() => inputRef.current?.focus(), 200);
|
|
148
|
+
}
|
|
149
|
+
}, [open]);
|
|
150
|
+
|
|
151
|
+
const sendMessage = useCallback(() => {
|
|
152
|
+
const text = input.trim();
|
|
153
|
+
if (!text || isTyping) return;
|
|
154
|
+
|
|
155
|
+
const userMsg: Message = {
|
|
156
|
+
id: Date.now().toString(),
|
|
157
|
+
role: "user",
|
|
158
|
+
content: text,
|
|
159
|
+
};
|
|
160
|
+
const updatedMessages = [...messages, userMsg];
|
|
161
|
+
setMessages(updatedMessages);
|
|
162
|
+
setInput("");
|
|
163
|
+
getAIResponse(updatedMessages);
|
|
164
|
+
}, [input, isTyping, messages, getAIResponse]);
|
|
165
|
+
|
|
166
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
167
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
sendMessage();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (!open) return null;
|
|
174
|
+
|
|
175
|
+
const active = !!input.trim() && !isTyping;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div style={S.chatPanel}>
|
|
179
|
+
{/* Header */}
|
|
180
|
+
<div style={S.chatHeader}>
|
|
181
|
+
<div style={S.chatHeaderLeft}>
|
|
182
|
+
<div style={S.chatHeaderIcon}>
|
|
183
|
+
<Sparkles size={15} style={{ color: "var(--nb-explain-accent)" }} />
|
|
184
|
+
</div>
|
|
185
|
+
<div>
|
|
186
|
+
<h3 style={S.chatHeaderTitle}>AI Assistant</h3>
|
|
187
|
+
<span style={S.chatHeaderSubtitle}>Ask anything</span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<button
|
|
191
|
+
onClick={onClose}
|
|
192
|
+
style={S.chatCloseBtn}
|
|
193
|
+
onMouseEnter={(e) => {
|
|
194
|
+
e.currentTarget.style.color = "var(--nb-explain-text)";
|
|
195
|
+
e.currentTarget.style.background = "var(--nb-explain-bg-hover)";
|
|
196
|
+
}}
|
|
197
|
+
onMouseLeave={(e) => {
|
|
198
|
+
e.currentTarget.style.color = "var(--nb-explain-text-dim)";
|
|
199
|
+
e.currentTarget.style.background = "none";
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<X size={16} />
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Messages */}
|
|
207
|
+
<div style={S.messagesArea}>
|
|
208
|
+
{messages.length === 0 && !isTyping && (
|
|
209
|
+
<div style={S.emptyState}>
|
|
210
|
+
<MessageCircleQuestion
|
|
211
|
+
size={40}
|
|
212
|
+
style={{ color: "var(--nb-explain-text-dim)" }}
|
|
213
|
+
/>
|
|
214
|
+
<div>
|
|
215
|
+
<p style={S.emptyStateTitle}>How can I help?</p>
|
|
216
|
+
<p style={S.emptyStateHint}>
|
|
217
|
+
Use the Explain button to click on any element, or ask a
|
|
218
|
+
question below.
|
|
219
|
+
</p>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{messages.map((msg) =>
|
|
225
|
+
msg.role === "context" ? (
|
|
226
|
+
<div key={msg.id} style={S.contextBadge}>
|
|
227
|
+
<div style={S.contextBadgeInner}>
|
|
228
|
+
<Sparkles
|
|
229
|
+
size={10}
|
|
230
|
+
style={{ color: "var(--nb-explain-accent)", opacity: 0.6 }}
|
|
231
|
+
/>
|
|
232
|
+
{msg.content}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
) : (
|
|
236
|
+
<div key={msg.id} style={S.messageRow(msg.role === "user")}>
|
|
237
|
+
{msg.role === "assistant" && (
|
|
238
|
+
<div style={S.messageAvatar(false)}>
|
|
239
|
+
<Bot size={13} style={{ color: "var(--nb-explain-accent)" }} />
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
<div style={S.messageBubble(msg.role === "user")}>
|
|
243
|
+
{msg.content.split("\n").map((line, i) => (
|
|
244
|
+
<React.Fragment key={i}>
|
|
245
|
+
{line
|
|
246
|
+
.split(/(\*\*[^*]+\*\*)/)
|
|
247
|
+
.map((part, j) =>
|
|
248
|
+
part.startsWith("**") && part.endsWith("**") ? (
|
|
249
|
+
<strong key={j} style={S.messageBold}>
|
|
250
|
+
{part.slice(2, -2)}
|
|
251
|
+
</strong>
|
|
252
|
+
) : (
|
|
253
|
+
<React.Fragment key={j}>{part}</React.Fragment>
|
|
254
|
+
),
|
|
255
|
+
)}
|
|
256
|
+
{i < msg.content.split("\n").length - 1 && <br />}
|
|
257
|
+
</React.Fragment>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
{msg.role === "user" && (
|
|
261
|
+
<div style={S.messageAvatar(true)}>
|
|
262
|
+
<User size={13} style={{ color: "var(--nb-explain-user-text)" }} />
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
),
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
{isTyping && (
|
|
270
|
+
<div style={S.typingRow}>
|
|
271
|
+
<div style={S.messageAvatar(false)}>
|
|
272
|
+
<Bot size={13} style={{ color: "var(--nb-explain-accent)" }} />
|
|
273
|
+
</div>
|
|
274
|
+
<div style={S.typingBubble}>
|
|
275
|
+
<span className="nb-explain-dot-1" style={S.typingDot} />
|
|
276
|
+
<span className="nb-explain-dot-2" style={S.typingDot} />
|
|
277
|
+
<span className="nb-explain-dot-3" style={S.typingDot} />
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
<div ref={messagesEndRef} />
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{/* Input */}
|
|
286
|
+
<div style={S.inputArea}>
|
|
287
|
+
<div style={S.inputRow}>
|
|
288
|
+
<input
|
|
289
|
+
ref={inputRef}
|
|
290
|
+
type="text"
|
|
291
|
+
value={input}
|
|
292
|
+
onChange={(e) => setInput(e.target.value)}
|
|
293
|
+
onKeyDown={handleKeyDown}
|
|
294
|
+
placeholder="Ask a follow-up question..."
|
|
295
|
+
style={S.inputField}
|
|
296
|
+
/>
|
|
297
|
+
<button
|
|
298
|
+
onClick={sendMessage}
|
|
299
|
+
disabled={!active}
|
|
300
|
+
style={S.sendBtn(active)}
|
|
301
|
+
>
|
|
302
|
+
<Send size={15} />
|
|
303
|
+
</button>
|
|
304
|
+
</div>
|
|
305
|
+
<p style={S.inputFooter}>AI-powered assistant</p>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { MessageCircleQuestion, X } from "lucide-react";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import * as S from "./styles";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function AIFloatingButton({ isOpen, onClick }: Props) {
|
|
13
|
+
return (
|
|
14
|
+
<button
|
|
15
|
+
onClick={onClick}
|
|
16
|
+
data-nb-explain-ignore
|
|
17
|
+
style={S.fab(isOpen)}
|
|
18
|
+
onMouseEnter={(e) => {
|
|
19
|
+
e.currentTarget.style.transform = "scale(1.05)";
|
|
20
|
+
}}
|
|
21
|
+
onMouseLeave={(e) => {
|
|
22
|
+
e.currentTarget.style.transform = "scale(1)";
|
|
23
|
+
}}
|
|
24
|
+
onMouseDown={(e) => {
|
|
25
|
+
e.currentTarget.style.transform = "scale(0.95)";
|
|
26
|
+
}}
|
|
27
|
+
onMouseUp={(e) => {
|
|
28
|
+
e.currentTarget.style.transform = "scale(1.05)";
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
{isOpen ? <X size={20} /> : <MessageCircleQuestion size={22} />}
|
|
32
|
+
</button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All styles for netbird-explain components.
|
|
3
|
+
* Uses inline styles + CSS custom properties so the package
|
|
4
|
+
* works without any external CSS framework.
|
|
5
|
+
*
|
|
6
|
+
* Consumers can override via CSS custom properties:
|
|
7
|
+
* --nb-explain-bg: panel background
|
|
8
|
+
* --nb-explain-bg-subtle: input/message background
|
|
9
|
+
* --nb-explain-border: border color
|
|
10
|
+
* --nb-explain-text: primary text
|
|
11
|
+
* --nb-explain-text-muted: secondary text
|
|
12
|
+
* --nb-explain-text-dim: tertiary/placeholder text
|
|
13
|
+
* --nb-explain-accent: accent color (yellow)
|
|
14
|
+
* --nb-explain-accent-hover: accent hover
|
|
15
|
+
* --nb-explain-user-bg: user message background
|
|
16
|
+
* --nb-explain-user-text: user message text
|
|
17
|
+
* --nb-explain-radius: border radius
|
|
18
|
+
* --nb-explain-font: font family
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const CSS_VARS = `
|
|
22
|
+
:root {
|
|
23
|
+
--nb-explain-bg: #0a0a0f;
|
|
24
|
+
--nb-explain-bg-subtle: rgba(255,255,255,0.06);
|
|
25
|
+
--nb-explain-bg-hover: rgba(255,255,255,0.08);
|
|
26
|
+
--nb-explain-border: rgba(255,255,255,0.1);
|
|
27
|
+
--nb-explain-text: #f0f0f5;
|
|
28
|
+
--nb-explain-text-muted: #9ca3af;
|
|
29
|
+
--nb-explain-text-dim: #6b7280;
|
|
30
|
+
--nb-explain-accent: #eab308;
|
|
31
|
+
--nb-explain-accent-hover: #facc15;
|
|
32
|
+
--nb-explain-accent-glow: rgba(234,179,8,0.15);
|
|
33
|
+
--nb-explain-user-bg: #4f46e5;
|
|
34
|
+
--nb-explain-user-text: #ffffff;
|
|
35
|
+
--nb-explain-user-glow: rgba(79,70,229,0.25);
|
|
36
|
+
--nb-explain-radius: 12px;
|
|
37
|
+
--nb-explain-radius-sm: 8px;
|
|
38
|
+
--nb-explain-radius-xs: 6px;
|
|
39
|
+
--nb-explain-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
40
|
+
--nb-explain-shadow: 0 25px 50px -12px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05);
|
|
41
|
+
--nb-explain-banner-bg: rgba(234,179,8,0.92);
|
|
42
|
+
--nb-explain-banner-text: #000000;
|
|
43
|
+
--nb-explain-error-text: #f87171;
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
// --- Chat Panel ---
|
|
48
|
+
|
|
49
|
+
export const chatPanel: React.CSSProperties = {
|
|
50
|
+
position: "fixed",
|
|
51
|
+
bottom: 80,
|
|
52
|
+
right: 20,
|
|
53
|
+
zIndex: 9998,
|
|
54
|
+
width: 420,
|
|
55
|
+
height: 600,
|
|
56
|
+
display: "flex",
|
|
57
|
+
flexDirection: "column",
|
|
58
|
+
borderRadius: "var(--nb-explain-radius)",
|
|
59
|
+
border: "1px solid var(--nb-explain-border)",
|
|
60
|
+
background: "var(--nb-explain-bg)",
|
|
61
|
+
boxShadow: "var(--nb-explain-shadow)",
|
|
62
|
+
fontFamily: "var(--nb-explain-font)",
|
|
63
|
+
overflow: "hidden",
|
|
64
|
+
animation: "nb-explain-slide-up 0.2s ease-out",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const chatHeader: React.CSSProperties = {
|
|
68
|
+
display: "flex",
|
|
69
|
+
alignItems: "center",
|
|
70
|
+
justifyContent: "space-between",
|
|
71
|
+
padding: "12px 16px",
|
|
72
|
+
borderBottom: "1px solid var(--nb-explain-border)",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const chatHeaderLeft: React.CSSProperties = {
|
|
76
|
+
display: "flex",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
gap: 10,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const chatHeaderIcon: React.CSSProperties = {
|
|
82
|
+
width: 28,
|
|
83
|
+
height: 28,
|
|
84
|
+
borderRadius: "var(--nb-explain-radius-sm)",
|
|
85
|
+
background: "var(--nb-explain-accent-glow)",
|
|
86
|
+
display: "flex",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
justifyContent: "center",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const chatHeaderTitle: React.CSSProperties = {
|
|
92
|
+
fontSize: 14,
|
|
93
|
+
fontWeight: 600,
|
|
94
|
+
color: "var(--nb-explain-text)",
|
|
95
|
+
lineHeight: 1,
|
|
96
|
+
margin: 0,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const chatHeaderSubtitle: React.CSSProperties = {
|
|
100
|
+
fontSize: 11,
|
|
101
|
+
color: "var(--nb-explain-text-dim)",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const chatCloseBtn: React.CSSProperties = {
|
|
105
|
+
padding: 6,
|
|
106
|
+
borderRadius: "var(--nb-explain-radius-xs)",
|
|
107
|
+
color: "var(--nb-explain-text-dim)",
|
|
108
|
+
background: "none",
|
|
109
|
+
border: "none",
|
|
110
|
+
cursor: "pointer",
|
|
111
|
+
display: "flex",
|
|
112
|
+
alignItems: "center",
|
|
113
|
+
transition: "color 0.15s, background 0.15s",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// --- Messages ---
|
|
117
|
+
|
|
118
|
+
export const messagesArea: React.CSSProperties = {
|
|
119
|
+
flex: 1,
|
|
120
|
+
overflowY: "auto",
|
|
121
|
+
padding: "16px",
|
|
122
|
+
display: "flex",
|
|
123
|
+
flexDirection: "column",
|
|
124
|
+
gap: 16,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const emptyState: React.CSSProperties = {
|
|
128
|
+
display: "flex",
|
|
129
|
+
flexDirection: "column",
|
|
130
|
+
alignItems: "center",
|
|
131
|
+
justifyContent: "center",
|
|
132
|
+
height: "100%",
|
|
133
|
+
textAlign: "center",
|
|
134
|
+
gap: 12,
|
|
135
|
+
opacity: 0.5,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const emptyStateTitle: React.CSSProperties = {
|
|
139
|
+
fontSize: 14,
|
|
140
|
+
color: "var(--nb-explain-text-muted)",
|
|
141
|
+
fontWeight: 500,
|
|
142
|
+
margin: 0,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const emptyStateHint: React.CSSProperties = {
|
|
146
|
+
fontSize: 12,
|
|
147
|
+
color: "var(--nb-explain-text-dim)",
|
|
148
|
+
marginTop: 4,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Context badge
|
|
152
|
+
export const contextBadge: React.CSSProperties = {
|
|
153
|
+
display: "flex",
|
|
154
|
+
justifyContent: "center",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const contextBadgeInner: React.CSSProperties = {
|
|
158
|
+
fontSize: 11,
|
|
159
|
+
color: "var(--nb-explain-text-dim)",
|
|
160
|
+
background: "var(--nb-explain-bg-subtle)",
|
|
161
|
+
borderRadius: 20,
|
|
162
|
+
padding: "4px 12px",
|
|
163
|
+
display: "flex",
|
|
164
|
+
alignItems: "center",
|
|
165
|
+
gap: 6,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Message row
|
|
169
|
+
export const messageRow = (isUser: boolean): React.CSSProperties => ({
|
|
170
|
+
display: "flex",
|
|
171
|
+
gap: 10,
|
|
172
|
+
justifyContent: isUser ? "flex-end" : "flex-start",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export const messageAvatar = (isUser: boolean): React.CSSProperties => ({
|
|
176
|
+
width: 24,
|
|
177
|
+
height: 24,
|
|
178
|
+
borderRadius: "var(--nb-explain-radius-xs)",
|
|
179
|
+
background: isUser ? "var(--nb-explain-user-glow)" : "var(--nb-explain-accent-glow)",
|
|
180
|
+
display: "flex",
|
|
181
|
+
alignItems: "center",
|
|
182
|
+
justifyContent: "center",
|
|
183
|
+
flexShrink: 0,
|
|
184
|
+
marginTop: 2,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const messageBubble = (isUser: boolean): React.CSSProperties => ({
|
|
188
|
+
maxWidth: "85%",
|
|
189
|
+
borderRadius: "var(--nb-explain-radius-sm)",
|
|
190
|
+
padding: "8px 12px",
|
|
191
|
+
fontSize: 14,
|
|
192
|
+
lineHeight: 1.6,
|
|
193
|
+
background: isUser ? "var(--nb-explain-user-bg)" : "var(--nb-explain-bg-subtle)",
|
|
194
|
+
color: isUser ? "var(--nb-explain-user-text)" : "var(--nb-explain-text)",
|
|
195
|
+
wordBreak: "break-word",
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export const messageBold: React.CSSProperties = {
|
|
199
|
+
fontWeight: 600,
|
|
200
|
+
color: "var(--nb-explain-text)",
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Typing indicator
|
|
204
|
+
export const typingRow: React.CSSProperties = {
|
|
205
|
+
display: "flex",
|
|
206
|
+
gap: 10,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const typingBubble: React.CSSProperties = {
|
|
210
|
+
background: "var(--nb-explain-bg-subtle)",
|
|
211
|
+
borderRadius: "var(--nb-explain-radius-sm)",
|
|
212
|
+
padding: "12px 16px",
|
|
213
|
+
display: "flex",
|
|
214
|
+
gap: 6,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const typingDot: React.CSSProperties = {
|
|
218
|
+
width: 6,
|
|
219
|
+
height: 6,
|
|
220
|
+
borderRadius: "50%",
|
|
221
|
+
background: "var(--nb-explain-text-dim)",
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// --- Input area ---
|
|
225
|
+
|
|
226
|
+
export const inputArea: React.CSSProperties = {
|
|
227
|
+
padding: "12px 16px",
|
|
228
|
+
borderTop: "1px solid var(--nb-explain-border)",
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export const inputRow: React.CSSProperties = {
|
|
232
|
+
display: "flex",
|
|
233
|
+
alignItems: "center",
|
|
234
|
+
gap: 8,
|
|
235
|
+
background: "var(--nb-explain-bg-subtle)",
|
|
236
|
+
borderRadius: "var(--nb-explain-radius-sm)",
|
|
237
|
+
padding: "8px 12px",
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const inputField: React.CSSProperties = {
|
|
241
|
+
flex: 1,
|
|
242
|
+
background: "none",
|
|
243
|
+
border: "none",
|
|
244
|
+
outline: "none",
|
|
245
|
+
fontSize: 14,
|
|
246
|
+
color: "var(--nb-explain-text)",
|
|
247
|
+
fontFamily: "var(--nb-explain-font)",
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const sendBtn = (active: boolean): React.CSSProperties => ({
|
|
251
|
+
padding: 6,
|
|
252
|
+
borderRadius: "var(--nb-explain-radius-xs)",
|
|
253
|
+
background: "none",
|
|
254
|
+
border: "none",
|
|
255
|
+
cursor: active ? "pointer" : "not-allowed",
|
|
256
|
+
color: active ? "var(--nb-explain-accent)" : "var(--nb-explain-text-dim)",
|
|
257
|
+
display: "flex",
|
|
258
|
+
alignItems: "center",
|
|
259
|
+
transition: "color 0.15s",
|
|
260
|
+
opacity: active ? 1 : 0.5,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
export const inputFooter: React.CSSProperties = {
|
|
264
|
+
fontSize: 10,
|
|
265
|
+
color: "var(--nb-explain-text-dim)",
|
|
266
|
+
textAlign: "center",
|
|
267
|
+
marginTop: 6,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// --- Floating button ---
|
|
271
|
+
|
|
272
|
+
export const fab = (isOpen: boolean): React.CSSProperties => ({
|
|
273
|
+
position: "fixed",
|
|
274
|
+
bottom: 20,
|
|
275
|
+
right: 20,
|
|
276
|
+
zIndex: 9997,
|
|
277
|
+
width: 48,
|
|
278
|
+
height: 48,
|
|
279
|
+
borderRadius: "50%",
|
|
280
|
+
display: "flex",
|
|
281
|
+
alignItems: "center",
|
|
282
|
+
justifyContent: "center",
|
|
283
|
+
border: "none",
|
|
284
|
+
cursor: "pointer",
|
|
285
|
+
transition: "transform 0.2s, box-shadow 0.2s",
|
|
286
|
+
boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
|
287
|
+
background: isOpen
|
|
288
|
+
? "var(--nb-explain-bg-hover)"
|
|
289
|
+
: "linear-gradient(135deg, var(--nb-explain-accent), #f97316)",
|
|
290
|
+
color: isOpen ? "var(--nb-explain-text-muted)" : "#fff",
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// --- Banner ---
|
|
294
|
+
|
|
295
|
+
export const banner: React.CSSProperties = {
|
|
296
|
+
position: "fixed",
|
|
297
|
+
top: 12,
|
|
298
|
+
left: "50%",
|
|
299
|
+
transform: "translateX(-50%)",
|
|
300
|
+
zIndex: 9996,
|
|
301
|
+
background: "var(--nb-explain-banner-bg)",
|
|
302
|
+
color: "var(--nb-explain-banner-text)",
|
|
303
|
+
fontSize: 14,
|
|
304
|
+
fontWeight: 500,
|
|
305
|
+
padding: "6px 16px",
|
|
306
|
+
borderRadius: 20,
|
|
307
|
+
boxShadow: "0 4px 20px rgba(234,179,8,0.3)",
|
|
308
|
+
display: "flex",
|
|
309
|
+
alignItems: "center",
|
|
310
|
+
gap: 8,
|
|
311
|
+
fontFamily: "var(--nb-explain-font)",
|
|
312
|
+
animation: "nb-explain-fade-in 0.2s ease-out",
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export const bannerCancel: React.CSSProperties = {
|
|
316
|
+
marginLeft: 4,
|
|
317
|
+
color: "rgba(0,0,0,0.5)",
|
|
318
|
+
background: "none",
|
|
319
|
+
border: "none",
|
|
320
|
+
cursor: "pointer",
|
|
321
|
+
textDecoration: "underline",
|
|
322
|
+
fontSize: 12,
|
|
323
|
+
fontFamily: "var(--nb-explain-font)",
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// --- Animations (injected as <style>) ---
|
|
327
|
+
|
|
328
|
+
export const ANIMATIONS = `
|
|
329
|
+
@keyframes nb-explain-slide-up {
|
|
330
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
331
|
+
to { opacity: 1; transform: translateY(0); }
|
|
332
|
+
}
|
|
333
|
+
@keyframes nb-explain-fade-in {
|
|
334
|
+
from { opacity: 0; transform: translateX(-50%) translateY(-4px); }
|
|
335
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
336
|
+
}
|
|
337
|
+
@keyframes nb-explain-bounce {
|
|
338
|
+
0%, 80%, 100% { transform: translateY(0); }
|
|
339
|
+
40% { transform: translateY(-4px); }
|
|
340
|
+
}
|
|
341
|
+
.nb-explain-dot-1 { animation: nb-explain-bounce 1.2s infinite; animation-delay: 0ms; }
|
|
342
|
+
.nb-explain-dot-2 { animation: nb-explain-bounce 1.2s infinite; animation-delay: 150ms; }
|
|
343
|
+
.nb-explain-dot-3 { animation: nb-explain-bounce 1.2s infinite; animation-delay: 300ms; }
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
export const HIGHLIGHT_STYLES = `
|
|
347
|
+
[data-nb-explain-highlight] {
|
|
348
|
+
outline: 2px solid rgba(234, 179, 8, 0.7) !important;
|
|
349
|
+
border-radius: 6px;
|
|
350
|
+
cursor: help !important;
|
|
351
|
+
transition: outline 0.1s ease;
|
|
352
|
+
}
|
|
353
|
+
`;
|