Chandima Prabhath commited on
Commit
978caa8
·
1 Parent(s): ad386aa
frontend/README.md CHANGED
@@ -71,8 +71,3 @@ Yes, you can!
71
  To connect a domain, navigate to Project > Settings > Domains and click Connect Domain.
72
 
73
  Read more here: [Setting up a custom domain](https://docs.lovable.dev/tips-tricks/custom-domain#step-by-step-guide)
74
-
75
-
76
- TODO:
77
-
78
- make it that the think messege is by default colapsed and can be exapnded. implement that in a creative modern way. and make the thinking animation more noticable. also messeges after a certain msg variation are belong to that specific variention so when we switch between varietions other messages that belongs to that variention needs to be changed too. also i had to regenrate to make it the first varietion and then again to second varietion. but thats not how it needs to be. every messege is by default the first varietion. so if we regenerate the message it becomes second varietion.
 
71
  To connect a domain, navigate to Project > Settings > Domains and click Connect Domain.
72
 
73
  Read more here: [Setting up a custom domain](https://docs.lovable.dev/tips-tricks/custom-domain#step-by-step-guide)
 
 
 
 
 
frontend/src/components/chat/ChatBubble.tsx CHANGED
@@ -8,27 +8,10 @@ import { Avatar, AvatarFallback } from "../ui/avatar";
8
  import { ChatMessage } from "./ChatMessage";
9
  import { ThinkingAnimation } from "./ThinkingAnimation";
10
  import { toast } from '../ui/sonner';
11
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
12
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
13
- import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
14
-
15
- interface MessageVariation {
16
- id: string;
17
- content: string;
18
- timestamp: Date;
19
- }
20
-
21
- interface Message {
22
- id: string;
23
- content: string;
24
- sender: "user" | "system";
25
- timestamp: Date;
26
- isLoading?: boolean;
27
- error?: boolean;
28
- result?: any;
29
- variations?: MessageVariation[];
30
- activeVariation?: string;
31
- }
32
 
33
  interface ChatBubbleProps {
34
  message: Message;
@@ -49,9 +32,8 @@ export const ChatBubble = ({
49
  }: ChatBubbleProps) => {
50
  const [copied, setCopied] = useState(false);
51
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
52
- const isWelcomeMessage = message.id === "welcomems"
53
  const isSystem = message.sender === "system";
54
- const showCopyButton = true;
55
  const hasVariations = isSystem && message.variations && message.variations.length > 0;
56
 
57
  // If the message has variations, display the active one or the first one
@@ -60,7 +42,9 @@ export const ChatBubble = ({
60
  : message.content;
61
 
62
  const copyToClipboard = () => {
63
- navigator.clipboard.writeText(displayContent);
 
 
64
  setCopied(true);
65
  toast.success('Copied to clipboard!');
66
  setTimeout(() => setCopied(false), 2000);
@@ -73,43 +57,11 @@ export const ChatBubble = ({
73
  }
74
  };
75
 
76
- const handleSelectVariation = (variationId: string) => {
77
- if (onSelectVariation) {
78
- onSelectVariation(message.id, variationId);
79
- }
80
- };
81
-
82
- // Function to get the current variation index and navigate through variations
83
- const navigateVariations = (direction: 'prev' | 'next') => {
84
- if (!hasVariations || !message.variations || message.variations.length <= 1) return;
85
-
86
- const currentIndex = message.activeVariation
87
- ? message.variations.findIndex(v => v.id === message.activeVariation)
88
- : 0;
89
-
90
- let newIndex;
91
- if (direction === 'prev') {
92
- newIndex = (currentIndex - 1 + message.variations.length) % message.variations.length;
93
- } else {
94
- newIndex = (currentIndex + 1) % message.variations.length;
95
- }
96
-
97
- handleSelectVariation(message.variations[newIndex].id);
98
- };
99
-
100
- // Get current variation index for display
101
- const getCurrentVariationIndex = () => {
102
- if (!hasVariations || !message.variations) return 0;
103
- return message.activeVariation
104
- ? message.variations.findIndex(v => v.id === message.activeVariation) + 1
105
- : 1;
106
- };
107
-
108
  return (
109
  <>
110
  <div
111
  className={cn(
112
- "group flex w-full animate-slide-in mb-4",
113
  isSystem ? "justify-start" : "justify-end"
114
  )}
115
  >
@@ -146,134 +98,46 @@ export const ChatBubble = ({
146
  </div>
147
 
148
  {/* Chat bubble footer */}
149
- <div className="flex flex-row chat-bubble-footer justify-between">
150
  {/* Time */}
151
  <div className={cn(
152
- "text-xs text-muted-foreground mt-1",
153
  isSystem ? "text-left" : "text-right"
154
  )}>
155
  {format(message.timestamp, "h:mm a")}
156
  </div>
157
- {/* Controls */}
158
- {!isWelcomeMessage && <div className="controls flex gap-1">
159
- {/* Variation Navigation */}
160
- {hasVariations && message.variations && message.variations.length > 1 && (
161
- <div className="flex items-center mt-1 opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 rounded-md border">
162
- <Button
163
- variant="ghost"
164
- size="sm"
165
- onClick={() => navigateVariations('prev')}
166
- disabled={message.variations.length <= 1}
167
- className="h-6 px-1"
168
- >
169
- <ChevronLeft className="h-3 w-3" />
170
- </Button>
171
- <span className="text-xs px-1">
172
- {getCurrentVariationIndex()}/{message.variations.length}
173
- </span>
174
- <Button
175
- variant="ghost"
176
- size="sm"
177
- onClick={() => navigateVariations('next')}
178
- disabled={message.variations.length <= 1}
179
- className="h-6 px-1"
180
- >
181
- <ChevronRight className="h-3 w-3" />
182
- </Button>
183
- </div>
184
- )}
185
 
186
- {/* Retry button for failed messages */}
187
- {message.error && onRetry && (
188
- <div className="flex mt-1">
189
- <Button
190
- variant="secondary"
191
- size="sm"
192
- onClick={() => onRetry(message.id)}
193
- className="text-xs flex items-center gap-1.5 text-muted-foreground hover:border border-financial-accent/30 bg-background/50 backdrop-blur-sm"
194
- >
195
- <RefreshCcw className="h-3 w-3" />
196
- Retry
197
- </Button>
198
- </div>
199
- )}
200
-
201
- {/* Regenerate button for system messages */}
202
- {isSystem && !message.isLoading && onRegenerate && (
203
- <TooltipProvider>
204
- <Tooltip>
205
- <TooltipTrigger asChild>
206
- <div className="flex mt-1 opacity-0 group-hover:opacity-100 transition-opacity">
207
- <Button
208
- variant="link"
209
- size="sm"
210
- onClick={() => onRegenerate(message.id)}
211
- >
212
- <RotateCcw className="h-3 w-3" />
213
- </Button>
214
- </div>
215
- </TooltipTrigger>
216
- <TooltipContent>
217
- <p>Generate variation</p>
218
- </TooltipContent>
219
- </Tooltip>
220
- </TooltipProvider>
221
- )}
222
-
223
- {/* Delete button */}
224
- {onDelete && !message.isLoading && (
225
- <TooltipProvider>
226
- <Tooltip>
227
- <TooltipTrigger asChild>
228
- <div className="flex mt-1 opacity-0 group-hover:opacity-100">
229
- <Button
230
- variant="ghost"
231
- size="sm"
232
- onClick={() => setDeleteDialogOpen(true)}
233
- className="text-xs flex items-center gap-1.5 text-muted-foreground hover:text-destructive hover:bg-background/50 backdrop-blur-sm"
234
- >
235
- <Trash2 className="h-3 w-3" />
236
- </Button>
237
- </div>
238
- </TooltipTrigger>
239
- <TooltipContent>
240
- <p>Delete message</p>
241
- </TooltipContent>
242
- </Tooltip>
243
- </TooltipProvider>
244
- )}
245
 
246
- {/* Copy */}
247
- {showCopyButton && !message.isLoading && (
248
- <div className="relative top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
249
- <Button variant="link" size="sm" onClick={copyToClipboard}>
250
- {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
251
- </Button>
252
- </div>
253
- )}
254
- </div>}
 
255
  </div>
256
  </div>
257
  </div>
258
  </div>
259
 
260
- {/* Delete confirmation dialog */}
261
- <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
262
- <AlertDialogContent>
263
- <AlertDialogHeader>
264
- <AlertDialogTitle>Delete Message</AlertDialogTitle>
265
- <AlertDialogDescription>
266
- Are you sure you want to delete this message? This action cannot be undone.
267
- </AlertDialogDescription>
268
- </AlertDialogHeader>
269
- <AlertDialogFooter>
270
- <AlertDialogCancel>Cancel</AlertDialogCancel>
271
- <AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
272
- Delete
273
- </AlertDialogAction>
274
- </AlertDialogFooter>
275
- </AlertDialogContent>
276
- </AlertDialog>
277
  </>
278
  );
279
  };
 
8
  import { ChatMessage } from "./ChatMessage";
9
  import { ThinkingAnimation } from "./ThinkingAnimation";
10
  import { toast } from '../ui/sonner';
11
+ import { MessageActions } from "./MessageActions";
12
+ import { MessageVariationControls } from "./MessageVariationControls";
13
+ import { DeleteMessageDialog } from "./DeleteMessageDialog";
14
+ import { MessageVariation, Message } from "@/types/chat";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  interface ChatBubbleProps {
17
  message: Message;
 
32
  }: ChatBubbleProps) => {
33
  const [copied, setCopied] = useState(false);
34
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
35
+ const isWelcomeMessage = message.id === "welcomems";
36
  const isSystem = message.sender === "system";
 
37
  const hasVariations = isSystem && message.variations && message.variations.length > 0;
38
 
39
  // If the message has variations, display the active one or the first one
 
42
  : message.content;
43
 
44
  const copyToClipboard = () => {
45
+ // Strip thinking content before copying
46
+ const cleanContent = displayContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
47
+ navigator.clipboard.writeText(cleanContent);
48
  setCopied(true);
49
  toast.success('Copied to clipboard!');
50
  setTimeout(() => setCopied(false), 2000);
 
57
  }
58
  };
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  return (
61
  <>
62
  <div
63
  className={cn(
64
+ "group flex w-full animate-slide-in mb-2",
65
  isSystem ? "justify-start" : "justify-end"
66
  )}
67
  >
 
98
  </div>
99
 
100
  {/* Chat bubble footer */}
101
+ <div className="flex flex-row chat-bubble-footer justify-between items-center mt-1">
102
  {/* Time */}
103
  <div className={cn(
104
+ "text-xs text-muted-foreground",
105
  isSystem ? "text-left" : "text-right"
106
  )}>
107
  {format(message.timestamp, "h:mm a")}
108
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ {/* Controls */}
111
+ {!isWelcomeMessage && (
112
+ <div className="controls flex gap-1 items-center">
113
+ {/* Variation controls */}
114
+ {hasVariations && message.variations && message.variations.length > 1 && (
115
+ <MessageVariationControls
116
+ message={message}
117
+ onSelectVariation={onSelectVariation}
118
+ />
119
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ <MessageActions
122
+ message={message}
123
+ onRetry={onRetry}
124
+ onRegenerate={onRegenerate}
125
+ onDelete={() => setDeleteDialogOpen(true)}
126
+ onCopy={copyToClipboard}
127
+ copied={copied}
128
+ />
129
+ </div>
130
+ )}
131
  </div>
132
  </div>
133
  </div>
134
  </div>
135
 
136
+ <DeleteMessageDialog
137
+ isOpen={deleteDialogOpen}
138
+ onOpenChange={setDeleteDialogOpen}
139
+ onDelete={handleDelete}
140
+ />
 
 
 
 
 
 
 
 
 
 
 
 
141
  </>
142
  );
143
  };
frontend/src/components/chat/ChatInterface.tsx CHANGED
@@ -7,7 +7,7 @@ import { ChatBubble } from "@/components/chat/ChatBubble";
7
  import { Separator } from "@/components/ui/separator";
8
  import { cn } from "@/lib/utils";
9
  import { storage, STORAGE_KEYS } from "@/lib/storage";
10
- import { Message, Chat } from "@/types/chat";
11
  import { ProfileModal } from "../modals/ProfileModal";
12
  import { ChatSidebar } from "./ChatSidebar";
13
  import { ChatInputArea } from "./ChatInputArea";
@@ -195,6 +195,28 @@ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfacePr
195
  isLoading: true
196
  };
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  // Update active chat with new messages
199
  const updatedChat = {
200
  ...activeChat,
@@ -207,14 +229,51 @@ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfacePr
207
  setIsLoading(true);
208
 
209
  try {
210
- // Prepare chat history for the API
211
- const chatHistory: APIMessage[] = updatedChat.messages
212
- .filter(msg => !msg.isLoading && msg.content) // filter out loading messages
213
- .slice(0, -1) // exclude the loading message we just added
214
- .map(msg => ({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  role: msg.sender === "user" ? "user" : "assistant",
216
- content: msg.content
217
- }));
 
218
 
219
  const response = await apiService.queryRulings({
220
  query: userMessage.content,
@@ -228,7 +287,9 @@ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfacePr
228
  ...msg,
229
  content: response.answer,
230
  isLoading: false,
231
- result: response.retrieved_sources
 
 
232
  }
233
  : msg
234
  );
@@ -405,9 +466,15 @@ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfacePr
405
  const variationId = generateId();
406
  const now = new Date();
407
 
408
- // Prepare the updated message with a loading variation
409
- const variations = message.variations || [];
410
- const existingVariations = variations.map(v => ({ ...v }));
 
 
 
 
 
 
411
 
412
  // Create a new variations array with the loading state
413
  const updatedVariations = [
@@ -424,23 +491,30 @@ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfacePr
424
  activeVariation: variationId
425
  };
426
 
 
 
 
 
427
  const updatedChat = {
428
  ...activeChat,
429
- messages: updatedMessages
430
  };
431
 
432
  setActiveChat(updatedChat);
433
  setIsLoading(true);
434
 
435
- // Prepare chat history for the API
436
- // Include only the messages up to the user message that triggered the original response
437
  const chatHistory: APIMessage[] = activeChat.messages
438
  .slice(0, userMessageIndex)
439
  .filter(msg => !msg.isLoading && msg.content)
440
- .map(msg => ({
441
- role: msg.sender === "user" ? "user" : "assistant",
442
- content: msg.content
443
- }));
 
 
 
 
444
 
445
  // Send the API request
446
  apiService.queryRulings({
@@ -455,7 +529,7 @@ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfacePr
455
  : v
456
  );
457
 
458
- const finalMessages = [...updatedMessages];
459
  finalMessages[messageIndex] = {
460
  ...finalMessages[messageIndex],
461
  variations: finalVariations,
@@ -515,16 +589,26 @@ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfacePr
515
  const messageIndex = activeChat.messages.findIndex(msg => msg.id === messageId);
516
  if (messageIndex < 0) return;
517
 
518
- // Set the active variation
519
  const updatedMessages = [...activeChat.messages];
520
  updatedMessages[messageIndex] = {
521
  ...updatedMessages[messageIndex],
522
  activeVariation: variationId
523
  };
524
 
 
 
 
 
 
 
 
 
 
 
525
  const updatedChat = {
526
  ...activeChat,
527
- messages: updatedMessages
528
  };
529
 
530
  setActiveChat(updatedChat);
@@ -723,3 +807,5 @@ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfacePr
723
  </>
724
  );
725
  };
 
 
 
7
  import { Separator } from "@/components/ui/separator";
8
  import { cn } from "@/lib/utils";
9
  import { storage, STORAGE_KEYS } from "@/lib/storage";
10
+ import { Message, Chat, MessageVariation } from "@/types/chat";
11
  import { ProfileModal } from "../modals/ProfileModal";
12
  import { ChatSidebar } from "./ChatSidebar";
13
  import { ChatInputArea } from "./ChatInputArea";
 
195
  isLoading: true
196
  };
197
 
198
+ // Find the active variation if we're replying to a message that has variations
199
+ let parentVariationId: string | undefined;
200
+ let parentMessageId: string | undefined;
201
+
202
+ // Find the last system message that might have variations
203
+ const lastMessages = [...activeChat.messages].reverse();
204
+ for (const msg of lastMessages) {
205
+ if (msg.sender === "system" && msg.variations && msg.variations.length > 0 && msg.activeVariation) {
206
+ parentMessageId = msg.id;
207
+ parentVariationId = msg.activeVariation;
208
+ break;
209
+ }
210
+ }
211
+
212
+ // If responding to a variation, link these messages to that variation
213
+ if (parentMessageId && parentVariationId) {
214
+ userMessage.parentMessageId = parentMessageId;
215
+ userMessage.variationId = parentVariationId;
216
+ loadingMessage.parentMessageId = parentMessageId;
217
+ loadingMessage.variationId = parentVariationId;
218
+ }
219
+
220
  // Update active chat with new messages
221
  const updatedChat = {
222
  ...activeChat,
 
229
  setIsLoading(true);
230
 
231
  try {
232
+ // Prepare chat history for the API based on the active variation path
233
+ const chatHistory: APIMessage[] = [];
234
+
235
+ // Build chat history based on the active variation path
236
+ const getMessagesForHistory = (messages: Message[]): Message[] => {
237
+ const result: Message[] = [];
238
+
239
+ for (const msg of messages) {
240
+ if (msg.isLoading) continue;
241
+
242
+ // If this is a system message with variations, use the active variation content
243
+ if (msg.sender === "system" && msg.variations && msg.variations.length > 0 && msg.activeVariation) {
244
+ const activeVar = msg.variations.find(v => v.id === msg.activeVariation);
245
+ if (activeVar) {
246
+ // Add the message with the active variation content
247
+ result.push({
248
+ ...msg,
249
+ content: activeVar.content
250
+ });
251
+ }
252
+ } else {
253
+ // Add regular messages
254
+ result.push(msg);
255
+ }
256
+ }
257
+
258
+ return result;
259
+ };
260
+
261
+ // Get messages following the active variation path
262
+ const historyMessages = getMessagesForHistory(updatedChat.messages.slice(0, -2));
263
+
264
+ // Convert to API format
265
+ for (const msg of historyMessages) {
266
+ if (!msg.content) continue;
267
+
268
+ // Strip thinking content from messages before sending to API
269
+ const cleanedContent = msg.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
270
+ if (!cleanedContent) continue;
271
+
272
+ chatHistory.push({
273
  role: msg.sender === "user" ? "user" : "assistant",
274
+ content: cleanedContent
275
+ });
276
+ }
277
 
278
  const response = await apiService.queryRulings({
279
  query: userMessage.content,
 
287
  ...msg,
288
  content: response.answer,
289
  isLoading: false,
290
+ result: response.retrieved_sources,
291
+ variations: [{ id: loadingMessage.id + "-original", content: response.answer, timestamp: new Date() }],
292
+ activeVariation: loadingMessage.id + "-original"
293
  }
294
  : msg
295
  );
 
466
  const variationId = generateId();
467
  const now = new Date();
468
 
469
+ // Get existing variations or initialize if none exist
470
+ let existingVariations = message.variations || [];
471
+
472
+ // If there are no variations yet, add the original content as the first variation
473
+ if (existingVariations.length === 0) {
474
+ existingVariations = [
475
+ { id: message.id + "-original", content: message.content, timestamp: message.timestamp }
476
+ ];
477
+ }
478
 
479
  // Create a new variations array with the loading state
480
  const updatedVariations = [
 
491
  activeVariation: variationId
492
  };
493
 
494
+ // Find and remove any messages that were children of the previous variation
495
+ // We'll preserve the core tree but remove messages specific to other variations
496
+ const messagesToKeep = updatedMessages.slice(0, messageIndex + 1);
497
+
498
  const updatedChat = {
499
  ...activeChat,
500
+ messages: messagesToKeep
501
  };
502
 
503
  setActiveChat(updatedChat);
504
  setIsLoading(true);
505
 
506
+ // Prepare chat history for the API - strip thinking content
 
507
  const chatHistory: APIMessage[] = activeChat.messages
508
  .slice(0, userMessageIndex)
509
  .filter(msg => !msg.isLoading && msg.content)
510
+ .map(msg => {
511
+ // Strip thinking content from messages
512
+ const cleanedContent = msg.content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
513
+ return {
514
+ role: msg.sender === "user" ? "user" : "assistant",
515
+ content: cleanedContent
516
+ };
517
+ });
518
 
519
  // Send the API request
520
  apiService.queryRulings({
 
529
  : v
530
  );
531
 
532
+ const finalMessages = [...messagesToKeep];
533
  finalMessages[messageIndex] = {
534
  ...finalMessages[messageIndex],
535
  variations: finalVariations,
 
589
  const messageIndex = activeChat.messages.findIndex(msg => msg.id === messageId);
590
  if (messageIndex < 0) return;
591
 
592
+ // Update the active variation for this message
593
  const updatedMessages = [...activeChat.messages];
594
  updatedMessages[messageIndex] = {
595
  ...updatedMessages[messageIndex],
596
  activeVariation: variationId
597
  };
598
 
599
+ // Keep messages up to and including the varied message
600
+ const baseMessages = updatedMessages.slice(0, messageIndex + 1);
601
+
602
+ // Find any existing messages that belong to this variation
603
+ const childMessages = activeChat.messages
604
+ .filter(msg => msg.parentMessageId === messageId && msg.variationId === variationId);
605
+
606
+ // Combine to get the complete message list
607
+ const finalMessages = [...baseMessages, ...childMessages];
608
+
609
  const updatedChat = {
610
  ...activeChat,
611
+ messages: finalMessages
612
  };
613
 
614
  setActiveChat(updatedChat);
 
807
  </>
808
  );
809
  };
810
+
811
+ export default ChatInterface;
frontend/src/components/chat/ChatMessage.tsx CHANGED
@@ -1,11 +1,12 @@
1
 
2
- // src/components/ChatMessage.tsx
3
  import React, { useState, useMemo } from 'react'
4
  import ReactMarkdown from 'react-markdown'
5
  import remarkGfm from 'remark-gfm'
6
  import rehypeRaw from 'rehype-raw'
7
  import { cn } from '@/lib/utils'
8
- import { toast } from '../ui/sonner'
 
9
 
10
  interface ChatMessageProps {
11
  content: string
@@ -16,34 +17,44 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
16
  content,
17
  className,
18
  }) => {
 
 
 
 
19
 
20
- // Process thinking tags
21
- const processedContent = useMemo(() => {
22
- // Replace <think>...</think> tags with a special format
23
- const contentWithProcessedThinking = content.replace(
24
  /<think>([\s\S]*?)<\/think>/g,
25
  (_, thinkContent) => {
26
- return `<div class="think-block">${thinkContent}</div>`;
 
 
 
 
27
  }
28
  );
29
 
30
- // Continue processing source tags as before
31
- return contentWithProcessedThinking.replace(
32
  /<source\s+path=["'](.+?)["']\s*\/>/g,
33
  (_match, path) => {
34
  const filename = path
35
  .split('/')
36
  .pop()!
37
  .replace(/\.[^/.]+$/, '')
38
- // embed your ExternalLink SVG inline so you get the icon
39
  return `<a href="${path}" target="_blank" class="inline-flex items-center text-xs font-medium mx-0.5 rounded-sm px-1 bg-financial-accent/10 text-financial-accent border border-financial-accent/20 hover:bg-financial-accent/20 transition-colors">
40
  ${filename}
41
  <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
42
  <path stroke-linecap="round" stroke-linejoin="round" d="M18 13v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6m0 0v6m0-6L10 14"/>
43
  </svg>
44
- </a>`
45
  }
46
  );
 
 
 
 
 
47
  }, [content]);
48
 
49
  return (
@@ -53,30 +64,64 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
53
  className
54
  )}
55
  >
56
- <ReactMarkdown
57
- remarkPlugins={[remarkGfm]}
58
- rehypePlugins={[rehypeRaw]}
59
- components={{
60
- // style your normal links if you like
61
- a: ({ href, children, node, ...props }) =>
62
- href && href.endsWith('.md') ? (
63
- <a
64
- href={href}
65
- target="_blank"
66
- rel="noopener noreferrer"
67
- {...props}
68
- >
69
- {children}
70
- </a>
71
- ) : (
72
- <a href={href} {...props}>
73
- {children}
74
- </a>
75
- ),
76
- }}
77
- >
78
- {processedContent}
79
- </ReactMarkdown>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
  )
82
  }
 
1
 
2
+ // src/components/chat/ChatMessage.tsx
3
  import React, { useState, useMemo } from 'react'
4
  import ReactMarkdown from 'react-markdown'
5
  import remarkGfm from 'remark-gfm'
6
  import rehypeRaw from 'rehype-raw'
7
  import { cn } from '@/lib/utils'
8
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible'
9
+ import { ChevronDown, Brain } from 'lucide-react'
10
 
11
  interface ChatMessageProps {
12
  content: string
 
17
  content,
18
  className,
19
  }) => {
20
+ // Extract thinking content and actual response
21
+ const { processedContent, thinkingBlocks } = useMemo(() => {
22
+ const blocks: { id: number; content: string }[] = [];
23
+ let thinkBlockCounter = 0;
24
 
25
+ // Extract thinking content between <think> tags
26
+ const contentWithoutThinking = content.replace(
 
 
27
  /<think>([\s\S]*?)<\/think>/g,
28
  (_, thinkContent) => {
29
+ blocks.push({
30
+ id: thinkBlockCounter++,
31
+ content: thinkContent.trim()
32
+ });
33
+ return ''; // Remove thinking content from the main message
34
  }
35
  );
36
 
37
+ // Continue processing source tags
38
+ const processedText = contentWithoutThinking.replace(
39
  /<source\s+path=["'](.+?)["']\s*\/>/g,
40
  (_match, path) => {
41
  const filename = path
42
  .split('/')
43
  .pop()!
44
  .replace(/\.[^/.]+$/, '')
 
45
  return `<a href="${path}" target="_blank" class="inline-flex items-center text-xs font-medium mx-0.5 rounded-sm px-1 bg-financial-accent/10 text-financial-accent border border-financial-accent/20 hover:bg-financial-accent/20 transition-colors">
46
  ${filename}
47
  <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
48
  <path stroke-linecap="round" stroke-linejoin="round" d="M18 13v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6m0 0v6m0-6L10 14"/>
49
  </svg>
50
+ </a>`;
51
  }
52
  );
53
+
54
+ return {
55
+ processedContent: processedText.trim(),
56
+ thinkingBlocks: blocks
57
+ };
58
  }, [content]);
59
 
60
  return (
 
64
  className
65
  )}
66
  >
67
+ {/* First render the thinking blocks if any */}
68
+ {thinkingBlocks.length > 0 && (
69
+ <Collapsible
70
+ className="think-collapsible my-3 rounded-lg bg-financial-accent/5 mb-4"
71
+ defaultOpen={false}
72
+ >
73
+ <CollapsibleTrigger className="flex items-center gap-2 w-full p-2 text-left hover:bg-financial-accent/10 rounded-t-lg">
74
+ <div className="flex items-center gap-2 w-full">
75
+ <div className="thinking-brain-small relative">
76
+ <Brain className="h-4 w-4 text-financial-accent" />
77
+ </div>
78
+ <span className="text-xs font-medium text-financial-accent">Thoughts</span>
79
+ <ChevronDown className="h-4 w-4 text-financial-accent/70 transition-transform duration-200 ml-auto" />
80
+ </div>
81
+ </CollapsibleTrigger>
82
+
83
+ <CollapsibleContent>
84
+ <div className="think-block p-3 text-sm text-muted-foreground bg-financial-accent/5">
85
+ {thinkingBlocks.map((block, index) => (
86
+ <ReactMarkdown
87
+ key={`thinking-${block.id}`}
88
+ remarkPlugins={[remarkGfm]}
89
+ rehypePlugins={[rehypeRaw]}
90
+ >
91
+ {block.content}
92
+ </ReactMarkdown>
93
+ ))}
94
+ </div>
95
+ </CollapsibleContent>
96
+ </Collapsible>
97
+ )}
98
+
99
+ {/* Then render the actual response content */}
100
+ {processedContent && (
101
+ <ReactMarkdown
102
+ remarkPlugins={[remarkGfm]}
103
+ rehypePlugins={[rehypeRaw]}
104
+ components={{
105
+ a: ({ href, children, node, ...props }) =>
106
+ href && href.endsWith('.md') ? (
107
+ <a
108
+ href={href}
109
+ target="_blank"
110
+ rel="noopener noreferrer"
111
+ {...props}
112
+ >
113
+ {children}
114
+ </a>
115
+ ) : (
116
+ <a href={href} {...props}>
117
+ {children}
118
+ </a>
119
+ ),
120
+ }}
121
+ >
122
+ {processedContent}
123
+ </ReactMarkdown>
124
+ )}
125
  </div>
126
  )
127
  }
frontend/src/components/chat/DeleteMessageDialog.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle
12
+ } from "@/components/ui/alert-dialog";
13
+
14
+ interface DeleteMessageDialogProps {
15
+ isOpen: boolean;
16
+ onOpenChange: (open: boolean) => void;
17
+ onDelete: () => void;
18
+ }
19
+
20
+ export const DeleteMessageDialog: React.FC<DeleteMessageDialogProps> = ({
21
+ isOpen,
22
+ onOpenChange,
23
+ onDelete
24
+ }) => {
25
+ return (
26
+ <AlertDialog open={isOpen} onOpenChange={onOpenChange}>
27
+ <AlertDialogContent>
28
+ <AlertDialogHeader>
29
+ <AlertDialogTitle>Delete Message</AlertDialogTitle>
30
+ <AlertDialogDescription>
31
+ Are you sure you want to delete this message? This action cannot be undone.
32
+ </AlertDialogDescription>
33
+ </AlertDialogHeader>
34
+ <AlertDialogFooter>
35
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
36
+ <AlertDialogAction
37
+ onClick={onDelete}
38
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
39
+ >
40
+ Delete
41
+ </AlertDialogAction>
42
+ </AlertDialogFooter>
43
+ </AlertDialogContent>
44
+ </AlertDialog>
45
+ );
46
+ };
frontend/src/components/chat/MessageActions.tsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { RefreshCcw, Copy, Check, Trash2, RotateCcw } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
6
+ import { Message } from "@/types/chat";
7
+
8
+ interface MessageActionsProps {
9
+ message: Message;
10
+ onRetry?: (messageId: string) => void;
11
+ onRegenerate?: (messageId: string) => void;
12
+ onDelete?: () => void;
13
+ onCopy?: () => void;
14
+ copied?: boolean;
15
+ }
16
+
17
+ export const MessageActions: React.FC<MessageActionsProps> = ({
18
+ message,
19
+ onRetry,
20
+ onRegenerate,
21
+ onDelete,
22
+ onCopy,
23
+ copied = false
24
+ }) => {
25
+ const isSystem = message.sender === "system";
26
+
27
+ return (
28
+ <>
29
+ {/* Retry button for failed messages */}
30
+ {message.error && onRetry && (
31
+ <div className="flex">
32
+ <Button
33
+ variant="secondary"
34
+ size="sm"
35
+ onClick={() => onRetry(message.id)}
36
+ className="text-xs flex items-center gap-1.5 text-muted-foreground hover:border border-financial-accent/30 bg-background/50 backdrop-blur-sm"
37
+ >
38
+ <RefreshCcw className="h-3 w-3" />
39
+ Retry
40
+ </Button>
41
+ </div>
42
+ )}
43
+
44
+ {/* Regenerate button for system messages */}
45
+ {isSystem && !message.isLoading && onRegenerate && (
46
+ <TooltipProvider>
47
+ <Tooltip>
48
+ <TooltipTrigger asChild>
49
+ <div className="flex opacity-0 group-hover:opacity-100 transition-opacity">
50
+ <Button
51
+ variant="link"
52
+ size="sm"
53
+ onClick={() => onRegenerate(message.id)}
54
+ >
55
+ <RotateCcw className="h-3 w-3" />
56
+ </Button>
57
+ </div>
58
+ </TooltipTrigger>
59
+ <TooltipContent>
60
+ <p>Generate variation</p>
61
+ </TooltipContent>
62
+ </Tooltip>
63
+ </TooltipProvider>
64
+ )}
65
+
66
+ {/* Delete button */}
67
+ {onDelete && !message.isLoading && (
68
+ <TooltipProvider>
69
+ <Tooltip>
70
+ <TooltipTrigger asChild>
71
+ <div className="flex opacity-0 group-hover:opacity-100">
72
+ <Button
73
+ variant="ghost"
74
+ size="sm"
75
+ onClick={onDelete}
76
+ className="text-xs flex items-center gap-1.5 text-muted-foreground hover:text-destructive hover:bg-background/50 backdrop-blur-sm"
77
+ >
78
+ <Trash2 className="h-3 w-3" />
79
+ </Button>
80
+ </div>
81
+ </TooltipTrigger>
82
+ <TooltipContent>
83
+ <p>Delete message</p>
84
+ </TooltipContent>
85
+ </Tooltip>
86
+ </TooltipProvider>
87
+ )}
88
+
89
+ {/* Copy button */}
90
+ {onCopy && !message.isLoading && (
91
+ <div className="opacity-0 group-hover:opacity-100 transition-opacity">
92
+ <Button variant="link" size="sm" onClick={onCopy}>
93
+ {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
94
+ </Button>
95
+ </div>
96
+ )}
97
+ </>
98
+ );
99
+ };
frontend/src/components/chat/MessageVariationControls.tsx ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { ChevronLeft, ChevronRight } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Message } from "@/types/chat";
6
+
7
+ interface MessageVariationControlsProps {
8
+ message: Message;
9
+ onSelectVariation?: (messageId: string, variationId: string) => void;
10
+ }
11
+
12
+ export const MessageVariationControls: React.FC<MessageVariationControlsProps> = ({
13
+ message,
14
+ onSelectVariation
15
+ }) => {
16
+ if (!message.variations || message.variations.length <= 1 || !onSelectVariation) {
17
+ return null;
18
+ }
19
+
20
+ // Function to get the current variation index and navigate through variations
21
+ const navigateVariations = (direction: 'prev' | 'next') => {
22
+ const currentIndex = message.activeVariation
23
+ ? message.variations!.findIndex(v => v.id === message.activeVariation)
24
+ : 0;
25
+
26
+ let newIndex;
27
+ if (direction === 'prev') {
28
+ newIndex = (currentIndex - 1 + message.variations!.length) % message.variations!.length;
29
+ } else {
30
+ newIndex = (currentIndex + 1) % message.variations!.length;
31
+ }
32
+
33
+ onSelectVariation(message.id, message.variations![newIndex].id);
34
+ };
35
+
36
+ // Get current variation index for display
37
+ const getCurrentVariationIndex = () => {
38
+ return message.activeVariation
39
+ ? message.variations!.findIndex(v => v.id === message.activeVariation) + 1
40
+ : 1;
41
+ };
42
+
43
+ return (
44
+ <div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity bg-background/50 rounded-md border shadow-sm">
45
+ <Button
46
+ variant="ghost"
47
+ size="sm"
48
+ onClick={() => navigateVariations('prev')}
49
+ className="h-6 w-6 p-0 flex items-center justify-center"
50
+ aria-label="Previous variation"
51
+ >
52
+ <ChevronLeft className="h-3 w-3" />
53
+ </Button>
54
+ <span className="text-xs px-1.5 font-medium">
55
+ {getCurrentVariationIndex()}/{message.variations.length}
56
+ </span>
57
+ <Button
58
+ variant="ghost"
59
+ size="sm"
60
+ onClick={() => navigateVariations('next')}
61
+ className="h-6 w-6 p-0 flex items-center justify-center"
62
+ aria-label="Next variation"
63
+ >
64
+ <ChevronRight className="h-3 w-3" />
65
+ </Button>
66
+ </div>
67
+ );
68
+ };
frontend/src/components/chat/ThinkingAnimation.tsx CHANGED
@@ -1,49 +1,36 @@
1
 
 
2
  import { useState, useEffect } from "react";
3
 
4
  export const ThinkingAnimation = () => {
5
  const [dots, setDots] = useState(0);
6
-
7
  useEffect(() => {
8
  const interval = setInterval(() => {
9
  setDots((prev) => (prev + 1) % 4);
10
  }, 400);
11
-
12
  return () => clearInterval(interval);
13
  }, []);
14
-
15
  return (
16
- <div className="flex flex-col items-start gap-2 px-4 py-3">
17
- <div className="thinking-brain">
18
- <svg viewBox="0 0 24 24" width="24" height="24" className="thinking-brain-svg">
19
- <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-2.5 2.5h-5A2.5 2.5 0 0 1 2 19.5v-15A2.5 2.5 0 0 1 4.5 2h5Z"
20
- className="fill-financial-accent/20 stroke-financial-accent stroke-[1.5]" />
21
- <path d="M12 4.5A2.5 2.5 0 0 1 14.5 2h5A2.5 2.5 0 0 1 22 4.5v15a2.5 2.5 0 0 1-2.5 2.5h-5A2.5 2.5 0 0 1 12 19.5v-15Z"
22
- className="fill-financial-accent/10 stroke-financial-accent stroke-[1.5]" />
23
- <path d="M6 12h4" className="stroke-financial-accent stroke-[1.5]" />
24
- <path d="M14 12h4" className="stroke-financial-accent stroke-[1.5]" />
25
- <path d="M13.5 8h1" className="stroke-financial-accent stroke-[1.5]" />
26
- <path d="M16.5 8h1" className="stroke-financial-accent stroke-[1.5]" />
27
- <path d="M13.5 16h1" className="stroke-financial-accent stroke-[1.5]" />
28
- <path d="M16.5 16h1" className="stroke-financial-accent stroke-[1.5]" />
29
- <path d="M9.5 8h1" className="stroke-financial-accent stroke-[1.5]" />
30
- <path d="M6.5 8h1" className="stroke-financial-accent stroke-[1.5]" />
31
- <path d="M9.5 16h1" className="stroke-financial-accent stroke-[1.5]" />
32
- <path d="M6.5 16h1" className="stroke-financial-accent stroke-[1.5]" />
33
- </svg>
34
- <div className="thinking-waves">
35
- <span></span>
36
- <span></span>
37
- <span></span>
38
  </div>
39
  </div>
40
- <span className="text-sm font-medium text-muted-foreground flex items-center">
 
41
  Thinking
42
  <span className="thinking-dots-container ml-2">
43
  {Array(3).fill(0).map((_, i) => (
44
- <span
45
- key={i}
46
- className={`thinking-dot ${i <= dots ? 'thinking-dot-active' : ''}`}
47
  ></span>
48
  ))}
49
  </span>
 
1
 
2
+ import { Brain } from "lucide-react";
3
  import { useState, useEffect } from "react";
4
 
5
  export const ThinkingAnimation = () => {
6
  const [dots, setDots] = useState(0);
7
+
8
  useEffect(() => {
9
  const interval = setInterval(() => {
10
  setDots((prev) => (prev + 1) % 4);
11
  }, 400);
12
+
13
  return () => clearInterval(interval);
14
  }, []);
15
+
16
  return (
17
+ <div className="flex items-center gap-2 px-4 py-1">
18
+ <div className="thinking-brain-svg relative flex items-center justify-center">
19
+ <Brain className="h-4 w-4 text-financial-accent" />
20
+ <div className="thinking-waves-enhanced flex items-center justify-center absolute inset-0">
21
+ <span className="block absolute border-4 border-financial-accent/20 bg-financial-accent/20 rounded-full animate-[ripple_1.5s_ease-out_infinite]"></span>
22
+ <span className="block absolute border-4 border-financial-accent/30 bg-financial-accent/20 rounded-full animate-[ripple_1.5s_ease-out_0.4s_infinite]"></span>
23
+ <span className="block absolute border-4 border-financial-accent/40 bg-financial-accent/20 rounded-full animate-[ripple_1.5s_ease-out_0.8s_infinite]"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
  </div>
26
+
27
+ <span className="text-sm font-medium text-financial-accent flex items-center">
28
  Thinking
29
  <span className="thinking-dots-container ml-2">
30
  {Array(3).fill(0).map((_, i) => (
31
+ <span
32
+ key={i}
33
+ className={`thinking-dot ${i <= dots ? 'thinking-dot-active bg-financial-accent' : 'bg-financial-accent/30'}`}
34
  ></span>
35
  ))}
36
  </span>
frontend/src/index.css CHANGED
@@ -200,7 +200,8 @@
200
  .message-bubble-user {
201
  border-top-right-radius: 4px;
202
  }
203
- /* Thinking animation */
 
204
  .thinking-brain {
205
  position: relative;
206
  width: 32px;
@@ -211,12 +212,20 @@
211
  justify-content: center;
212
  }
213
 
214
- .thinking-brain-svg {
215
- z-index: 10;
216
  position: relative;
 
 
 
 
 
 
 
 
 
217
  }
218
 
219
- .thinking-waves {
220
  position: absolute;
221
  top: 0;
222
  left: 0;
@@ -225,29 +234,30 @@
225
  display: flex;
226
  justify-content: center;
227
  align-items: center;
 
228
  }
229
 
230
- .thinking-waves span {
231
  position: absolute;
232
- border: 1px solid var(--financial-accent, #7C3AED);
233
  border-radius: 50%;
234
  opacity: 0;
235
- animation: thinking-wave 2s ease-out infinite;
236
  }
237
 
238
- .thinking-waves span:nth-child(1) {
239
  width: 100%;
240
  height: 100%;
241
  animation-delay: 0s;
242
  }
243
 
244
- .thinking-waves span:nth-child(2) {
245
  width: 80%;
246
  height: 80%;
247
  animation-delay: 0.3s;
248
  }
249
 
250
- .thinking-waves span:nth-child(3) {
251
  width: 60%;
252
  height: 60%;
253
  animation-delay: 0.6s;
@@ -256,13 +266,13 @@
256
  @keyframes thinking-wave {
257
  0% {
258
  transform: scale(0.5);
259
- opacity: 0.1;
260
  }
261
  50% {
262
- opacity: 0.3;
263
  }
264
  100% {
265
- transform: scale(1.2);
266
  opacity: 0;
267
  }
268
  }
@@ -286,6 +296,27 @@
286
  opacity: 1;
287
  }
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  /* Variations UI */
290
  .variations-container {
291
  margin-top: 0.5rem;
@@ -441,7 +472,10 @@ pre code {
441
  /* Chat bubble styles */
442
  .thinking-dot {
443
  @apply inline-block rounded-full bg-financial-accent;
444
- opacity: 0.7;
 
 
 
445
  }
446
 
447
  @keyframes pulse {
@@ -466,7 +500,7 @@ pre code {
466
 
467
  /* Think tag styling */
468
  .think-block {
469
- @apply bg-stone-50/70 dark:bg-stone-900/50 border-l-4 border-financial-accent/50 p-3 my-2 rounded-md italic text-muted-foreground;
470
  position: relative;
471
  overflow: hidden;
472
  backdrop-filter: blur(6px);
@@ -480,7 +514,6 @@ pre code {
480
  height: 100%;
481
  width: 4px;
482
  background: linear-gradient(to bottom, theme('colors.financial.accent'), theme('colors.financial.light-accent'));
483
- animation: pulse-border 2s infinite alternate;
484
  }
485
 
486
  .think-block::after {
@@ -489,7 +522,7 @@ pre code {
489
  right: 10px;
490
  bottom: 5px;
491
  opacity: 0.2;
492
- font-size: 1.5rem;
493
  }
494
 
495
  /* Message variations styling */
@@ -529,4 +562,4 @@ pre code {
529
  100% {
530
  box-shadow: 0 0 0 0 rgba(var(--accent), 0);
531
  }
532
- }
 
200
  .message-bubble-user {
201
  border-top-right-radius: 4px;
202
  }
203
+
204
+ /* Thinking animation - Enhanced */
205
  .thinking-brain {
206
  position: relative;
207
  width: 32px;
 
212
  justify-content: center;
213
  }
214
 
215
+ .thinking-brain-small {
 
216
  position: relative;
217
+ width: 20px;
218
+ height: 20px;
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: center;
222
+ }
223
+
224
+ .thinking-brain-svg {
225
+ z-index: 2;
226
  }
227
 
228
+ .thinking-waves-enhanced {
229
  position: absolute;
230
  top: 0;
231
  left: 0;
 
234
  display: flex;
235
  justify-content: center;
236
  align-items: center;
237
+ z-index: 1;
238
  }
239
 
240
+ .thinking-waves-enhanced span {
241
  position: absolute;
242
+ border: 1.5px solid var(--financial-accent, #7C3AED);
243
  border-radius: 50%;
244
  opacity: 0;
245
+ animation: thinking-wave 2.5s ease-out infinite;
246
  }
247
 
248
+ .thinking-waves-enhanced span:nth-child(1) {
249
  width: 100%;
250
  height: 100%;
251
  animation-delay: 0s;
252
  }
253
 
254
+ .thinking-waves-enhanced span:nth-child(2) {
255
  width: 80%;
256
  height: 80%;
257
  animation-delay: 0.3s;
258
  }
259
 
260
+ .thinking-waves-enhanced span:nth-child(3) {
261
  width: 60%;
262
  height: 60%;
263
  animation-delay: 0.6s;
 
266
  @keyframes thinking-wave {
267
  0% {
268
  transform: scale(0.5);
269
+ opacity: 0.3;
270
  }
271
  50% {
272
+ opacity: 0.7;
273
  }
274
  100% {
275
+ transform: scale(1.4);
276
  opacity: 0;
277
  }
278
  }
 
296
  opacity: 1;
297
  }
298
 
299
+ /* Collapsible thinking block */
300
+ .think-collapsible {
301
+ @apply bg-financial-accent/5 dark:bg-financial-accent/10 rounded-md overflow-hidden;
302
+ transition: all 0.3s ease;
303
+ border-left: 3px solid var(--financial-accent, #7C3AED);
304
+ background: linear-gradient(to right, rgba(124, 58, 237, 0.05), transparent);
305
+ }
306
+
307
+ .think-collapsible:hover {
308
+ background: linear-gradient(to right, rgba(124, 58, 237, 0.1), rgba(124, 58, 237, 0.02));
309
+ }
310
+
311
+ .think-header {
312
+ @apply px-3 py-2 text-financial-accent;
313
+ }
314
+
315
+ .think-block {
316
+ @apply bg-financial-accent/5 dark:bg-financial-accent/10 p-3 rounded-md text-muted-foreground;
317
+ position: relative;
318
+ }
319
+
320
  /* Variations UI */
321
  .variations-container {
322
  margin-top: 0.5rem;
 
472
  /* Chat bubble styles */
473
  .thinking-dot {
474
  @apply inline-block rounded-full bg-financial-accent;
475
+ }
476
+
477
+ .thinking-dot-active {
478
+ @apply animate-pulse;
479
  }
480
 
481
  @keyframes pulse {
 
500
 
501
  /* Think tag styling */
502
  .think-block {
503
+ @apply bg-stone-50/70 dark:bg-stone-900/50 p-3 my-2 rounded-md italic text-muted-foreground;
504
  position: relative;
505
  overflow: hidden;
506
  backdrop-filter: blur(6px);
 
514
  height: 100%;
515
  width: 4px;
516
  background: linear-gradient(to bottom, theme('colors.financial.accent'), theme('colors.financial.light-accent'));
 
517
  }
518
 
519
  .think-block::after {
 
522
  right: 10px;
523
  bottom: 5px;
524
  opacity: 0.2;
525
+ font-size: 2rem;
526
  }
527
 
528
  /* Message variations styling */
 
562
  100% {
563
  box-shadow: 0 0 0 0 rgba(var(--accent), 0);
564
  }
565
+ }
frontend/src/types/chat.ts CHANGED
@@ -1,4 +1,11 @@
1
 
 
 
 
 
 
 
 
2
  export interface Message {
3
  id: string;
4
  content: string;
@@ -7,8 +14,10 @@ export interface Message {
7
  isLoading?: boolean;
8
  error?: boolean;
9
  result?: any;
10
- variations?: { id: string, content: string, timestamp: Date }[];
11
  activeVariation?: string;
 
 
12
  }
13
 
14
  export interface Chat {
 
1
 
2
+ export interface MessageVariation {
3
+ id: string;
4
+ content: string;
5
+ timestamp: Date;
6
+ childMessages?: Message[]; // Messages that belong to this variation
7
+ }
8
+
9
  export interface Message {
10
  id: string;
11
  content: string;
 
14
  isLoading?: boolean;
15
  error?: boolean;
16
  result?: any;
17
+ variations?: MessageVariation[];
18
  activeVariation?: string;
19
+ parentMessageId?: string; // Link to parent message if this is a child message
20
+ variationId?: string; // Link to the variation it belongs to
21
  }
22
 
23
  export interface Chat {