dhammawatthumpra commited on
Commit
728283f
·
1 Parent(s): 91c2e16

feat(ai): design improvements for chat popup, speeddial menu, resizable answer card, and centered header disclaimer

Browse files
webapp/tipitaka-web/src/components/ai/AIFloatingMenu.tsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence, type Variants } from 'framer-motion';
3
+ import { Sparkles, X, MessageSquareText, FileText, Scale, Lightbulb, MessageSquare } from 'lucide-react';
4
+ import { useAIStore } from '../../stores/aiStore';
5
+ import { useReaderStore, useThemeStore } from '../../stores/appStore';
6
+
7
+ const MENU_STYLES: Record<string, { btnBg: string; text: string; border: string; labelBg: string }> = {
8
+ dark: { btnBg: 'bg-[#1a1a2e]/90', text: 'text-white', border: 'border-[#3a3a5e]/60', labelBg: 'bg-[#0f0f1e]/95 text-white/90 border-[#3a3a5e]' },
9
+ light: { btnBg: 'bg-[#fdfaf5]/90', text: 'text-[#1a1a1a]', border: 'border-[#e0d0b0]/60', labelBg: 'bg-[#f5edd8]/95 text-[#1a1a1a] border-[#e0d0b0]' },
10
+ classic: { btnBg: 'bg-[#faf7f2]/90', text: 'text-[#1a1a1a]', border: 'border-[#d4c4a0]/60', labelBg: 'bg-[#fdfaf5]/95 text-[#1a1a1a] border-[#d4c4a0]' },
11
+ };
12
+
13
+ const AIFloatingMenu: React.FC = () => {
14
+ const [isOpenMenu, setIsOpenMenu] = useState(false);
15
+ const { theme } = useThemeStore();
16
+ const { currentVolume, currentPage, currentContent } = useReaderStore();
17
+ const { askAI, setViewMode, isOpen, toggleOpen, isStreaming } = useAIStore();
18
+
19
+ const s = MENU_STYLES[theme] ?? MENU_STYLES.dark;
20
+
21
+ const actions = [
22
+ {
23
+ icon: <MessageSquareText size={18} />,
24
+ label: 'อธิบาย',
25
+ prompt: 'ช่วยอธิบายเนื้อหานี้ให้เข้าใจง่ายขึ้น',
26
+ isPrompt: true,
27
+ },
28
+ {
29
+ icon: <FileText size={18} />,
30
+ label: 'สรุป',
31
+ prompt: 'ช่วยสรุปใจความสำคัญของเนื้อหานี้เป็นข้อๆ',
32
+ isPrompt: true,
33
+ },
34
+ {
35
+ icon: <Scale size={18} />,
36
+ label: 'วิเคราะห์ธรรม',
37
+ prompt: 'ช่วยวิเคราะห์หลักธรรมที่ปรากฏในเนื้อหานี้',
38
+ isPrompt: true,
39
+ },
40
+ {
41
+ icon: <Lightbulb size={18} />,
42
+ label: 'ประยุกต์ใช้',
43
+ prompt: 'หลักธรรมนี้ประยุกต์ใช้ในชีวิตประจำวันได้อย่างไร',
44
+ isPrompt: true,
45
+ },
46
+ {
47
+ icon: <MessageSquare size={18} />,
48
+ label: 'แชทสอบถาม',
49
+ prompt: null,
50
+ isPrompt: false,
51
+ },
52
+ ];
53
+
54
+ const handleAction = async (prompt: string | null, isPrompt: boolean) => {
55
+ setIsOpenMenu(false);
56
+
57
+ if (isPrompt && prompt) {
58
+ setViewMode('answer');
59
+ const context = `เล่มที่ ${currentVolume} หน้าที่ ${currentPage}\nเนื้อหา:\n${currentContent}`;
60
+ await askAI(prompt, context);
61
+ } else {
62
+ setViewMode('chat');
63
+ // If not open, open it
64
+ if (!isOpen) {
65
+ toggleOpen();
66
+ }
67
+ }
68
+ };
69
+
70
+ const containerVariants: Variants = {
71
+ open: {
72
+ transition: {
73
+ staggerChildren: 0.05,
74
+ },
75
+ },
76
+ closed: {
77
+ transition: {
78
+ staggerChildren: 0.05,
79
+ staggerDirection: -1,
80
+ },
81
+ },
82
+ };
83
+
84
+ const itemVariants: Variants = {
85
+ open: {
86
+ opacity: 1,
87
+ y: 0,
88
+ scale: 1,
89
+ transition: { type: 'spring', stiffness: 300, damping: 25 },
90
+ },
91
+ closed: {
92
+ opacity: 0,
93
+ y: 20,
94
+ scale: 0.8,
95
+ transition: { duration: 0.15 },
96
+ },
97
+ };
98
+
99
+ // If the main AI Chat/Answer window is open, hide the Speed Dial menu
100
+ if (isOpen) return null;
101
+
102
+ return (
103
+ <>
104
+ {/* Background Overlay (Click outside to close) */}
105
+ <AnimatePresence>
106
+ {isOpenMenu && (
107
+ <motion.div
108
+ initial={{ opacity: 0 }}
109
+ animate={{ opacity: 1 }}
110
+ exit={{ opacity: 0 }}
111
+ onClick={() => setIsOpenMenu(false)}
112
+ className="fixed inset-0 z-30 bg-black/10 backdrop-blur-[2px]"
113
+ />
114
+ )}
115
+ </AnimatePresence>
116
+
117
+ <div className="fixed right-14 bottom-14 lg:bottom-6 z-40 flex flex-col items-end">
118
+ {/* Speed Dial Menu Items */}
119
+ <AnimatePresence>
120
+ {isOpenMenu && (
121
+ <motion.div
122
+ variants={containerVariants}
123
+ initial="closed"
124
+ animate="open"
125
+ exit="closed"
126
+ className="flex flex-col items-end gap-3 mb-4 pointer-events-auto"
127
+ >
128
+ {actions.map((act, idx) => (
129
+ <motion.div
130
+ key={idx}
131
+ variants={itemVariants}
132
+ className="flex items-center group cursor-pointer"
133
+ onClick={() => handleAction(act.prompt, act.isPrompt)}
134
+ >
135
+ {/* Label Pill */}
136
+ <span className={`mr-3 px-3 py-1.5 rounded-xl text-[11px] font-bold border shadow-md whitespace-nowrap transition-transform duration-200 group-hover:scale-105 ${s.labelBg}`}>
137
+ {act.label}
138
+ </span>
139
+
140
+ {/* Action Button */}
141
+ <button
142
+ disabled={isStreaming}
143
+ className={`w-11 h-11 rounded-full flex items-center justify-center border shadow-lg backdrop-blur-md transition-all duration-200 group-hover:scale-110 active:scale-95 disabled:opacity-50 ${s.btnBg} ${s.text} ${s.border} hover:bg-[#c8860a] hover:text-white hover:border-[#c8860a]`}
144
+ title={act.label}
145
+ >
146
+ {act.icon}
147
+ </button>
148
+ </motion.div>
149
+ ))}
150
+ </motion.div>
151
+ )}
152
+ </AnimatePresence>
153
+
154
+ {/* Main Floating Action Button (FAB) */}
155
+ <motion.button
156
+ onClick={() => setIsOpenMenu(v => !v)}
157
+ className="w-14 h-14 rounded-full bg-[#c8860a] text-white flex items-center justify-center shadow-xl hover:bg-[#9a6307] transition-all duration-200 active:scale-95"
158
+ style={{ transformOrigin: 'center' }}
159
+ animate={{ rotate: isOpenMenu ? 135 : 0 }}
160
+ transition={{ type: 'spring', stiffness: 300, damping: 20 }}
161
+ title="ผู้ช่วย AI"
162
+ >
163
+ {isOpenMenu ? <X size={24} /> : <Sparkles size={24} />}
164
+ </motion.button>
165
+ </div>
166
+ </>
167
+ );
168
+ };
169
+
170
+ export default AIFloatingMenu;
webapp/tipitaka-web/src/components/ai/AIPopup.tsx CHANGED
@@ -12,20 +12,24 @@ const PANEL_STYLES: Record<string, { bg: string; text: string; border: string; m
12
  classic: { bg: 'bg-[#fdfaf5]', text: 'text-[#1a1a1a]', border: 'border-[#d4c4a0]', msgBg: 'bg-[#faf7f2]', focusBg: 'bg-[#f8f4ed]' },
13
  };
14
 
 
 
 
 
 
 
15
  const AIPopup: React.FC = () => {
16
  const {
17
  isOpen, toggleOpen, messages, isStreaming,
18
  askAI, clearHistory, mode, setMode,
19
  useRag, setUseRag, checkRagStatus, ragReady, ragChecking, ragLoading,
20
- dragPos, setDragPos,
21
  } = useAIStore();
22
 
23
  const { currentVolume, currentPage, currentContent } = useReaderStore();
24
  const { theme } = useThemeStore();
 
25
  const [input, setInput] = useState('');
26
- const [showAiDisclaimer, setShowAiDisclaimer] = useState(false);
27
- const [hasShownDisclaimer, setHasShownDisclaimer] = useState(false);
28
- const [showQuickPrompts, setShowQuickPrompts] = useState(true);
29
  const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
30
 
31
  const scrollRef = useRef<HTMLDivElement>(null);
@@ -69,23 +73,25 @@ const AIPopup: React.FC = () => {
69
  }
70
  }, []);
71
 
72
- // Disclaimer Logic: Show for 5s when opening
73
- useEffect(() => {
74
- if (isOpen && !hasShownDisclaimer) {
75
- setShowAiDisclaimer(true);
76
- const timer = setTimeout(() => {
77
- setShowAiDisclaimer(false);
78
- setHasShownDisclaimer(true);
79
- }, 10000);
80
- return () => clearTimeout(timer);
81
- }
82
- }, [isOpen, hasShownDisclaimer]);
83
 
84
  const handleMouseDown = useCallback((e: React.MouseEvent) => {
85
  // Only desktop — ignore if touch device or small screen
86
  if (window.innerWidth < 768) return;
87
  if (expandedIndex !== null) return; // Disable drag in focus mode
88
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  setIsDragging(true);
90
  const rect = panelRef.current?.getBoundingClientRect();
91
  if (rect) {
@@ -297,7 +303,7 @@ const AIPopup: React.FC = () => {
297
  exit={{ opacity: 0, scale: 0.95, y: 20 }}
298
  className={`
299
  fixed z-50 flex flex-col overflow-hidden rounded-2xl border shadow-2xl
300
- ${s.bg} ${s.text} ${s.border}
301
  bottom-24
302
  ${pos && window.innerWidth >= 768 ? '' : 'left-1/2 -translate-x-1/2 md:left-auto md:right-14 md:translate-x-0'}
303
  md:w-auto
@@ -306,7 +312,7 @@ const AIPopup: React.FC = () => {
306
  ${isDragging ? 'cursor-grabbing select-none' : ''}
307
  ${resizeDir ? 'select-none' : ''}
308
  ${expandedIndex !== null ? 'pointer-events-none opacity-0' : ''}
309
- transition-opacity duration-300
310
  `}
311
  style={{
312
  width: window.innerWidth >= 768 ? width : undefined,
@@ -336,198 +342,282 @@ const AIPopup: React.FC = () => {
336
  </div>
337
  </>
338
  )}
339
- {/* ═══════════ Compact Header + Drag Handle ═══════════ */}
340
- <div
341
- className="flex flex-col flex-shrink-0 relative overflow-hidden"
342
- onMouseDown={handleMouseDown}
343
- style={{ cursor: window.innerWidth >= 768 ? 'grab' : undefined }}
344
- >
345
- {/* Glass background layer */}
346
- <div className="absolute inset-0 bg-[#c8860a] opacity-90" />
347
- <div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent pointer-events-none" />
348
-
349
- {/* Row 1: Title + actions */}
350
- <div className="relative flex items-center justify-between px-3 py-2.5 z-10">
351
- <div className="flex items-center gap-2.5 min-w-0">
352
- <div className="w-7 h-7 rounded-lg bg-white/20 flex items-center justify-center shadow-inner">
353
- <Sparkles size={16} className="text-white" />
354
- </div>
355
- <div>
356
- <h3 className="font-bold text-sm tracking-wide text-white">ช่วย AI</h3>
357
- <p className="text-[9px] text-white/70 font-medium uppercase tracking-tighter">Dhamma Assistant</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  </div>
359
  </div>
360
- <div className="flex items-center gap-1 flex-shrink-0">
361
- <div className="flex bg-black/15 rounded-lg p-0.5 mr-1">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  <button
363
- onClick={() => setMode('fast')}
364
- onMouseDown={e => e.stopPropagation()}
365
- className={`flex items-center gap-1 px-2 py-1 text-[10px] font-bold uppercase rounded-md transition-all ${
366
- mode === 'fast' ? 'bg-white/20 text-white' : 'text-white/60 hover:text-white/90'
367
- }`}
368
- ><Zap size={10} /> เร็ว</button>
 
369
  <button
370
- onClick={() => setMode('reasoner')}
371
- onMouseDown={e => e.stopPropagation()}
372
- className={`flex items-center gap-1 px-2 py-1 text-[10px] font-bold uppercase rounded-md transition-all ${
373
- mode === 'reasoner' ? 'bg-white/20 text-white' : 'text-white/60 hover:text-white/90'
374
- }`}
375
- ><Brain size={10} /> คิดลึก</button>
376
  </div>
377
- <button onClick={clearHistory} onMouseDown={e => e.stopPropagation()} className="p-1.5 hover:bg-white/20 rounded-lg transition-colors" title="ล้างการสนทนา" aria-label="ล้างการสนทนา"><Trash2 size={14} /></button>
378
- <button onClick={toggleOpen} onMouseDown={e => e.stopPropagation()} className="p-1.5 hover:bg-white/20 rounded-lg transition-colors" aria-label="ปิด"><X size={16} /></button>
379
  </div>
380
- </div>
381
-
382
- {/* Row 2: RAG toggle + Quick prompt toggle */}
383
- <div className="flex items-center justify-between px-3 py-1 border-t border-white/10">
384
- <button
385
- onClick={() => setUseRag(!useRag)}
386
- onMouseDown={e => e.stopPropagation()}
387
- className="flex items-center gap-1.5 text-[10px] font-medium text-white/60 hover:text-white transition-colors"
388
  >
389
- <span className="flex items-center gap-1">
390
- <span className={`inline-block w-1.5 h-1.5 rounded-full ${ragDot}`} title={
391
- ragLoading ? 'RAG: กำลังโหลดโมเดล...' : ragReady ? 'RAG พร้อม' : 'RAG ไม่พร้อม'
392
- } />
393
- 📚 ค้นเล่มอื่น
394
- </span>
395
- <span className={`inline-flex items-center px-0.5 w-7 h-3.5 rounded-full transition-colors ${
396
- useRag ? 'bg-white/50 justify-end' : 'bg-white/20 justify-start'
397
- }`}>
398
- <span className="w-2.5 h-2.5 bg-white rounded-full shadow-xs" />
399
- </span>
400
- </button>
401
- <button
402
- onClick={() => setShowQuickPrompts(v => !v)}
403
- onMouseDown={e => e.stopPropagation()}
404
- className="text-[10px] font-bold text-white/50 hover:text-white transition-colors"
405
- >{showQuickPrompts ? '▲ ซ่อนปุ่มลัด' : '▼ ปุ่มลัด'}</button>
406
- </div>
407
- </div>
408
-
409
- {/* ═══════════ Quick Prompt / Disclaimer ═══════════ */}
410
- <div className={`relative overflow-hidden transition-all duration-300 ${
411
- (showQuickPrompts || showAiDisclaimer) ? 'max-h-40 border-b py-2' : 'max-h-0 border-transparent'
412
- } ${s.border}`}>
413
- <AnimatePresence mode="wait">
414
- {showAiDisclaimer ? (
415
- <motion.div
416
- key="disclaimer"
417
- initial={{ opacity: 0, y: 10 }}
418
- animate={{ opacity: 1, y: 0 }}
419
- exit={{ opacity: 0, y: -10 }}
420
- className="px-4 py-1"
421
- >
422
- <div className="bg-[#c8860a]/10 border border-[#c8860a]/30 rounded-xl p-2.5 relative">
423
- <button
424
- onClick={() => { setShowAiDisclaimer(false); setHasShownDisclaimer(true); }}
425
- className="absolute top-1 right-1 p-1 opacity-50 hover:opacity-100 transition-opacity"
426
- >
427
- <X size={12} />
428
- </button>
429
- <p className="text-[10.5px] leading-relaxed text-[#c8860a] font-medium pr-4">
430
- <span className="font-bold">💡 ข้อชี้แจง:</span> บทวิเคราะห์นี้ประมวลผลโดยเทคโนโลยีปัญญาประดิษฐ์ (AI) ผู้อ่านพึงใช้วิจารณญาณและสอบทานข้อมูลอย่างรอบด้านตามหลักกาลามสูตร ก่อนนำไปอ้างอิงหรือปฏิบัติ
431
- </p>
432
  </div>
433
- </motion.div>
434
- ) : showQuickPrompts && (
435
- <motion.div
436
- key="prompts"
437
- initial={{ opacity: 0 }}
438
- animate={{ opacity: 1 }}
439
- exit={{ opacity: 0 }}
440
- className="grid grid-cols-2 gap-1.5 px-3"
441
- >
442
- {[
443
- { emoji: '💬', text: 'อธิบาย', prompt: 'ช่วยอธิบายเนื้อหานี้ให้เข้าใจง่ายขึ้น' },
444
- { emoji: '📝', text: 'สรุป', prompt: 'ช่วยสรุปใจความสำคัญของเนื้อหานี้เป็นข้อๆ' },
445
- { emoji: '⚖️', text: 'วิเคราะห์ธรรม', prompt: 'ช่วยวิเคราะห์หลักธรรมที่ปรากฏในเนื้อหานี้' },
446
- { emoji: '💡', text: 'ประยุกต์ใช้', prompt: 'หลักธรรมนี้ประยุกต์ใช้ในชีวิตประจำวันได้อย่างไร' },
447
- ].map(({ emoji, text, prompt }) => (
448
- <button key={text} onClick={() => { const c = `เล่มที่ ${currentVolume} หน้าที่ ${currentPage}\nเนื้อหา:\n${currentContent}`; askAI(prompt, c); }}
449
- disabled={isStreaming}
450
- className={`flex items-center justify-center gap-1 px-2 py-1.5 text-[11px] font-medium rounded-xl border transition-all ${s.msgBg} ${s.border} hover:border-[#c8860a]/50 hover:text-[#c8860a] disabled:opacity-40`}
451
- >{emoji} {text}</button>
452
- ))}
453
- </motion.div>
454
- )}
455
- </AnimatePresence>
456
- </div>
457
-
458
- {/* ═══════════ Messages ═══════════ */}
459
- <div className="relative flex-1 min-h-0 flex flex-col">
460
- <div ref={scrollRef} className="flex-1 overflow-y-auto p-3 md:p-4 space-y-4">
461
- {messages.length === 0 && (
462
- <div className="h-full flex flex-col items-center justify-center text-center opacity-50 px-8 gap-3">
463
- <div className={`w-12 h-12 rounded-full flex items-center justify-center ${s.msgBg}`}>
464
- <Sparkles size={24} className="text-[#c8860a]" />
465
  </div>
466
- <div>
467
- <p className="text-sm font-bold mb-0.5">ยินดีต้อนรับ</p>
468
- <p className="text-xs opacity-60">องถมเี่ยวกับการสรุปห้าปัจจุบัน หรือคำศัพ์บาลีที่สสัยดูคับ</p>
 
469
  </div>
470
  </div>
471
- )}
472
-
473
- {messages.map((msg, i) => (
474
- <div key={i} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'} group`}>
475
- <div className={`relative max-w-[90%] rounded-2xl p-3 text-sm ${
476
- msg.role === 'user'
477
- ? 'bg-[#c8860a] text-white rounded-tr-none'
478
- : `${s.msgBg} border ${s.border} text-inherit rounded-tl-none`
479
- }`}>
480
- {msg.role === 'assistant' && msg.thinking && (
481
- <div className={`mb-2 p-2 rounded-lg border-l-2 border-[#c8860a]/30 text-[10px] italic opacity-70 whitespace-pre-wrap ${s.msgBg}`}>
482
- <span className="font-bold flex items-center gap-1 mb-1 not-italic text-[#c8860a]/60">
483
- <Brain size={10} /> กำลังคิด...
484
- </span>
485
- {msg.thinking}
486
- </div>
487
- )}
488
- <div className={`prose prose-sm max-w-none ${proseTheme} prose-headings:text-inherit prose-headings:font-bold prose-headings:mt-3 prose-headings:mb-1 prose-p:my-1 prose-ul:my-1 prose-li:my-0.5 prose-table:text-xs prose-table:border-collapse prose-th:border prose-th:border-[#c8860a]/30 prose-th:bg-[#c8860a]/10 prose-th:px-2 prose-th:py-1 prose-td:border prose-td:border-[#c8860a]/20 prose-td:px-2 prose-td:py-1 prose-code:bg-[#c8860a]/10 prose-code:px-1 prose-code:rounded prose-pre:bg-black/20 prose-pre:rounded-xl prose-pre:p-3 prose-pre:text-xs prose-strong:text-inherit prose-a:text-[#c8860a] leading-relaxed`}>
489
- {msg.content || (isStreaming && i === messages.length - 1 ? '...' : '') ? (
490
- <ReactMarkdown remarkPlugins={[remarkGfm]}>
491
- {msg.content || (isStreaming && i === messages.length - 1 ? '...' : '')}
492
- </ReactMarkdown>
493
- ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  </div>
495
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
  </div>
497
- ))}
498
- </div>
499
 
500
- {/* Persistent Floating Maximize Button (Visible when scrolled) */}
501
- {messages.some(m => m.role === 'assistant') && !isStreaming && (
502
- <div className="absolute bottom-4 right-4 z-10">
503
- <button
504
- onClick={() => {
505
- const lastAi = messages.findLastIndex(m => m.role === 'assistant');
506
- if (lastAi !== -1) setExpandedIndex(lastAi);
507
- }}
508
- className="p-3 bg-white/10 backdrop-blur-md hover:bg-[#c8860a] text-[#c8860a] hover:text-white rounded-2xl shadow-xl transition-all border border-[#c8860a]/20 hover:border-[#c8860a] active:scale-95"
509
- title="อ่านแบบเต็มจอ"
510
- >
511
- <Maximize2 size={20} />
512
- </button>
 
 
513
  </div>
514
- )}
515
- </div>
516
-
517
- {/* ═══════════ Input ═══════════ */}
518
- <form onSubmit={handleSubmit} className={`p-3 md:p-4 border-t ${s.border} flex gap-2 flex-shrink-0 ${s.bg}`}>
519
- <div className="relative flex-1">
520
- <input type="text" value={input} onChange={(e) => setInput(e.target.value)}
521
- placeholder="ถามเกี่ยวกับพระธรรมในหน้านี้..." disabled={isStreaming}
522
- className={`w-full pl-4 pr-12 py-2.5 rounded-2xl outline-none text-sm disabled:opacity-50 border shadow-inner transition-all ${s.border} ${s.msgBg} ${s.text} focus:border-[#c8860a] focus:ring-2 focus:ring-[#c8860a]/20`}
523
- />
524
- <button type="submit" disabled={!input.trim() || isStreaming}
525
- className="absolute right-1.5 top-1.5 p-2 bg-[#c8860a] text-white rounded-xl disabled:opacity-50 hover:bg-[#9a6307] transition-all shadow-md active:scale-95"
526
- >
527
- <Send size={14} />
528
- </button>
529
- </div>
530
- </form>
531
  </motion.div>
532
  </>
533
  )}
 
12
  classic: { bg: 'bg-[#fdfaf5]', text: 'text-[#1a1a1a]', border: 'border-[#d4c4a0]', msgBg: 'bg-[#faf7f2]', focusBg: 'bg-[#f8f4ed]' },
13
  };
14
 
15
+ const ANSWER_CARD_STYLES: Record<string, { bg: string; border: string; text: string; btnBg: string; titleBg: string }> = {
16
+ dark: { bg: 'bg-[#0f0f1e]/80 backdrop-blur-md', border: 'border-[#3a3a5e]/80', text: 'text-[#e0e0e0]', btnBg: 'bg-white/10 hover:bg-[#c8860a] text-white border-white/10 hover:border-[#c8860a]', titleBg: 'bg-black/20' },
17
+ light: { bg: 'bg-[#f5edd8]/85 backdrop-blur-md', border: 'border-[#e0d0b0]/80', text: 'text-[#1a1a1a]', btnBg: 'bg-black/5 hover:bg-[#c8860a] text-neutral-800 hover:text-white border-black/10 hover:border-[#c8860a]', titleBg: 'bg-black/5' },
18
+ classic: { bg: 'bg-[#fdfaf5]/85 backdrop-blur-md', border: 'border-[#d4c4a0]/80', text: 'text-[#1a1a1a]', btnBg: 'bg-black/5 hover:bg-[#c8860a] text-neutral-800 hover:text-white border-black/10 hover:border-[#c8860a]', titleBg: 'bg-black/5' },
19
+ };
20
+
21
  const AIPopup: React.FC = () => {
22
  const {
23
  isOpen, toggleOpen, messages, isStreaming,
24
  askAI, clearHistory, mode, setMode,
25
  useRag, setUseRag, checkRagStatus, ragReady, ragChecking, ragLoading,
26
+ dragPos, setDragPos, viewMode, setViewMode,
27
  } = useAIStore();
28
 
29
  const { currentVolume, currentPage, currentContent } = useReaderStore();
30
  const { theme } = useThemeStore();
31
+ const ac = ANSWER_CARD_STYLES[theme] ?? ANSWER_CARD_STYLES.dark;
32
  const [input, setInput] = useState('');
 
 
 
33
  const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
34
 
35
  const scrollRef = useRef<HTMLDivElement>(null);
 
73
  }
74
  }, []);
75
 
76
+
 
 
 
 
 
 
 
 
 
 
77
 
78
  const handleMouseDown = useCallback((e: React.MouseEvent) => {
79
  // Only desktop — ignore if touch device or small screen
80
  if (window.innerWidth < 768) return;
81
  if (expandedIndex !== null) return; // Disable drag in focus mode
82
 
83
+ // Check if the click was on interactive elements
84
+ const target = e.target as HTMLElement;
85
+ if (
86
+ target.closest('button') ||
87
+ target.closest('input') ||
88
+ target.closest('select') ||
89
+ target.closest('a') ||
90
+ target.closest('[role="button"]')
91
+ ) {
92
+ return;
93
+ }
94
+
95
  setIsDragging(true);
96
  const rect = panelRef.current?.getBoundingClientRect();
97
  if (rect) {
 
303
  exit={{ opacity: 0, scale: 0.95, y: 20 }}
304
  className={`
305
  fixed z-50 flex flex-col overflow-hidden rounded-2xl border shadow-2xl
306
+ ${viewMode === 'answer' ? `${ac.bg} ${ac.text} ${ac.border}` : `${s.bg} ${s.text} ${s.border}`}
307
  bottom-24
308
  ${pos && window.innerWidth >= 768 ? '' : 'left-1/2 -translate-x-1/2 md:left-auto md:right-14 md:translate-x-0'}
309
  md:w-auto
 
312
  ${isDragging ? 'cursor-grabbing select-none' : ''}
313
  ${resizeDir ? 'select-none' : ''}
314
  ${expandedIndex !== null ? 'pointer-events-none opacity-0' : ''}
315
+ transition-all duration-300
316
  `}
317
  style={{
318
  width: window.innerWidth >= 768 ? width : undefined,
 
342
  </div>
343
  </>
344
  )}
345
+
346
+ {viewMode === 'answer' ? (
347
+ <>
348
+ {/* ═══════════ Compact Answer Card Header ═══════════ */}
349
+ <div
350
+ className={`flex flex-col flex-shrink-0 relative overflow-hidden border-b ${ac.border}`}
351
+ onMouseDown={handleMouseDown}
352
+ style={{ cursor: window.innerWidth >= 768 ? 'grab' : undefined }}
353
+ >
354
+ <div className={`absolute inset-0 ${ac.titleBg}`} />
355
+ <div className="relative flex items-center justify-between px-3 py-2.5 z-10">
356
+ <div className="flex items-center gap-2.5 min-w-0">
357
+ <div className="w-7 h-7 rounded-lg bg-[#c8860a] flex items-center justify-center shadow-inner">
358
+ <Sparkles size={15} className="text-white animate-pulse" />
359
+ </div>
360
+ <div>
361
+ <h3 className="font-bold text-xs tracking-wide uppercase text-[#c8860a]">
362
+ {isStreaming ? 'กำลังประมวลลด้วย AI...' : 'บทวิเคราะห์ธรรมจาก AI'}
363
+ </h3>
364
+ </div>
365
+ </div>
366
+
367
+ {/* Disclaimer in the middle of title bar (Desktop only) */}
368
+ <div className="hidden md:block absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-[9.5px] font-semibold text-[#c8860a] max-w-[50%] text-center leading-normal select-none pointer-events-none animate-pulse">
369
+ 💡 บทวิเคราะห์นี้ประมวลผลโดย AI ผู้อ่านพึงใช้วิจารณญาณและสอบทานข้อมูลอย่างรอบด้านตามหลักกาลามสูตร
370
+ </div>
371
+
372
+ <button
373
+ onClick={toggleOpen}
374
+ onMouseDown={e => e.stopPropagation()}
375
+ className="p-1.5 hover:bg-white/10 rounded-lg transition-colors"
376
+ aria-label="ปิด"
377
+ >
378
+ <X size={16} />
379
+ </button>
380
  </div>
381
  </div>
382
+
383
+ {/* ═══════════ Answer Card Body (Prose with transparent styles) ═══════════ */}
384
+ <div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-4">
385
+ {(() => {
386
+ const lastAiIndex = messages.findLastIndex(m => m.role === 'assistant');
387
+ const lastAi = lastAiIndex !== -1 ? messages[lastAiIndex] : null;
388
+
389
+ if (!lastAi) {
390
+ return (
391
+ <div className="h-full flex flex-col items-center justify-center text-center opacity-50 gap-2">
392
+ <Sparkles size={20} className="animate-pulse text-[#c8860a]" />
393
+ <p className="text-xs">กำลังเตรียมคำตอบ...</p>
394
+ </div>
395
+ );
396
+ }
397
+
398
+ return (
399
+ <div className="flex flex-col items-start w-full">
400
+ {lastAi.thinking && (
401
+ <div className="w-full mb-3 p-3 rounded-xl border-l-2 border-[#c8860a]/30 text-[10.5px] italic opacity-70 bg-black/15 whitespace-pre-wrap">
402
+ <span className="font-bold flex items-center gap-1 mb-1 not-italic text-[#c8860a]/85">
403
+ <Brain size={11} /> กระบวนการคิด...
404
+ </span>
405
+ {lastAi.thinking}
406
+ </div>
407
+ )}
408
+ <div className={`prose prose-sm max-w-none w-full ${proseTheme} prose-headings:text-inherit prose-headings:font-bold prose-headings:mt-3 prose-headings:mb-1 prose-p:my-1.5 prose-ul:my-1.5 prose-li:my-0.5 prose-table:text-xs prose-table:border-collapse prose-th:border prose-th:border-[#c8860a]/30 prose-th:bg-[#c8860a]/10 prose-th:px-2 prose-th:py-1 prose-td:border prose-td:border-[#c8860a]/20 prose-td:px-2 prose-td:py-1 prose-code:bg-[#c8860a]/10 prose-code:px-1 prose-code:rounded prose-pre:bg-black/20 prose-pre:rounded-xl prose-pre:p-3 prose-pre:text-xs prose-strong:text-inherit prose-a:text-[#c8860a] leading-relaxed`}>
409
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
410
+ {lastAi.content || (isStreaming ? '...' : '')}
411
+ </ReactMarkdown>
412
+ </div>
413
+ </div>
414
+ );
415
+ })()}
416
+ </div>
417
+
418
+ {/* ═══════════ Answer Card Footer ═══════════ */}
419
+ <div className={`p-3 border-t flex flex-col gap-2.5 flex-shrink-0 ${ac.border}`}>
420
+ {/* Action Buttons */}
421
+ <div className="flex gap-2 justify-end">
422
+ {(() => {
423
+ const lastAiIdx = messages.findLastIndex(m => m.role === 'assistant');
424
+ return (
425
+ <button
426
+ onClick={() => {
427
+ if (lastAiIdx !== -1) setExpandedIndex(lastAiIdx);
428
+ }}
429
+ disabled={isStreaming || lastAiIdx === -1}
430
+ className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-bold border transition-all active:scale-95 disabled:opacity-40 ${ac.btnBg}`}
431
+ >
432
+ <Maximize2 size={13} />
433
+ อ่านเต็มจอ
434
+ </button>
435
+ );
436
+ })()}
437
  <button
438
+ onClick={() => setViewMode('chat')}
439
+ disabled={isStreaming}
440
+ className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-bold border transition-all active:scale-95 disabled:opacity-40 ${ac.btnBg}`}
441
+ >
442
+ <Send size={13} />
443
+ คุยต่อ
444
+ </button>
445
  <button
446
+ onClick={toggleOpen}
447
+ className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-bold border transition-all active:scale-95 ${ac.btnBg}`}
448
+ >
449
+ <X size={13} />
450
+ ปิด
451
+ </button>
452
  </div>
 
 
453
  </div>
454
+ </>
455
+ ) : (
456
+ <>
457
+ {/* ═══════════ Compact Chat Header ═══════════ */}
458
+ <div
459
+ className="flex flex-col flex-shrink-0 relative overflow-hidden"
460
+ onMouseDown={handleMouseDown}
461
+ style={{ cursor: window.innerWidth >= 768 ? 'grab' : undefined }}
462
  >
463
+ {/* Glass background layer */}
464
+ <div className="absolute inset-0 bg-[#c8860a] opacity-90" />
465
+ <div className="absolute inset-0 bg-gradient-to-br from-white/20 to-transparent pointer-events-none" />
466
+
467
+ {/* Row 1: Title + actions */}
468
+ <div className="relative flex items-center justify-between px-3 py-2.5 z-10">
469
+ <div className="flex items-center gap-2.5 min-w-0">
470
+ <div className="w-7 h-7 rounded-lg bg-white/20 flex items-center justify-center shadow-inner">
471
+ <Sparkles size={16} className="text-white" />
472
+ </div>
473
+ <div>
474
+ <h3 className="font-bold text-sm tracking-wide text-white">ผู้ช่วย AI</h3>
475
+ <p className="text-[9px] text-white/70 font-medium uppercase tracking-tighter">Dhamma Assistant</p>
476
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  </div>
478
+
479
+ {/* Disclaimer in the middle of title bar (Desktop only) */}
480
+ <div className="hidden md:block absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-[9.5px] font-semibold text-[#ffe0a0] max-w-[50%] text-center leading-normal select-none pointer-events-none">
481
+ 💡 บทวิเคราะห์นี้ประมวลผลโดย AI ผู้อ่านพึงใช้วิจารณญาณและสอบทานข้อมูลอย่างรอบด้านตามหลักกาลามสูตร
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  </div>
483
+
484
+ <div className="flex items-center gap-1 flex-shrink-0">
485
+ <button onClick={clearHistory} onMouseDown={e => e.stopPropagation()} className="p-1.5 hover:bg-white/20 rounded-lg transition-colors text-white" title="การสนท" aria-label="้ากาสนทนา"><Trash2 size={14} /></button>
486
+ <button onClick={toggleOpen} onMouseDown={e => e.stopPropagation()} className="p-1.5 hover:bg-white/20 rounded-lg transition-colors text-white" aria-label="ปิด"><X size={16} /></button>
487
  </div>
488
  </div>
489
+ </div>
490
+
491
+ {/* ═══════════ Settings Bar (Row 2) with high contrast ═══════════ */}
492
+ {(() => {
493
+ const settingsBg = theme === 'dark' ? 'bg-[#15152a]' : theme === 'classic' ? 'bg-[#f4ebe0]' : 'bg-[#ece4cf]';
494
+ const settingsBorder = theme === 'dark' ? 'border-[#3a3a5e]/50' : theme === 'classic' ? 'border-[#d4c4a0]/50' : 'border-[#e0d0b0]/50';
495
+ const settingsText = theme === 'dark' ? 'text-white/80 hover:text-white' : 'text-neutral-700 hover:text-neutral-900';
496
+ return (
497
+ <div className={`flex items-center justify-between px-3 py-1.5 border-b ${settingsBorder} ${settingsBg} ${settingsText} z-10`}>
498
+ {/* RAG Toggle */}
499
+ <button
500
+ onClick={() => setUseRag(!useRag)}
501
+ onMouseDown={e => e.stopPropagation()}
502
+ className="flex items-center gap-1.5 text-[10px] font-semibold transition-colors"
503
+ >
504
+ <span className="flex items-center gap-1.5">
505
+ <span className={`inline-block w-1.5 h-1.5 rounded-full ${ragDot}`} title={
506
+ ragLoading ? 'RAG: กำลังโหลดโมเดล...' : ragReady ? 'RAG พร้อม' : 'RAG ไม่พร้อม'
507
+ } />
508
+ 📚 ค้นเล่มอื่น
509
+ </span>
510
+ <span className={`inline-flex items-center px-0.5 w-7 h-3.5 rounded-full transition-colors ${
511
+ useRag
512
+ ? 'bg-[#c8860a] justify-end'
513
+ : (theme === 'dark' ? 'bg-white/10 justify-start' : 'bg-black/10 justify-start')
514
+ }`}>
515
+ <span className="w-2.5 h-2.5 bg-white rounded-full shadow-xs" />
516
+ </span>
517
+ </button>
518
+
519
+ {/* Model switcher */}
520
+ <div className="flex bg-black/10 rounded-lg p-0.5 border border-white/5">
521
+ <button
522
+ onClick={() => setMode('fast')}
523
+ onMouseDown={e => e.stopPropagation()}
524
+ className={`flex items-center gap-1 px-2 py-0.5 text-[10px] font-bold uppercase rounded-md transition-all ${
525
+ mode === 'fast' ? 'bg-[#c8860a] text-white shadow-sm' : 'opacity-60 hover:opacity-100'
526
+ }`}
527
+ >
528
+ <Zap size={9} /> เร็ว
529
+ </button>
530
+ <button
531
+ onClick={() => setMode('reasoner')}
532
+ onMouseDown={e => e.stopPropagation()}
533
+ className={`flex items-center gap-1 px-2 py-0.5 text-[10px] font-bold uppercase rounded-md transition-all ${
534
+ mode === 'reasoner' ? 'bg-[#c8860a] text-white shadow-sm' : 'opacity-60 hover:opacity-100'
535
+ }`}
536
+ >
537
+ <Brain size={9} /> คิดลึก
538
+ </button>
539
  </div>
540
  </div>
541
+ );
542
+ })()}
543
+
544
+
545
+
546
+ {/* ═══════════ Messages Area ═══════════ */}
547
+ <div className="relative flex-1 min-h-0 flex flex-col">
548
+ <div ref={scrollRef} className="flex-1 overflow-y-auto p-3 md:p-4 space-y-4">
549
+ {messages.length === 0 && (
550
+ <div className="h-full flex flex-col items-center justify-center text-center opacity-50 px-8 gap-3">
551
+ <div className={`w-12 h-12 rounded-full flex items-center justify-center ${s.msgBg}`}>
552
+ <Sparkles size={24} className="text-[#c8860a]" />
553
+ </div>
554
+ <div>
555
+ <p className="text-sm font-bold mb-0.5">ยินดีต้อนรับ</p>
556
+ <p className="text-xs opacity-60">ลองถามเกี่ยวกับการสรุปหน้าปัจจุบัน หรือคำศัพท์บาลีที่สงสัยดูครับ</p>
557
+ </div>
558
+ </div>
559
+ )}
560
+
561
+ {messages.map((msg, i) => (
562
+ <div key={i} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'} group`}>
563
+ <div className={`relative max-w-[90%] rounded-2xl p-3 text-sm ${
564
+ msg.role === 'user'
565
+ ? 'bg-[#c8860a] text-white rounded-tr-none'
566
+ : `${s.msgBg} border ${s.border} text-inherit rounded-tl-none`
567
+ }`}>
568
+ {msg.role === 'assistant' && msg.thinking && (
569
+ <div className={`mb-2 p-2 rounded-lg border-l-2 border-[#c8860a]/30 text-[10px] italic opacity-70 whitespace-pre-wrap ${s.msgBg}`}>
570
+ <span className="font-bold flex items-center gap-1 mb-1 not-italic text-[#c8860a]/60">
571
+ <Brain size={10} /> กำลังคิด...
572
+ </span>
573
+ {msg.thinking}
574
+ </div>
575
+ )}
576
+ <div className={`prose prose-sm max-w-none ${proseTheme} prose-headings:text-inherit prose-headings:font-bold prose-headings:mt-3 prose-headings:mb-1 prose-p:my-1 prose-ul:my-1 prose-li:my-0.5 prose-table:text-xs prose-table:border-collapse prose-th:border prose-th:border-[#c8860a]/30 prose-th:bg-[#c8860a]/10 prose-th:px-2 prose-th:py-1 prose-td:border prose-td:border-[#c8860a]/20 prose-td:px-2 prose-td:py-1 prose-code:bg-[#c8860a]/10 prose-code:px-1 prose-code:rounded prose-pre:bg-black/20 prose-pre:rounded-xl prose-pre:p-3 prose-pre:text-xs prose-strong:text-inherit prose-a:text-[#c8860a] leading-relaxed`}>
577
+ {msg.content || (isStreaming && i === messages.length - 1 ? '...' : '') ? (
578
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
579
+ {msg.content || (isStreaming && i === messages.length - 1 ? '...' : '')}
580
+ </ReactMarkdown>
581
+ ) : null}
582
+ </div>
583
+ </div>
584
+ </div>
585
+ ))}
586
  </div>
 
 
587
 
588
+ {/* Persistent Floating Maximize Button (Visible when scrolled) */}
589
+ {messages.some(m => m.role === 'assistant') && !isStreaming && (
590
+ <div className="absolute bottom-4 right-4 z-10">
591
+ <button
592
+ onClick={() => {
593
+ const lastAi = messages.findLastIndex(m => m.role === 'assistant');
594
+ if (lastAi !== -1) setExpandedIndex(lastAi);
595
+ }}
596
+ className="p-3 bg-white/10 backdrop-blur-md hover:bg-[#c8860a] text-[#c8860a] hover:text-white rounded-2xl shadow-xl transition-all border border-[#c8860a]/20 hover:border-[#c8860a] active:scale-95"
597
+ title="อ่านแบบเต็มจอ"
598
+ >
599
+ <Maximize2 size={20} />
600
+ </button>
601
+ </div>
602
+ )}
603
  </div>
604
+
605
+ {/* ═══════════ Input ═══════════ */}
606
+ <form onSubmit={handleSubmit} className={`p-3 md:p-4 border-t ${s.border} flex gap-2 flex-shrink-0 ${s.bg}`}>
607
+ <div className="relative flex-1">
608
+ <input type="text" value={input} onChange={(e) => setInput(e.target.value)}
609
+ placeholder="ถามเกี่ยวกับพระธรรมในหน้านี้..." disabled={isStreaming}
610
+ className={`w-full pl-4 pr-12 py-2.5 rounded-2xl outline-none text-sm disabled:opacity-50 border shadow-inner transition-all ${s.border} ${s.msgBg} ${s.text} focus:border-[#c8860a] focus:ring-2 focus:ring-[#c8860a]/20`}
611
+ />
612
+ <button type="submit" disabled={!input.trim() || isStreaming}
613
+ className="absolute right-1.5 top-1.5 p-2 bg-[#c8860a] text-white rounded-xl disabled:opacity-50 hover:bg-[#9a6307] transition-all shadow-md active:scale-95"
614
+ >
615
+ <Send size={14} />
616
+ </button>
617
+ </div>
618
+ </form>
619
+ </>
620
+ )}
621
  </motion.div>
622
  </>
623
  )}
webapp/tipitaka-web/src/components/layout/AppShell.tsx CHANGED
@@ -4,6 +4,7 @@ import ReaderPanel from './ReaderPanel';
4
  import RightToolbar from '../toolbar/RightToolbar';
5
  import MobileBottomBar from './MobileBottomBar';
6
  import AIPopup from '../ai/AIPopup';
 
7
  import ErrorBoundary from '../common/ErrorBoundary';
8
  import { Menu } from 'lucide-react';
9
  import { useUIStore, useThemeStore } from '../../stores/appStore';
@@ -27,7 +28,7 @@ const MENU_BTN: Record<string, string> = {
27
 
28
  const AppShell: React.FC = () => {
29
  const { toggleNav, isNavOpen } = useUIStore();
30
- const { toggleOpen, isOpen } = useAIStore();
31
  const { theme } = useThemeStore();
32
 
33
  // Keyboard shortcut: ← → PgUp PgDown Space for page navigation
@@ -93,16 +94,10 @@ const AppShell: React.FC = () => {
93
  {/* Mobile: bottom navigation bar (hidden on desktop) */}
94
  <MobileBottomBar />
95
 
96
- {/* AI FAB raised on mobile to clear bottom bar */}
97
- <button
98
- onClick={toggleOpen}
99
- className={`fixed right-14 bottom-14 lg:bottom-6 z-40 w-14 h-14 rounded-full bg-[#c8860a] text-white flex items-center justify-center text-xl shadow-lg hover:bg-[#9a6307] transition-all duration-200 ${
100
- isOpen ? 'scale-0 opacity-0 pointer-events-none' : 'scale-100 opacity-100'
101
- }`}
102
- title="ผู้ช่วย AI"
103
- >
104
-
105
- </button>
106
 
107
  {/* AI Popup */}
108
  <AnimatePresence>
 
4
  import RightToolbar from '../toolbar/RightToolbar';
5
  import MobileBottomBar from './MobileBottomBar';
6
  import AIPopup from '../ai/AIPopup';
7
+ import AIFloatingMenu from '../ai/AIFloatingMenu';
8
  import ErrorBoundary from '../common/ErrorBoundary';
9
  import { Menu } from 'lucide-react';
10
  import { useUIStore, useThemeStore } from '../../stores/appStore';
 
28
 
29
  const AppShell: React.FC = () => {
30
  const { toggleNav, isNavOpen } = useUIStore();
31
+ const { isOpen } = useAIStore();
32
  const { theme } = useThemeStore();
33
 
34
  // Keyboard shortcut: ← → PgUp PgDown Space for page navigation
 
94
  {/* Mobile: bottom navigation bar (hidden on desktop) */}
95
  <MobileBottomBar />
96
 
97
+ {/* AI Floating Menu */}
98
+ <ErrorBoundary componentName="AIFloatingMenu">
99
+ <AIFloatingMenu />
100
+ </ErrorBoundary>
 
 
 
 
 
 
101
 
102
  {/* AI Popup */}
103
  <AnimatePresence>
webapp/tipitaka-web/src/stores/aiStore.ts CHANGED
@@ -16,11 +16,13 @@ interface AIState {
16
  ragChecking: boolean; // true while checking status
17
  ragLoading: boolean; // true when Qdrant exists but Reranker still loading
18
  dragPos: { x: number; y: number } | null; // saved drag position (desktop)
 
19
  panelWidth: number; // saved panel width (desktop)
20
  panelHeight: number; // saved panel height (desktop)
21
 
22
  // Actions
23
  toggleOpen: () => void;
 
24
  setMode: (mode: 'fast' | 'reasoner') => void;
25
  setUseRag: (use: boolean) => void;
26
  addMessage: (msg: Message) => void;
@@ -44,10 +46,12 @@ export const useAIStore = create<AIState>((set, get) => ({
44
  ragChecking: false,
45
  ragLoading: false,
46
  dragPos: null,
 
47
  panelWidth: 400,
48
  panelHeight: 550,
49
 
50
  toggleOpen: () => set((state) => ({ isOpen: !state.isOpen })),
 
51
  setMode: (mode) => set({ mode }),
52
  setUseRag: (useRag) => set({ useRag }),
53
  addMessage: (msg) => set((state) => ({ messages: [...state.messages, msg] })),
 
16
  ragChecking: boolean; // true while checking status
17
  ragLoading: boolean; // true when Qdrant exists but Reranker still loading
18
  dragPos: { x: number; y: number } | null; // saved drag position (desktop)
19
+ viewMode: 'chat' | 'answer';
20
  panelWidth: number; // saved panel width (desktop)
21
  panelHeight: number; // saved panel height (desktop)
22
 
23
  // Actions
24
  toggleOpen: () => void;
25
+ setViewMode: (mode: 'chat' | 'answer') => void;
26
  setMode: (mode: 'fast' | 'reasoner') => void;
27
  setUseRag: (use: boolean) => void;
28
  addMessage: (msg: Message) => void;
 
46
  ragChecking: false,
47
  ragLoading: false,
48
  dragPos: null,
49
+ viewMode: 'chat',
50
  panelWidth: 400,
51
  panelHeight: 550,
52
 
53
  toggleOpen: () => set((state) => ({ isOpen: !state.isOpen })),
54
+ setViewMode: (viewMode) => set({ viewMode }),
55
  setMode: (mode) => set({ mode }),
56
  setUseRag: (useRag) => set({ useRag }),
57
  addMessage: (msg) => set((state) => ({ messages: [...state.messages, msg] })),