@kaifusion/widget 1.0.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.
@@ -0,0 +1,709 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkMath from "remark-math";
5
+ import rehypeKatex from "rehype-katex";
6
+ import rehypeRaw from "rehype-raw";
7
+ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
8
+ import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism";
9
+ import {
10
+ MessageSquare,
11
+ X,
12
+ Send,
13
+ Maximize2,
14
+ Minimize2,
15
+ Loader2,
16
+ Bot,
17
+ Copy,
18
+ CheckCircle,
19
+ Terminal,
20
+ FileText,
21
+ ExternalLink,
22
+ RefreshCcw,
23
+ Quote,
24
+ } from "lucide-react";
25
+ import { motion, AnimatePresence } from "motion/react";
26
+ import { v4 as uuidv4 } from "uuid";
27
+
28
+ interface Message {
29
+ id: string;
30
+ content: string;
31
+ isBot: boolean;
32
+ timestamp: Date;
33
+ isError?: boolean;
34
+ }
35
+
36
+ export interface KaiChatWidgetProps {
37
+ authToken?: string;
38
+ workflowId?: string;
39
+ title?: string;
40
+ targetUrl?: string;
41
+ position?: "left" | "right";
42
+ color?: string;
43
+ }
44
+
45
+ export default function KaiChatWidget({
46
+ authToken,
47
+ workflowId = "e64585ca-45c1-4dad-ab62-9b59e4979995",
48
+ title = "ApiFort AI",
49
+ targetUrl = "http://localhost:23056",
50
+ position = "right",
51
+ color = "#526cfe",
52
+ }: KaiChatWidgetProps) {
53
+ const [isOpen, setIsOpen] = useState(false);
54
+ const [isMaximized, setIsMaximized] = useState(false);
55
+ const [messages, setMessages] = useState<Message[]>([
56
+ {
57
+ id: "welcome",
58
+ content: "Merhaba! Size nasıl yardımcı olabilirim?",
59
+ isBot: true,
60
+ timestamp: new Date(),
61
+ },
62
+ ]);
63
+ const [input, setInput] = useState("");
64
+ const [isLoading, setIsLoading] = useState(false);
65
+ const [copiedCode, setCopiedCode] = useState<string | null>(null);
66
+ const messagesEndRef = useRef<HTMLDivElement>(null);
67
+ const sessionIdRef = useRef(uuidv4());
68
+
69
+ const copyToClipboard = async (text: string) => {
70
+ try {
71
+ await navigator.clipboard.writeText(text);
72
+ setCopiedCode(text);
73
+ setTimeout(() => {
74
+ setCopiedCode(null);
75
+ }, 2000);
76
+ } catch (err) {
77
+ console.error("Kopyalama başarısız:", err);
78
+ }
79
+ };
80
+
81
+ useEffect(() => {
82
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
83
+ }, [messages, isOpen]);
84
+
85
+ const sendMessage = async () => {
86
+ if (!input.trim() || isLoading) return;
87
+
88
+ const userMessage: Message = {
89
+ id: uuidv4(),
90
+ content: input,
91
+ isBot: false,
92
+ timestamp: new Date(),
93
+ };
94
+
95
+ setMessages((prev) => [...prev, userMessage]);
96
+ setInput("");
97
+ setIsLoading(true);
98
+
99
+ // Bot mesajı için placeholder oluştur
100
+ const botMessageId = uuidv4();
101
+ setMessages((prev) => [
102
+ ...prev,
103
+ {
104
+ id: botMessageId,
105
+ content: "",
106
+ isBot: true,
107
+ timestamp: new Date(),
108
+ },
109
+ ]);
110
+
111
+ try {
112
+ const token = authToken || localStorage.getItem("auth_access_token");
113
+ const headers: Record<string, string> = {
114
+ "Content-Type": "application/json",
115
+ Accept: "text/event-stream",
116
+ };
117
+
118
+ if (token) {
119
+ headers["Authorization"] = `Bearer ${token}`;
120
+ headers["X-API-Key"] = token;
121
+ }
122
+
123
+ const response = await fetch(`${targetUrl}/api/v1/workflows/execute`, {
124
+ method: "POST",
125
+ headers,
126
+ body: JSON.stringify({
127
+ input_text: userMessage.content,
128
+ session_id: sessionIdRef.current,
129
+ chatflow_id: sessionIdRef.current,
130
+ workflow_id: workflowId,
131
+ }),
132
+ });
133
+
134
+ const contentType = response.headers.get("content-type");
135
+ if (contentType && contentType.includes("application/json")) {
136
+ const data = await response.json();
137
+ if (data.error) {
138
+ setMessages((prev) =>
139
+ prev.map((msg) =>
140
+ msg.id === botMessageId
141
+ ? { ...msg, content: data.message, isError: true }
142
+ : msg
143
+ )
144
+ );
145
+ return;
146
+ }
147
+ }
148
+
149
+ if (!response.body) throw new Error("ReadableStream not supported");
150
+
151
+ const reader = response.body.getReader();
152
+ const decoder = new TextDecoder();
153
+ let fullText = "";
154
+
155
+ while (true) {
156
+ const { done, value } = await reader.read();
157
+ if (done) break;
158
+
159
+ const chunk = decoder.decode(value, { stream: true });
160
+ const lines = chunk.split("\n");
161
+
162
+ for (const line of lines) {
163
+ if (line.startsWith("data: ")) {
164
+ const data = line.slice(6).trim();
165
+ if (data === "[DONE]" || !data) continue;
166
+
167
+ try {
168
+ const parsed = JSON.parse(data);
169
+ let newContent = "";
170
+
171
+ if (parsed.type === "token" && parsed.content) {
172
+ newContent = parsed.content;
173
+ } else if (parsed.type === "output" && parsed.output) {
174
+ newContent = parsed.output;
175
+ } else if (parsed.type === "complete" && parsed.result) {
176
+ if (typeof parsed.result === "string" && !fullText) {
177
+ newContent = parsed.result;
178
+ }
179
+ }
180
+
181
+ if (newContent) {
182
+ fullText += newContent;
183
+ setMessages((prev) =>
184
+ prev.map((msg) =>
185
+ msg.id === botMessageId
186
+ ? { ...msg, content: fullText }
187
+ : msg
188
+ )
189
+ );
190
+ }
191
+ } catch (e) {
192
+ console.warn("Stream parse error:", e);
193
+ }
194
+ }
195
+ }
196
+ }
197
+ } catch (error) {
198
+ console.error("API Error:", error);
199
+ setMessages((prev) =>
200
+ prev.map((msg) =>
201
+ msg.id === botMessageId
202
+ ? { ...msg, content: "Üzgünüm, bir hata oluştu.", isError: true }
203
+ : msg
204
+ )
205
+ );
206
+ } finally {
207
+ setIsLoading(false);
208
+ }
209
+ };
210
+
211
+ const clearHistory = () => {
212
+ setMessages([]);
213
+ sessionIdRef.current = uuidv4();
214
+ };
215
+
216
+ return (
217
+ <div
218
+ className={`fixed bottom-5 z-50 flex flex-col items-end gap-4 font-sans ${
219
+ position === "left" ? "left-5" : "right-5"
220
+ }`}
221
+ style={{ fontFamily: "system-ui, -apple-system, sans-serif" }}
222
+ >
223
+ {/* Overlay - Tam Ekran Modu İçin */}
224
+ <AnimatePresence>
225
+ {isOpen && isMaximized && (
226
+ <motion.div
227
+ initial={{ opacity: 0 }}
228
+ animate={{ opacity: 1 }}
229
+ exit={{ opacity: 0 }}
230
+ className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
231
+ onClick={() => setIsMaximized(false)}
232
+ />
233
+ )}
234
+ </AnimatePresence>
235
+
236
+ {/* Chat Penceresi */}
237
+ <AnimatePresence>
238
+ {isOpen && (
239
+ <motion.div
240
+ initial={{ opacity: 0, y: 20, scale: 0.95 }}
241
+ animate={{
242
+ opacity: 1,
243
+ y: 0,
244
+ scale: 1,
245
+ width: isMaximized ? "900px" : "400px",
246
+ height: isMaximized ? "85vh" : "600px",
247
+ }}
248
+ exit={{ opacity: 0, y: 20, scale: 0.95 }}
249
+ className={`rounded-2xl shadow-2xl flex flex-col overflow-hidden z-50 transition-all duration-300 ${
250
+ isMaximized
251
+ ? "fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 m-0"
252
+ : "relative"
253
+ }`}
254
+ >
255
+ {/* Header */}
256
+ <div
257
+ className="px-4 py-3 text-white flex justify-between items-center shadow-md"
258
+ style={{ background: color }}
259
+ >
260
+ <div className="flex items-center gap-2 font-semibold">
261
+ <Bot className="w-5 h-5" />
262
+ {title}
263
+ </div>
264
+ <div className="flex items-center gap-1">
265
+ <button
266
+ onClick={clearHistory}
267
+ className="p-1.5 hover:bg-white/20 rounded-full transition-colors"
268
+ title="Sohbeti Temizle"
269
+ >
270
+ <RefreshCcw className="w-4 h-4" />
271
+ </button>
272
+ <button
273
+ onClick={() => setIsMaximized(!isMaximized)}
274
+ className="p-1.5 hover:bg-white/20 rounded-full transition-colors"
275
+ >
276
+ {isMaximized ? (
277
+ <Minimize2 className="w-4 h-4" />
278
+ ) : (
279
+ <Maximize2 className="w-4 h-4" />
280
+ )}
281
+ </button>
282
+ </div>
283
+ </div>
284
+
285
+ {/* Mesajlar */}
286
+ <div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
287
+ {messages.map((msg) => (
288
+ <div
289
+ key={msg.id}
290
+ className={`flex ${
291
+ msg.isBot ? "justify-start" : "justify-end"
292
+ } ${
293
+ msg.isBot && !msg.content && !msg.isError ? "hidden" : ""
294
+ }`}
295
+ >
296
+ <div
297
+ className={`max-w-[85%] rounded-2xl px-4 py-3 shadow-sm ${
298
+ msg.isBot
299
+ ? msg.isError
300
+ ? "bg-red-50 text-red-600 border border-red-100"
301
+ : "bg-white text-gray-800 border border-gray-100"
302
+ : "text-white"
303
+ }`}
304
+ style={!msg.isBot ? { backgroundColor: color } : {}}
305
+ >
306
+ <div className="max-w-none break-words">
307
+ <ReactMarkdown
308
+ remarkPlugins={[remarkGfm, remarkMath]}
309
+ rehypePlugins={[rehypeKatex, rehypeRaw]}
310
+ components={{
311
+ // Kod blokları - modern tasarım ile
312
+ code: ({ className, children, ...props }: any) => {
313
+ const match = /language-(\w+)/.exec(
314
+ className || ""
315
+ );
316
+ const language = match ? match[1] : "";
317
+ const isInline = !match;
318
+ const codeContent = String(children).replace(
319
+ /\n$/,
320
+ ""
321
+ );
322
+
323
+ return !isInline ? (
324
+ <div className="relative group my-2 sm:my-4 rounded-xl shadow-lg border border-gray-200 w-full overflow-hidden">
325
+ {/* Header with language and copy button */}
326
+ <div
327
+ className="flex items-center justify-between px-4 py-2 text-[#f3f4f6] border-b border-[#374151]"
328
+ style={{
329
+ background:
330
+ "linear-gradient(to right, #1f2937, #111827)",
331
+ fontSize: "14px",
332
+ }}
333
+ >
334
+ <div className="flex items-center gap-2 min-w-0 flex-1">
335
+ <Terminal className="w-4 h-4 text-blue-400 flex-shrink-0" />
336
+ <span className="font-medium capitalize truncate">
337
+ {language || "plaintext"}
338
+ </span>
339
+ </div>
340
+ <button
341
+ onClick={() => copyToClipboard(codeContent)}
342
+ className="flex items-center gap-1 px-3 py-1 bg-[#374151] hover:bg-[#4b5563] text-[#e5e7eb] rounded-md transition-all duration-200 text-xs font-medium cursor-pointer"
343
+ style={{ fontSize: "12px" }}
344
+ >
345
+ {copiedCode === codeContent ? (
346
+ <>
347
+ <CheckCircle className="w-3 h-3 text-green-400" />
348
+ <span>Kopyalandı!</span>
349
+ </>
350
+ ) : (
351
+ <>
352
+ <Copy className="w-3 h-3" />
353
+ <span>Kopyala</span>
354
+ </>
355
+ )}
356
+ </button>
357
+ </div>
358
+
359
+ {/* Syntax highlighted code */}
360
+ <div className="bg-gray-950 overflow-x-auto">
361
+ <SyntaxHighlighter
362
+ style={vscDarkPlus}
363
+ language={language || "text"}
364
+ customStyle={{
365
+ margin: 0,
366
+ padding: "1rem",
367
+ fontSize: "0.75rem",
368
+ lineHeight: "1.4",
369
+ background: "transparent",
370
+ borderRadius: 0,
371
+ }}
372
+ showLineNumbers={
373
+ codeContent.split("\n").length > 5
374
+ }
375
+ lineNumberStyle={{
376
+ color: "#6b7280",
377
+ fontSize: "0.7rem",
378
+ marginRight: "0.5rem",
379
+ userSelect: "none",
380
+ }}
381
+ wrapLongLines={false}
382
+ >
383
+ {codeContent}
384
+ </SyntaxHighlighter>
385
+ </div>
386
+ </div>
387
+ ) : (
388
+ <code
389
+ className="bg-blue-50 text-blue-700 px-2 py-1 rounded-md text-sm font-mono border border-blue-200 font-medium"
390
+ {...props}
391
+ >
392
+ {children}
393
+ </code>
394
+ );
395
+ },
396
+
397
+ // Pre etiketleri için beyaz arka plan
398
+ pre: ({ children }: any) => (
399
+ <pre className="bg-white max-w-full rounded-lg text-sm ">
400
+ {children}
401
+ </pre>
402
+ ),
403
+
404
+ // Başlıklar - modern hiyerarşi (Daha kompakt)
405
+ h1: ({ children }: any) => (
406
+ <h1 className="text-lg font-bold mb-2 text-gray-900 pb-2 border-b-2 border-gradient-to-r from-blue-500 to-purple-600 relative">
407
+ <span className="bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
408
+ {children}
409
+ </span>
410
+ </h1>
411
+ ),
412
+ h2: ({ children }: any) => (
413
+ <h2 className="text-base font-bold mb-2 text-gray-900 mt-4 flex items-center gap-2">
414
+ <span className="w-1 h-5 bg-gradient-to-b from-blue-500 to-purple-600 rounded-full"></span>
415
+ {children}
416
+ </h2>
417
+ ),
418
+ h3: ({ children }: any) => (
419
+ <h3 className="text-sm font-semibold mb-2 text-gray-800 mt-3 flex items-center gap-2">
420
+ <span className="w-1 h-4 bg-gradient-to-b from-blue-400 to-purple-500 rounded-full"></span>
421
+ {children}
422
+ </h3>
423
+ ),
424
+ h4: ({ children }: any) => (
425
+ <h4 className="text-xs font-semibold mb-1 text-gray-700 mt-2 flex items-center gap-2">
426
+ <span className="w-0.5 h-3 bg-gradient-to-b from-blue-400 to-purple-500 rounded-full"></span>
427
+ {children}
428
+ </h4>
429
+ ),
430
+
431
+ // Listeler - modern tasarım ile
432
+ ul: ({ children }: any) => (
433
+ <ul className="mb-2 space-y-0.5 text-sm">
434
+ {children}
435
+ </ul>
436
+ ),
437
+ ol: ({ children }: any) => (
438
+ <ol className="mt-3 mb-2 space-y-0.5 counter-reset-list text-sm">
439
+ {children}
440
+ </ol>
441
+ ),
442
+ li: ({ children, ...props }: any) => {
443
+ const isOrderedList =
444
+ props.className?.includes("ordered") ||
445
+ (typeof children === "object" &&
446
+ children?.props?.ordered);
447
+
448
+ return isOrderedList ? (
449
+ <li
450
+ className={`flex items-start gap-2 pl-1 leading-relaxed ${
451
+ !msg.isBot ? "text-white" : "text-gray-700"
452
+ }`}
453
+ >
454
+ <span className="flex-shrink-0 w-5 h-5 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-full flex items-center justify-center text-[10px] font-bold mt-0.5 shadow-sm counter-increment">
455
+
456
+ </span>
457
+ <div className="flex-1 pt-0.5 text-left">
458
+ {children}
459
+ </div>
460
+ </li>
461
+ ) : (
462
+ <li
463
+ className={`flex items-start gap-2 pl-1 leading-relaxed ${
464
+ !msg.isBot ? "text-white" : "text-gray-700"
465
+ }`}
466
+ >
467
+ <span className="flex-shrink-0 w-1.5 h-1.5 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full mt-2 shadow-sm"></span>
468
+ <div className="flex-1 text-left">
469
+ {children}
470
+ </div>
471
+ </li>
472
+ );
473
+ },
474
+
475
+ // Linkler
476
+ a: ({ href, children }: any) => (
477
+ <a
478
+ href={href}
479
+ className="text-blue-600 hover:text-blue-800 underline inline-flex items-center gap-0.5 transition-colors duration-200 break-all max-w-full text-sm"
480
+ target="_blank"
481
+ rel="noopener noreferrer"
482
+ >
483
+ <span className="break-all">{children}</span>
484
+ <ExternalLink className="w-3 h-3 flex-shrink-0" />
485
+ </a>
486
+ ),
487
+
488
+ // Alıntılar - modern tasarım
489
+ blockquote: ({ children }: any) => (
490
+ <blockquote className="relative my-2 sm:my-4 p-3 sm:p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg shadow-sm -mx-1 sm:mx-0">
491
+ <div className="absolute top-2 sm:top-3 left-2 sm:left-3">
492
+ <Quote className="w-3 h-3 sm:w-4 sm:h-4 text-blue-400 opacity-60" />
493
+ </div>
494
+ <div className="pl-4 sm:pl-6">
495
+ <div className="text-blue-600 text-[10px] sm:text-xs font-semibold mb-1 uppercase tracking-wide">
496
+ Alıntı
497
+ </div>
498
+ <div className="text-gray-700 italic leading-relaxed text-xs sm:text-sm">
499
+ {children}
500
+ </div>
501
+ </div>
502
+ <div className="absolute bottom-2 sm:bottom-3 right-2 sm:right-3">
503
+ <Quote className="w-2 h-2 sm:w-3 sm:h-3 text-blue-300 opacity-40 rotate-180" />
504
+ </div>
505
+ </blockquote>
506
+ ),
507
+
508
+ // Tablolar - modern responsive tasarım
509
+ table: ({ children }: any) => (
510
+ <div className="overflow-x-auto my-2 sm:my-4 rounded-lg border border-gray-200 shadow-sm bg-white -mx-1 sm:mx-0">
511
+ <table className="min-w-full text-xs">
512
+ {children}
513
+ </table>
514
+ </div>
515
+ ),
516
+ thead: ({ children }: any) => (
517
+ <thead className="bg-gradient-to-r from-gray-50 to-gray-100">
518
+ {children}
519
+ </thead>
520
+ ),
521
+ th: ({ children }: any) => (
522
+ <th className="px-2 sm:px-4 py-1.5 sm:py-2 text-left font-semibold text-gray-900 border-b border-gray-200 first:rounded-tl-lg last:rounded-tr-lg">
523
+ <div className="flex items-center gap-1">
524
+ <FileText className="w-3 h-3 text-gray-500 hidden sm:block" />
525
+ <span className="truncate">{children}</span>
526
+ </div>
527
+ </th>
528
+ ),
529
+ tbody: ({ children }: any) => (
530
+ <tbody className="divide-y divide-gray-100">
531
+ {children}
532
+ </tbody>
533
+ ),
534
+ tr: ({ children }: any) => (
535
+ <tr className="hover:bg-gray-50 transition-colors duration-150">
536
+ {children}
537
+ </tr>
538
+ ),
539
+ td: ({ children }: any) => (
540
+ <td className="px-2 sm:px-4 py-1.5 sm:py-2 text-gray-700 leading-relaxed text-xs">
541
+ <div
542
+ className="truncate max-w-[150px] sm:max-w-none"
543
+ title={
544
+ typeof children === "string" ? children : ""
545
+ }
546
+ >
547
+ {children}
548
+ </div>
549
+ </td>
550
+ ),
551
+
552
+ // Paragraflar - daha iyi spacing
553
+ p: ({ children }: any) => (
554
+ <p
555
+ className={`m-0! last:mb-0 leading-relaxed break-words overflow-wrap-anywhere text-sm ${
556
+ !msg.isBot ? "text-white" : "text-gray-700"
557
+ }`}
558
+ >
559
+ {children}
560
+ </p>
561
+ ),
562
+
563
+ // Vurgu metinleri
564
+ strong: ({ children }: any) => (
565
+ <strong
566
+ className={`font-bold inline ${
567
+ !msg.isBot ? "text-white" : "text-gray-900"
568
+ }`}
569
+ >
570
+ {children}
571
+ </strong>
572
+ ),
573
+ em: ({ children }: any) => (
574
+ <em
575
+ className={`italic ${
576
+ !msg.isBot ? "text-white/90" : "text-gray-600"
577
+ }`}
578
+ >
579
+ {children}
580
+ </em>
581
+ ),
582
+
583
+ // Çizgi
584
+ hr: () => <hr className="border-gray-300 my-4" />,
585
+
586
+ // Resimler - responsive
587
+ img: ({ src, alt }: any) => (
588
+ <img
589
+ src={src}
590
+ alt={alt}
591
+ className="max-w-full h-auto rounded-lg shadow-sm my-2"
592
+ loading="lazy"
593
+ />
594
+ ),
595
+
596
+ // Matematik formülleri için
597
+ div: ({ className, children, ...props }: any) => {
598
+ if (className === "math math-display") {
599
+ return (
600
+ <div className="my-4 text-center bg-gray-50 p-3 rounded-lg border">
601
+ <div className={className} {...props}>
602
+ {children}
603
+ </div>
604
+ </div>
605
+ );
606
+ }
607
+ return (
608
+ <div className={className} {...props}>
609
+ {children}
610
+ </div>
611
+ );
612
+ },
613
+
614
+ // Inline matematik
615
+ span: ({ className, children, ...props }: any) => {
616
+ if (className === "math math-inline") {
617
+ return (
618
+ <span className="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">
619
+ <span className={className} {...props}>
620
+ {children}
621
+ </span>
622
+ </span>
623
+ );
624
+ }
625
+ return (
626
+ <span className={className} {...props}>
627
+ {children}
628
+ </span>
629
+ );
630
+ },
631
+ }}
632
+ >
633
+ {msg.content}
634
+ </ReactMarkdown>
635
+ </div>
636
+ </div>
637
+ </div>
638
+ ))}
639
+ {isLoading && (
640
+ <div className="flex justify-start">
641
+ <div className="bg-white border border-gray-100 rounded-2xl px-4 py-3 shadow-sm flex items-center gap-2">
642
+ <span className="text-xs text-gray-500">Yazıyor</span>
643
+ <div className="flex space-x-1">
644
+ <div className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce [animation-delay:-0.3s]"></div>
645
+ <div className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce [animation-delay:-0.15s]"></div>
646
+ <div className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce"></div>
647
+ </div>
648
+ </div>
649
+ </div>
650
+ )}
651
+ <div ref={messagesEndRef} />
652
+ </div>
653
+
654
+ {/* Input Alanı */}
655
+ <div className="p-4 bg-white border-t border-gray-100">
656
+ <div className="flex gap-2 items-center bg-gray-50 rounded-full px-4 py-2 border border-gray-200 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500 transition-all">
657
+ <input
658
+ type="text"
659
+ value={input}
660
+ onChange={(e) => setInput(e.target.value)}
661
+ onKeyDown={(e) => e.key === "Enter" && sendMessage()}
662
+ placeholder="Mesajınızı yazın..."
663
+ className="flex-1 bg-transparent border-none focus:ring-0 outline-none text-sm py-1"
664
+ disabled={isLoading}
665
+ />
666
+ <button
667
+ onClick={sendMessage}
668
+ disabled={isLoading || !input.trim()}
669
+ className={`p-2 rounded-full transition-all ${
670
+ input.trim() && !isLoading
671
+ ? "text-blue-600 hover:bg-blue-50"
672
+ : "text-gray-400"
673
+ }`}
674
+ >
675
+ {isLoading ? (
676
+ <Loader2 className="w-5 h-5 animate-spin" />
677
+ ) : (
678
+ <Send className="w-5 h-5" />
679
+ )}
680
+ </button>
681
+ </div>
682
+ <div className="text-center mt-2">
683
+ <span className="text-[10px] text-gray-400">
684
+ Powered by Agenticgro
685
+ </span>
686
+ </div>
687
+ </div>
688
+ </motion.div>
689
+ )}
690
+ </AnimatePresence>
691
+
692
+ <motion.button
693
+ initial={{ scale: 0 }}
694
+ animate={{ scale: 1 }}
695
+ whileHover={{ scale: 1.1 }}
696
+ whileTap={{ scale: 0.9 }}
697
+ onClick={() => setIsOpen(!isOpen)}
698
+ className="w-14 h-14 rounded-full shadow-xl flex items-center justify-center text-white transition-shadow hover:shadow-2xl"
699
+ style={{ backgroundColor: color }}
700
+ >
701
+ {isOpen ? (
702
+ <X className="w-7 h-7" />
703
+ ) : (
704
+ <MessageSquare className="w-7 h-7" />
705
+ )}
706
+ </motion.button>
707
+ </div>
708
+ );
709
+ }