Chandima Prabhath commited on
Commit
1904e4c
·
1 Parent(s): 22b1735

feat: Add chat components and modals for enhanced user interaction

Browse files

- Implement DeleteChatDialog for confirming chat deletions.
- Create ThinkingAnimation component to indicate processing state.
- Develop WelcomeScreen to greet users and initiate new chats.
- Establish ChatLayout to manage chat interface and settings modals.
- Introduce ProfileModal for user profile management with avatar support.
- Add SettingsModal for configuring application settings and preferences.
- Create SourcesModal for displaying and filtering knowledge sources.
- Enhance apiService for querying and managing data interactions.
- Define chat and message types for structured data handling.

frontend/README.md CHANGED
@@ -71,3 +71,8 @@ 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)
 
 
 
 
 
 
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.
frontend/index.html CHANGED
@@ -1,14 +1,15 @@
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>ruling-insight-navigator</title>
7
- <meta name="description" content="Lovable Generated Project" />
8
  <meta name="author" content="Lovable" />
9
 
10
- <meta property="og:title" content="ruling-insight-navigator" />
11
- <meta property="og:description" content="Lovable Generated Project" />
12
  <meta property="og:type" content="website" />
13
  <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
14
 
 
1
+
2
  <!DOCTYPE html>
3
  <html lang="en">
4
  <head>
5
  <meta charset="UTF-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Insight AI - Chat Assistant</title>
8
+ <meta name="description" content="AI-powered chat assistant with RAG capabilities" />
9
  <meta name="author" content="Lovable" />
10
 
11
+ <meta property="og:title" content="Insight AI" />
12
+ <meta property="og:description" content="AI-powered chat assistant with RAG capabilities" />
13
  <meta property="og:type" content="website" />
14
  <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
15
 
frontend/package.json CHANGED
@@ -12,6 +12,7 @@
12
  },
13
  "dependencies": {
14
  "@hookform/resolvers": "^3.9.0",
 
15
  "@radix-ui/react-accordion": "^1.2.0",
16
  "@radix-ui/react-alert-dialog": "^1.1.1",
17
  "@radix-ui/react-aspect-ratio": "^1.1.0",
 
12
  },
13
  "dependencies": {
14
  "@hookform/resolvers": "^3.9.0",
15
+ "@multiavatar/multiavatar": "^1.0.7",
16
  "@radix-ui/react-accordion": "^1.2.0",
17
  "@radix-ui/react-alert-dialog": "^1.1.1",
18
  "@radix-ui/react-aspect-ratio": "^1.1.0",
frontend/src/App.tsx CHANGED
@@ -5,10 +5,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
5
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
  import { BrowserRouter, Routes, Route } from "react-router-dom";
7
  import { useEffect } from "react";
8
- import MainLayout from "@/components/layout/MainLayout";
9
- import HomePage from "@/pages/HomePage";
10
- import SourcesPage from "@/pages/SourcesPage";
11
- import SettingsPage from "@/pages/SettingsPage";
12
  import NotFound from "@/pages/NotFound";
13
 
14
  const queryClient = new QueryClient({
@@ -23,7 +20,7 @@ const queryClient = new QueryClient({
23
  const App = () => {
24
  // Set the window title
25
  useEffect(() => {
26
- document.title = "Financial Insight System (FIS)";
27
  }, []);
28
 
29
  return (
@@ -33,9 +30,7 @@ const App = () => {
33
  <Sonner />
34
  <BrowserRouter>
35
  <Routes>
36
- <Route path="/" element={<MainLayout><HomePage /></MainLayout>} />
37
- <Route path="/sources" element={<MainLayout><SourcesPage /></MainLayout>} />
38
- <Route path="/settings" element={<MainLayout><SettingsPage /></MainLayout>} />
39
  <Route path="*" element={<NotFound />} />
40
  </Routes>
41
  </BrowserRouter>
 
5
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
  import { BrowserRouter, Routes, Route } from "react-router-dom";
7
  import { useEffect } from "react";
8
+ import ChatLayout from "@/components/layout/ChatLayout";
 
 
 
9
  import NotFound from "@/pages/NotFound";
10
 
11
  const queryClient = new QueryClient({
 
20
  const App = () => {
21
  // Set the window title
22
  useEffect(() => {
23
+ document.title = "Insight AI - Chat Assistant";
24
  }, []);
25
 
26
  return (
 
30
  <Sonner />
31
  <BrowserRouter>
32
  <Routes>
33
+ <Route path="/" element={<ChatLayout />} />
 
 
34
  <Route path="*" element={<NotFound />} />
35
  </Routes>
36
  </BrowserRouter>
frontend/src/components/chat/ChatBubble.tsx CHANGED
@@ -1,12 +1,22 @@
1
 
2
  import { useState } from "react";
3
- import { ArrowRight, RefreshCcw, Copy, Check } from "lucide-react";
4
  import { Button } from "@/components/ui/button";
5
  import { cn } from "@/lib/utils";
6
  import { format } from "date-fns";
7
  import { Avatar, AvatarFallback } from "../ui/avatar";
8
  import { ChatMessage } from "./ChatMessage";
9
- import { toast } from '../ui/sonner'
 
 
 
 
 
 
 
 
 
 
10
 
11
  interface Message {
12
  id: string;
@@ -16,106 +26,254 @@ interface Message {
16
  isLoading?: boolean;
17
  error?: boolean;
18
  result?: any;
 
 
19
  }
20
 
21
  interface ChatBubbleProps {
22
  message: Message;
23
  onViewSearchResults?: (messageId: string) => void;
24
  onRetry?: (messageId: string) => void;
 
 
 
25
  }
26
 
27
-
28
- export const ChatBubble = ({ message, onViewSearchResults, onRetry }: ChatBubbleProps) => {
29
- const [copied, setCopied] = useState(false)
 
 
 
 
 
 
 
 
30
  const isSystem = message.sender === "system";
31
- const showCopyButton = true
 
 
 
 
 
 
 
32
  const copyToClipboard = () => {
33
- navigator.clipboard.writeText(message.content)
34
- setCopied(true)
35
- toast.success('Copied to clipboard!')
36
- setTimeout(() => setCopied(false), 2000)
37
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  return (
40
- <div
41
- className={cn(
42
- "group flex w-full animate-slide-in mb-4",
43
- isSystem ? "justify-start" : "justify-end"
44
- )}
45
- >
46
- <div className={cn(
47
- "flex gap-3 max-w-[80%]",
48
- isSystem ? "flex-row" : "flex-row-reverse"
49
- )}>
50
- <Avatar className={cn(
51
- "h-8 w-8 border",
52
- isSystem
53
- ? "bg-financial-accent text-white shadow-lg"
54
- : "bg-muted"
55
  )}>
56
- <AvatarFallback className="text-xs font-semibold">
57
- {isSystem ? "AI" : "You"}
58
- </AvatarFallback>
59
- </Avatar>
60
-
61
- <div className="flex flex-col">
62
- <div className={cn(
63
- "rounded-2xl shadow-lg message-bubble backdrop-blur-sm",
64
  isSystem
65
- ? "bg-white/90 dark:bg-card/90 border border-border text-foreground message-bubble-ai"
66
- : "bg-financial-accent/30 border border-financial-accent/30 text-white message-bubble-user",
67
- message.error && "border-destructive dark:border-red-500"
68
  )}>
69
- {message.isLoading ? (
70
- <div className="typing-indicator flex items-center space-x-1 px-2">
71
- <span></span>
72
- <span></span>
73
- <span></span>
74
- </div>
75
- ) : (
76
- <ChatMessage
77
- content={message.content}
78
- />
79
- )}
80
 
81
- </div>
82
- {/* Chat bubble footer */}
83
- <div className="flex flex-row chat-bubble-footer justify-between">
84
- {/* Time */}
85
  <div className={cn(
86
- "text-xs text-muted-foreground mt-1",
87
- isSystem ? "text-left" : "text-right"
 
 
 
88
  )}>
89
- {format(message.timestamp, "h:mm a")}
90
- </div>
91
- {/* Controls */}
92
- <div className="controls flex ">
93
- {/* Retry button for failed messages */}
94
- {message.error && onRetry && (
95
- <div className="flex justify-end mt-1">
96
- <Button
97
- variant="secondary"
98
- size="sm"
99
- onClick={() => onRetry(message.id)}
100
- className="text-xs flex items-center gap-1.5 text-muted-foreground hover:border border-financial-accent/30 bg-background/50 backdrop-blur-sm"
101
- >
102
- <RefreshCcw className="h-3 w-3" />
103
- Retry
104
- </Button>
105
- </div>
106
- )}
107
- {/* Copy */}
108
- {showCopyButton && (
109
- <div className="relative top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
110
- <Button variant="link" size="sm" onClick={copyToClipboard}>
111
- {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
112
- </Button>
113
- </div>
114
  )}
115
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  </div>
117
  </div>
118
  </div>
119
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  );
121
  };
 
1
 
2
  import { useState } from "react";
3
+ import { ArrowRight, RefreshCcw, Copy, Check, Trash2, RotateCcw, ListFilter, ChevronLeft, ChevronRight } from "lucide-react";
4
  import { Button } from "@/components/ui/button";
5
  import { cn } from "@/lib/utils";
6
  import { format } from "date-fns";
7
  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;
 
26
  isLoading?: boolean;
27
  error?: boolean;
28
  result?: any;
29
+ variations?: MessageVariation[];
30
+ activeVariation?: string;
31
  }
32
 
33
  interface ChatBubbleProps {
34
  message: Message;
35
  onViewSearchResults?: (messageId: string) => void;
36
  onRetry?: (messageId: string) => void;
37
+ onRegenerate?: (messageId: string) => void;
38
+ onDelete?: (messageId: string) => void;
39
+ onSelectVariation?: (messageId: string, variationId: string) => void;
40
  }
41
 
42
+ export const ChatBubble = ({
43
+ message,
44
+ onViewSearchResults,
45
+ onRetry,
46
+ onRegenerate,
47
+ onDelete,
48
+ onSelectVariation
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
58
+ const displayContent = isSystem && hasVariations && message.activeVariation
59
+ ? message.variations.find(v => v.id === message.activeVariation)?.content || message.content
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);
67
+ };
68
+
69
+ const handleDelete = () => {
70
+ setDeleteDialogOpen(false);
71
+ if (onDelete) {
72
+ onDelete(message.id);
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
+ >
116
+ <div className={cn(
117
+ "flex gap-3 max-w-[80%]",
118
+ isSystem ? "flex-row" : "flex-row-reverse"
 
 
 
 
 
119
  )}>
120
+ <Avatar className={cn(
121
+ "h-8 w-8 border",
 
 
 
 
 
 
122
  isSystem
123
+ ? "bg-financial-accent dark:text-white shadow-lg"
124
+ : "bg-muted"
 
125
  )}>
126
+ <AvatarFallback className="text-xs font-semibold">
127
+ {isSystem ? "AI" : "You"}
128
+ </AvatarFallback>
129
+ </Avatar>
 
 
 
 
 
 
 
130
 
131
+ <div className="flex flex-col">
 
 
 
132
  <div className={cn(
133
+ "rounded-2xl shadow-lg message-bubble backdrop-blur-sm",
134
+ isSystem
135
+ ? "bg-white/90 dark:bg-card/90 border border-border text-foreground message-bubble-ai"
136
+ : "bg-financial-accent/30 border border-financial-accent/30 dark:text-white message-bubble-user",
137
+ message.error && "border-destructive dark:border-red-500"
138
  )}>
139
+ {message.isLoading ? (
140
+ <ThinkingAnimation />
141
+ ) : (
142
+ <ChatMessage
143
+ content={displayContent}
144
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  )}
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
  };
frontend/src/components/chat/ChatInputArea.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { RefObject } from "react";
3
+ import { CornerDownLeft } from "lucide-react";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Button } from "@/components/ui/button";
6
+
7
+ interface ChatInputAreaProps {
8
+ inputRef: RefObject<HTMLInputElement>;
9
+ inputValue: string;
10
+ setInputValue: (value: string) => void;
11
+ handleSendMessage: (e?: React.FormEvent) => void;
12
+ isLoading: boolean;
13
+ }
14
+
15
+ export const ChatInputArea: React.FC<ChatInputAreaProps> = ({
16
+ inputRef,
17
+ inputValue,
18
+ setInputValue,
19
+ handleSendMessage,
20
+ isLoading
21
+ }) => {
22
+ return (
23
+ <div className="absolute bottom-0 left-0 right-0 p-4 bg-background/80 dark:bg-background/50 backdrop-blur-md border-t border-border/30">
24
+ <div className="max-w-3xl mx-auto">
25
+ <form onSubmit={handleSendMessage} className="relative">
26
+ <div className="relative">
27
+ <Input
28
+ ref={inputRef}
29
+ type="text"
30
+ placeholder="Message Insight AI..."
31
+ value={inputValue}
32
+ onChange={(e) => setInputValue(e.target.value)}
33
+ className="pr-20 py-6 text-base bg-background/50 border border-border/50 focus-visible:ring-1 focus-visible:ring-primary/50 rounded-xl"
34
+ disabled={isLoading}
35
+ />
36
+
37
+ <div className="absolute right-2 top-1/2 -translate-y-1/2">
38
+ <Button
39
+ type="submit"
40
+ size="icon"
41
+ className="rounded-lg"
42
+ disabled={isLoading || !inputValue.trim()}
43
+ >
44
+ {isLoading ? (
45
+ <div className="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
46
+ ) : (
47
+ <CornerDownLeft className="h-4 w-4" />
48
+ )}
49
+ </Button>
50
+ </div>
51
+ </div>
52
+
53
+ <div className="mt-2 text-center text-xs text-muted-foreground">
54
+ Insight AI may produce inaccurate information. Verify important information.
55
+ </div>
56
+ </form>
57
+ </div>
58
+ </div>
59
+ );
60
+ };
frontend/src/{pages/HomePage.tsx → components/chat/ChatInterface.tsx} RENAMED
@@ -1,48 +1,29 @@
1
-
2
  import { useState, useRef, useEffect } from "react";
3
- import { useNavigate } from "react-router-dom";
4
- import { Send, Plus, CornerDownLeft, TrashIcon, RefreshCcw, Bot, Sparkles, Menu } from "lucide-react";
5
  import { Button } from "@/components/ui/button";
6
- import { Input } from "@/components/ui/input";
7
- import { rulingService, Message as APIMessage } from "@/services/rulingService";
8
  import { toast } from "@/components/ui/sonner";
9
  import { ChatBubble } from "@/components/chat/ChatBubble";
10
  import { Separator } from "@/components/ui/separator";
11
  import { cn } from "@/lib/utils";
12
  import { storage, STORAGE_KEYS } from "@/lib/storage";
13
- import { format } from "date-fns";
14
- import { ModeToggle } from "@/components/layout/ModeToggle";
15
- import { Link } from "react-router-dom";
16
- import { FileText, MessageCircle, Settings, PanelLeft } from "lucide-react";
17
-
18
- interface Message {
19
- id: string;
20
- content: string;
21
- sender: "user" | "system";
22
- timestamp: Date;
23
- isLoading?: boolean;
24
- error?: boolean;
25
- result?: any;
26
- }
27
-
28
- interface Chat {
29
- id: string;
30
- title: string;
31
- messages: Message[];
32
- createdAt: Date;
33
- updatedAt: Date;
34
  }
35
 
36
- const WELCOME_MESSAGE = "Hello! I'm Insight AI. How can I help you today?";
37
 
38
  const generateId = () => Math.random().toString(36).substring(2, 11);
39
 
40
- const navItems = [
41
- { name: "Conversations", path: "/", icon: MessageCircle },
42
- { name: "Sources", path: "/sources", icon: FileText }
43
- ];
44
-
45
- const HomePage = () => {
46
  const [chats, setChats] = useState<Chat[]>(() => {
47
  const savedChats = storage.get<Chat[]>(STORAGE_KEYS.CHATS);
48
  if (savedChats) {
@@ -69,32 +50,15 @@ const HomePage = () => {
69
  if (chats.length > 0) {
70
  return chats[0];
71
  }
72
-
73
- // Create initial chat if none exists
74
- const initialChat: Chat = {
75
- id: generateId(),
76
- title: "New Chat",
77
- messages: [
78
- {
79
- id: generateId(),
80
- content: WELCOME_MESSAGE,
81
- sender: "system" as const,
82
- timestamp: new Date()
83
- }
84
- ],
85
- createdAt: new Date(),
86
- updatedAt: new Date()
87
- };
88
-
89
- return initialChat;
90
  });
91
 
92
  const [inputValue, setInputValue] = useState("");
93
  const [isLoading, setIsLoading] = useState(false);
94
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
95
  const [isGeneratingTitle, setIsGeneratingTitle] = useState(false);
 
 
96
 
97
- const navigate = useNavigate();
98
  const messagesEndRef = useRef<HTMLDivElement>(null);
99
  const inputRef = useRef<HTMLInputElement>(null);
100
 
@@ -112,9 +76,6 @@ const HomePage = () => {
112
  : chats;
113
 
114
  storage.set(STORAGE_KEYS.CHATS, allChats);
115
-
116
- // Dispatch a custom event to notify storage updates
117
- window.dispatchEvent(new Event("storage-updated"));
118
  }, [chats, activeChat]);
119
 
120
  const scrollToBottom = () => {
@@ -134,63 +95,21 @@ const HomePage = () => {
134
  }
135
  }, [isLoading]);
136
 
137
- // Event listeners for custom events
138
  useEffect(() => {
139
- const handleNewChat = () => {
140
- const newChat: Chat = {
141
- id: generateId(),
142
- title: "New Chat",
143
- messages: [
144
- {
145
- id: generateId(),
146
- content: WELCOME_MESSAGE,
147
- sender: "system" as const,
148
- timestamp: new Date()
149
- }
150
- ],
151
- createdAt: new Date(),
152
- updatedAt: new Date()
153
- };
154
-
155
- setActiveChat(newChat);
156
- setChats(prev => [newChat, ...prev]);
157
- setIsSidebarOpen(false);
158
- setTimeout(() => {
159
- inputRef.current?.focus();
160
- }, 100);
161
- };
162
-
163
  const handleSelectChat = (e: Event) => {
164
  const customEvent = e as CustomEvent;
165
  const chatId = customEvent.detail?.chatId;
166
  if (chatId) {
167
- const selectedChat = chats.find(chat => chat.id === chatId);
168
- if (selectedChat) {
169
- setActiveChat(selectedChat);
170
- setIsSidebarOpen(false);
171
- setTimeout(() => {
172
- inputRef.current?.focus();
173
- }, 100);
174
- }
175
  }
176
  };
177
-
178
  const handleDeleteChat = (e: Event) => {
179
  const customEvent = e as CustomEvent;
180
  const chatId = customEvent.detail?.chatId;
181
  if (chatId) {
182
- const updatedChats = chats.filter(chat => chat.id !== chatId);
183
- setChats(updatedChats);
184
-
185
- // If we're deleting the active chat, switch to another one
186
- if (activeChat?.id === chatId) {
187
- setActiveChat(updatedChats.length > 0 ? updatedChats[0] : null);
188
-
189
- // If no chats left, create a new one
190
- if (updatedChats.length === 0) {
191
- handleNewChat();
192
- }
193
- }
194
  }
195
  };
196
 
@@ -206,25 +125,19 @@ const HomePage = () => {
206
  }, [chats, activeChat]);
207
 
208
  const generateChatTitle = async (query: string) => {
209
- if (!activeChat) return;
210
-
211
- // Only generate titles for new chats
212
- if (activeChat.title !== "New Chat") return;
213
 
214
  setIsGeneratingTitle(true);
215
 
216
  try {
217
- const response = await rulingService.generateTitle(query);
218
 
219
  if (response.title) {
220
- // Update the existing chat object directly
221
  setActiveChat(prevChat => {
222
  if (!prevChat) return null;
223
 
224
- const updatedChat = {
225
- ...prevChat,
226
- title: response.title
227
- };
228
 
229
  // Update chats list
230
  setChats(prevChats =>
@@ -238,7 +151,7 @@ const HomePage = () => {
238
  }
239
  } catch (error) {
240
  console.error("Error generating chat title:", error);
241
- // Fallback to using query as title if title generation fails
242
  if (activeChat.title === "New Chat") {
243
  setActiveChat(prevChat => {
244
  if (!prevChat) return null;
@@ -248,7 +161,6 @@ const HomePage = () => {
248
  title: query.slice(0, 30) + (query.length > 30 ? '...' : '')
249
  };
250
 
251
- // Update chats list
252
  setChats(prevChats =>
253
  prevChats.map(chat =>
254
  chat.id === updatedChat.id ? updatedChat : chat
@@ -297,14 +209,14 @@ const HomePage = () => {
297
  try {
298
  // Prepare chat history for the API
299
  const chatHistory: APIMessage[] = updatedChat.messages
300
- .filter(msg => !msg.isLoading && msg.content) // filter out loading messages and empty messages
301
  .slice(0, -1) // exclude the loading message we just added
302
  .map(msg => ({
303
  role: msg.sender === "user" ? "user" : "assistant",
304
  content: msg.content
305
  }));
306
 
307
- const response = await rulingService.queryRulings({
308
  query: userMessage.content,
309
  chat_history: chatHistory
310
  });
@@ -341,7 +253,7 @@ const HomePage = () => {
341
  }
342
 
343
  } catch (error) {
344
- console.error("Error querying rulings:", error);
345
 
346
  // Replace loading message with error
347
  const updatedMessages = updatedChat.messages.map(msg =>
@@ -370,20 +282,13 @@ const HomePage = () => {
370
  }
371
  };
372
 
373
- const handleViewSearchResults = (messageId: string) => {
374
- const message = activeChat?.messages.find(msg => msg.id === messageId);
375
- if (message && message.content) {
376
- navigate(`/sources`);
377
- }
378
- };
379
-
380
- const handleNewChat = () => {
381
  const newChat: Chat = {
382
  id: generateId(),
383
  title: "New Chat",
384
  messages: [
385
  {
386
- id: generateId(),
387
  content: WELCOME_MESSAGE,
388
  sender: "system" as const,
389
  timestamp: new Date()
@@ -395,11 +300,13 @@ const HomePage = () => {
395
 
396
  setActiveChat(newChat);
397
  setChats(prev => [newChat, ...prev]);
398
- inputRef.current?.focus();
 
 
399
  setIsSidebarOpen(false);
400
  };
401
 
402
- const handleSelectChat = (chatId: string) => {
403
  const selectedChat = chats.find(chat => chat.id === chatId);
404
  if (selectedChat) {
405
  setActiveChat(selectedChat);
@@ -410,9 +317,7 @@ const HomePage = () => {
410
  }
411
  };
412
 
413
- const handleDeleteChat = (chatId: string, e: React.MouseEvent) => {
414
- e.stopPropagation();
415
-
416
  const updatedChats = chats.filter(chat => chat.id !== chatId);
417
  setChats(updatedChats);
418
 
@@ -422,9 +327,214 @@ const HomePage = () => {
422
 
423
  // If no chats left, create a new one
424
  if (updatedChats.length === 0) {
425
- handleNewChat();
426
  }
427
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  };
429
 
430
  const handleRetryMessage = (messageId: string) => {
@@ -474,7 +584,7 @@ const HomePage = () => {
474
  }));
475
 
476
  // Retry the query
477
- rulingService.queryRulings({
478
  query: userMessageContent,
479
  chat_history: chatHistory
480
  })
@@ -494,8 +604,6 @@ const HomePage = () => {
494
  };
495
 
496
  setActiveChat(finalChat);
497
-
498
- // Update chats list
499
  setChats(prevChats =>
500
  prevChats.map(chat =>
501
  chat.id === finalChat.id ? finalChat : chat
@@ -528,217 +636,90 @@ const HomePage = () => {
528
  });
529
  };
530
 
531
- const toggleSidebar = () => {
532
- setIsSidebarOpen(!isSidebarOpen);
533
  };
534
 
535
  return (
536
- <div className="flex h-full">
537
- {/* Chat Sidebar */}
538
- <div className={cn(
539
- "fixed top-0 bottom-0 left-0 z-20 bg-background/90 backdrop-blur-lg",
540
- "transition-transform duration-300 ease-in-out",
541
- isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
542
- "w-72 lg:w-80 border-r border-border/50 flex-shrink-0",
543
- "md:relative md:inset-auto h-full md:z-0"
544
- )}>
545
- <div className="flex flex-col h-full">
546
- <div className="p-4 pb-0 border-b border-border/50">
547
- <div className="flex pb-4 justify-between items-center gap-3">
548
- <Link to="/" className="flex items-center space-x-2">
549
- <div className="h-8 w-8 bg-financial-accent rounded-md flex items-center justify-center">
550
- <span className="text-white font-bold text-lg">AI</span>
551
- </div>
552
- <h1 className="text-xl font-semibold text-financial-navy dark:text-white">
553
- Insight AI
554
- </h1>
555
- </Link>
556
- <Button className="md:hidden" variant="ghost" size="icon" onClick={toggleSidebar}>
557
- <PanelLeft className="h-5 w-5" />
558
- </Button>
559
- </div>
560
- <Separator />
561
- {/* navigation */}
562
- <div className="flex flex-row gap-2 mb-4">
563
- {navItems.map((item) => (
564
- <Link
565
- key={item.name}
566
- to={item.path}
567
- className={cn(
568
- "flex items-center gap-2 p-2 rounded-md text-sm",
569
- item.path === location.pathname
570
- ? "bg-financial-accent/30 border border-financial-accent/30"
571
- : "hover:bg-muted text-muted-foreground"
572
- )}
573
- onClick={() => setIsSidebarOpen(false)}
574
- >
575
- <item.icon className="h-4 w-4" />
576
- <span>{item.name}</span>
577
- </Link>
578
- ))}
579
- </div>
580
- <div className="flex items-center justify-between">
581
- <div className="flex items-center gap-2">
582
- <Bot className="h-5 w-5 text-financial-accent" />
583
- <span className="font-semibold">Recent Chats</span>
584
- </div>
585
- <Button
586
- onClick={handleNewChat}
587
- variant="ghost"
588
- className="h-8 w-8 p-0 hover:bg-financial-accent/10 hover:text-financial-accent"
589
- >
590
- <Plus className="h-4 w-4" />
591
- </Button>
592
  </div>
 
 
 
593
  </div>
594
 
595
- <div className="flex-1 overflow-y-auto p-1 space-y-2 scrollbar-thin">
596
- {chats.length === 0 ? (
597
- <div className="text-center text-muted-foreground p-4">
598
- No conversations yet. Start a new one!
599
- </div>
600
  ) : (
601
- chats.map(chat => (
602
- <div
603
- key={chat.id}
604
- onClick={() => handleSelectChat(chat.id)}
605
- className={cn(
606
- "flex items-center justify-between p-1 px-4 rounded-lg cursor-pointer group transition-all",
607
- activeChat?.id === chat.id
608
- ? "bg-financial-accent/20 border border-financial-accent/30"
609
- : "hover:bg-muted/50 border border-transparent"
610
- )}
611
- >
612
- <div className="flex-1 truncate">
613
- <div className={cn(
614
- "font-medium truncate flex items-center",
615
- activeChat?.id === chat.id && "text-financial-accent"
616
- )}>
617
- <Bot className="h-3.5 w-3.5 mr-1.5 opacity-70" />
618
- {chat.title}
619
- {chat.id === activeChat?.id && isGeneratingTitle && (
620
- <span className="ml-1.5 inline-block h-2 w-2 rounded-full bg-financial-accent/70 animate-pulse"></span>
621
- )}
622
- </div>
623
- <div className="text-xs text-muted-foreground">
624
- {chat.messages.filter(m => m.sender === "user").length} messages • {format(new Date(chat.updatedAt), "MMM d")}
625
- </div>
626
  </div>
627
- <Button
628
- variant="ghost"
629
- size="icon"
630
- className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-destructive/10 hover:text-destructive"
631
- onClick={(e) => handleDeleteChat(chat.id, e)}
632
- >
633
- <TrashIcon className="h-3.5 w-3.5" />
634
- </Button>
635
  </div>
636
- ))
637
- )}
638
- </div>
639
-
640
- {/* Sidebar Footer */}
641
- <div className="p-3 border-t mt-auto flex justify-between items-center">
642
- <Link to="/settings" className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-1.5">
643
- <Settings className="h-3.5 w-3.5" />
644
- <span>Settings</span>
645
- </Link>
646
-
647
- <ModeToggle />
648
- </div>
649
- </div>
650
- </div>
651
 
652
- {/* Chat Main Area */}
653
- <div className="flex-1 flex flex-col overflow-hidden">
654
- {/* Mobile Header */}
655
- <div className="md:hidden p-2 flex items-center border-b">
656
- <Button variant="ghost" size="icon" onClick={toggleSidebar}>
657
- <PanelLeft className="h-5 w-5" />
658
- </Button>
659
- <div className="mx-auto font-medium flex items-center">
660
- <Bot className="h-4 w-4 mr-1.5" />
661
- {activeChat?.title || "New Chat"}
662
  </div>
663
- <Button variant="ghost" size="icon" onClick={handleNewChat}>
664
- <Plus className="h-5 w-5" />
665
- </Button>
666
- </div>
667
-
668
- {/* Chat Area */}
669
- <div className="relative flex-1 overflow-hidden">
670
- {!activeChat ? (
671
- <div className="h-full flex flex-col items-center justify-center">
672
- <div className="text-center max-w-md p-8 bg-card/40 backdrop-blur-sm rounded-2xl border border-border/50">
673
- <Bot className="h-10 w-10 mx-auto mb-4 text-financial-accent" />
674
- <h2 className="text-2xl font-bold mb-2 bg-gradient-to-br from-financial-accent to-financial-light-accent bg-clip-text text-transparent">Welcome to Insight AI</h2>
675
- <p className="text-muted-foreground mb-6">
676
- Ask me anything, and I'll do my best to help you
677
- </p>
678
- <Button onClick={handleNewChat} className="animate-bounce-in bg-financial-accent hover:bg-financial-accent/90">
679
- Start a new conversation
680
- </Button>
681
- </div>
682
- </div>
683
- ) : (
684
- <>
685
- <div className="h-full overflow-y-auto px-4 pb-32 pt-4">
686
- <div className="max-w-3xl mx-auto space-y-4">
687
- {activeChat.messages.map((message) => (
688
- <ChatBubble
689
- key={message.id}
690
- message={message}
691
- onViewSearchResults={handleViewSearchResults}
692
- onRetry={handleRetryMessage}
693
- />
694
- ))}
695
- <div ref={messagesEndRef} />
696
- </div>
697
- </div>
698
-
699
- {/* Input Area */}
700
- <div className="absolute bottom-0 left-0 right-0 p-4 bg-background/80 dark:bg-background/50 backdrop-blur-md border-t border-border/30">
701
- <div className="max-w-3xl mx-auto">
702
- <form onSubmit={handleSendMessage} className="relative">
703
- <div className="relative">
704
- <Input
705
- ref={inputRef}
706
- type="text"
707
- placeholder="Ask me anything..."
708
- value={inputValue}
709
- onChange={(e) => setInputValue(e.target.value)}
710
- className="pr-20 py-6 text-base bg-background/50 border border-border/50 focus:border-financial-accent/50 focus-visible:ring-1 focus-visible:ring-financial-accent/50 rounded-xl"
711
- disabled={isLoading}
712
- />
713
-
714
- <div className="absolute right-2 top-1/2 -translate-y-1/2">
715
- <Button
716
- type="submit"
717
- size="icon"
718
- className="rounded-lg bg-financial-accent hover:bg-financial-accent/90"
719
- disabled={isLoading || !inputValue.trim()}
720
- >
721
- {isLoading ? (
722
- <div className="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
723
- ) : (
724
- <CornerDownLeft className="h-4 w-4" />
725
- )}
726
- </Button>
727
- </div>
728
- </div>
729
-
730
- <div className="mt-2 text-center text-xs text-muted-foreground">
731
- Insight AI may produce inaccurate information. Verify important details.
732
- </div>
733
- </form>
734
- </div>
735
- </div>
736
- </>
737
- )}
738
  </div>
739
  </div>
740
- </div>
 
 
 
 
 
 
 
 
 
 
741
  );
742
  };
743
-
744
- export default HomePage;
 
 
1
  import { useState, useRef, useEffect } from "react";
2
+ import { Send, Plus, PanelLeft, Bot } from "lucide-react";
 
3
  import { Button } from "@/components/ui/button";
4
+ import { apiService, Message as APIMessage } from "@/services/apiService";
 
5
  import { toast } from "@/components/ui/sonner";
6
  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";
14
+ import { WelcomeScreen } from "./WelcomeScreen";
15
+ import { DeleteChatDialog } from "./DeleteChatDialog";
16
+
17
+ interface ChatInterfaceProps {
18
+ onOpenSettings: () => void;
19
+ onOpenSources: () => void;
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
+ const WELCOME_MESSAGE = "Hello! I'm Insight AI, How can I help you today?";
23
 
24
  const generateId = () => Math.random().toString(36).substring(2, 11);
25
 
26
+ export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfaceProps) => {
 
 
 
 
 
27
  const [chats, setChats] = useState<Chat[]>(() => {
28
  const savedChats = storage.get<Chat[]>(STORAGE_KEYS.CHATS);
29
  if (savedChats) {
 
50
  if (chats.length > 0) {
51
  return chats[0];
52
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  });
54
 
55
  const [inputValue, setInputValue] = useState("");
56
  const [isLoading, setIsLoading] = useState(false);
57
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
58
  const [isGeneratingTitle, setIsGeneratingTitle] = useState(false);
59
+ const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
60
+ const [chatToDelete, setChatToDelete] = useState<string | null>(null);
61
 
 
62
  const messagesEndRef = useRef<HTMLDivElement>(null);
63
  const inputRef = useRef<HTMLInputElement>(null);
64
 
 
76
  : chats;
77
 
78
  storage.set(STORAGE_KEYS.CHATS, allChats);
 
 
 
79
  }, [chats, activeChat]);
80
 
81
  const scrollToBottom = () => {
 
95
  }
96
  }, [isLoading]);
97
 
98
+ // Handle new chat and chat selection events
99
  useEffect(() => {
100
+ const handleNewChat = () => createNewChat();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  const handleSelectChat = (e: Event) => {
102
  const customEvent = e as CustomEvent;
103
  const chatId = customEvent.detail?.chatId;
104
  if (chatId) {
105
+ selectChat(chatId);
 
 
 
 
 
 
 
106
  }
107
  };
 
108
  const handleDeleteChat = (e: Event) => {
109
  const customEvent = e as CustomEvent;
110
  const chatId = customEvent.detail?.chatId;
111
  if (chatId) {
112
+ setChatToDelete(chatId);
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
  };
115
 
 
125
  }, [chats, activeChat]);
126
 
127
  const generateChatTitle = async (query: string) => {
128
+ if (!activeChat || activeChat.title !== "New Chat") return;
 
 
 
129
 
130
  setIsGeneratingTitle(true);
131
 
132
  try {
133
+ const response = await apiService.generateTitle(query);
134
 
135
  if (response.title) {
136
+ // Update the active chat with the new title
137
  setActiveChat(prevChat => {
138
  if (!prevChat) return null;
139
 
140
+ const updatedChat = { ...prevChat, title: response.title };
 
 
 
141
 
142
  // Update chats list
143
  setChats(prevChats =>
 
151
  }
152
  } catch (error) {
153
  console.error("Error generating chat title:", error);
154
+ // Fallback to using query as title
155
  if (activeChat.title === "New Chat") {
156
  setActiveChat(prevChat => {
157
  if (!prevChat) return null;
 
161
  title: query.slice(0, 30) + (query.length > 30 ? '...' : '')
162
  };
163
 
 
164
  setChats(prevChats =>
165
  prevChats.map(chat =>
166
  chat.id === updatedChat.id ? updatedChat : chat
 
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,
221
  chat_history: chatHistory
222
  });
 
253
  }
254
 
255
  } catch (error) {
256
+ console.error("Error querying AI:", error);
257
 
258
  // Replace loading message with error
259
  const updatedMessages = updatedChat.messages.map(msg =>
 
282
  }
283
  };
284
 
285
+ const createNewChat = () => {
 
 
 
 
 
 
 
286
  const newChat: Chat = {
287
  id: generateId(),
288
  title: "New Chat",
289
  messages: [
290
  {
291
+ id: "welcomems",
292
  content: WELCOME_MESSAGE,
293
  sender: "system" as const,
294
  timestamp: new Date()
 
300
 
301
  setActiveChat(newChat);
302
  setChats(prev => [newChat, ...prev]);
303
+ setTimeout(() => {
304
+ inputRef.current?.focus();
305
+ }, 100);
306
  setIsSidebarOpen(false);
307
  };
308
 
309
+ const selectChat = (chatId: string) => {
310
  const selectedChat = chats.find(chat => chat.id === chatId);
311
  if (selectedChat) {
312
  setActiveChat(selectedChat);
 
317
  }
318
  };
319
 
320
+ const deleteChat = (chatId: string) => {
 
 
321
  const updatedChats = chats.filter(chat => chat.id !== chatId);
322
  setChats(updatedChats);
323
 
 
327
 
328
  // If no chats left, create a new one
329
  if (updatedChats.length === 0) {
330
+ createNewChat();
331
  }
332
  }
333
+
334
+ // Clear the chat being deleted
335
+ setChatToDelete(null);
336
+ };
337
+
338
+ const handleDeleteMessage = (messageId: string) => {
339
+ if (!activeChat) return;
340
+
341
+ // Find the index of the message to delete
342
+ const messageIndex = activeChat.messages.findIndex(msg => msg.id === messageId);
343
+ if (messageIndex === -1) return;
344
+
345
+ // Determine if we need to delete a pair (user message + assistant response)
346
+ const isUserMessage = activeChat.messages[messageIndex].sender === "user";
347
+ const updatedMessages = [...activeChat.messages];
348
+
349
+ if (isUserMessage && messageIndex + 1 < updatedMessages.length &&
350
+ updatedMessages[messageIndex + 1].sender === "system") {
351
+ // Remove both the user message and the following assistant response
352
+ updatedMessages.splice(messageIndex, 2);
353
+ } else if (!isUserMessage && messageIndex > 0 &&
354
+ updatedMessages[messageIndex - 1].sender === "user") {
355
+ // Remove both the assistant message and the preceding user message
356
+ updatedMessages.splice(messageIndex - 1, 2);
357
+ } else {
358
+ // Just remove the single message
359
+ updatedMessages.splice(messageIndex, 1);
360
+ }
361
+
362
+ const updatedChat = {
363
+ ...activeChat,
364
+ messages: updatedMessages,
365
+ updatedAt: new Date()
366
+ };
367
+
368
+ setActiveChat(updatedChat);
369
+
370
+ // Update chats list
371
+ setChats(prevChats =>
372
+ prevChats.map(chat =>
373
+ chat.id === updatedChat.id ? updatedChat : chat
374
+ )
375
+ );
376
+ };
377
+
378
+ const handleRegenerateMessage = (messageId: string) => {
379
+ if (!activeChat) return;
380
+
381
+ // Find the system message that needs to be regenerated
382
+ const messageIndex = activeChat.messages.findIndex(
383
+ msg => msg.id === messageId && msg.sender === "system"
384
+ );
385
+
386
+ if (messageIndex < 0) return;
387
+
388
+ const message = activeChat.messages[messageIndex];
389
+
390
+ // Find the last user message before this system message
391
+ let userMessageContent = "";
392
+ let userMessageIndex = -1;
393
+
394
+ for (let i = messageIndex - 1; i >= 0; i--) {
395
+ if (activeChat.messages[i].sender === "user") {
396
+ userMessageContent = activeChat.messages[i].content;
397
+ userMessageIndex = i;
398
+ break;
399
+ }
400
+ }
401
+
402
+ if (!userMessageContent) return;
403
+
404
+ // Create a new variation
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 = [
414
+ ...existingVariations,
415
+ { id: variationId, content: "", timestamp: now }
416
+ ];
417
+
418
+ // Update the message with loading state
419
+ const updatedMessages = [...activeChat.messages];
420
+ updatedMessages[messageIndex] = {
421
+ ...updatedMessages[messageIndex],
422
+ isLoading: true,
423
+ variations: updatedVariations,
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({
447
+ query: userMessageContent,
448
+ chat_history: chatHistory
449
+ })
450
+ .then(response => {
451
+ // Update the variation with the actual response
452
+ const finalVariations = updatedMessages[messageIndex].variations!.map(v =>
453
+ v.id === variationId
454
+ ? { ...v, content: response.answer, timestamp: new Date() }
455
+ : v
456
+ );
457
+
458
+ const finalMessages = [...updatedMessages];
459
+ finalMessages[messageIndex] = {
460
+ ...finalMessages[messageIndex],
461
+ variations: finalVariations,
462
+ isLoading: false,
463
+ error: false,
464
+ result: response.retrieved_sources,
465
+ activeVariation: variationId
466
+ };
467
+
468
+ const finalChat = {
469
+ ...updatedChat,
470
+ messages: finalMessages
471
+ };
472
+
473
+ setActiveChat(finalChat);
474
+ setChats(prevChats =>
475
+ prevChats.map(chat =>
476
+ chat.id === finalChat.id ? finalChat : chat
477
+ )
478
+ );
479
+ })
480
+ .catch(error => {
481
+ console.error("Error regenerating response:", error);
482
+
483
+ // Remove the failed variation
484
+ const finalVariations = updatedMessages[messageIndex].variations!.filter(v =>
485
+ v.id !== variationId
486
+ );
487
+
488
+ const finalMessages = [...updatedMessages];
489
+ finalMessages[messageIndex] = {
490
+ ...finalMessages[messageIndex],
491
+ variations: finalVariations,
492
+ isLoading: false,
493
+ activeVariation: finalVariations.length > 0 ? finalVariations[0].id : undefined
494
+ };
495
+
496
+ setActiveChat({
497
+ ...updatedChat,
498
+ messages: finalMessages
499
+ });
500
+
501
+ toast.error("Failed to generate variation");
502
+ })
503
+ .finally(() => {
504
+ setIsLoading(false);
505
+ setTimeout(() => {
506
+ inputRef.current?.focus();
507
+ }, 100);
508
+ });
509
+ };
510
+
511
+ const handleSelectVariation = (messageId: string, variationId: string) => {
512
+ if (!activeChat) return;
513
+
514
+ // Find the message
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);
531
+
532
+ // Update chats list
533
+ setChats(prevChats =>
534
+ prevChats.map(chat =>
535
+ chat.id === updatedChat.id ? updatedChat : chat
536
+ )
537
+ );
538
  };
539
 
540
  const handleRetryMessage = (messageId: string) => {
 
584
  }));
585
 
586
  // Retry the query
587
+ apiService.queryRulings({
588
  query: userMessageContent,
589
  chat_history: chatHistory
590
  })
 
604
  };
605
 
606
  setActiveChat(finalChat);
 
 
607
  setChats(prevChats =>
608
  prevChats.map(chat =>
609
  chat.id === finalChat.id ? finalChat : chat
 
636
  });
637
  };
638
 
639
+ const openProfileModal = () => {
640
+ setIsProfileModalOpen(true);
641
  };
642
 
643
  return (
644
+ <>
645
+ <div className="flex h-full w-full">
646
+ {/* Chat Sidebar */}
647
+ <ChatSidebar
648
+ chats={chats}
649
+ activeChat={activeChat}
650
+ isGeneratingTitle={isGeneratingTitle}
651
+ createNewChat={createNewChat}
652
+ selectChat={selectChat}
653
+ onRequestDelete={setChatToDelete}
654
+ onOpenSettings={onOpenSettings}
655
+ onOpenSources={onOpenSources}
656
+ openProfileModal={openProfileModal}
657
+ isSidebarOpen={isSidebarOpen}
658
+ setIsSidebarOpen={setIsSidebarOpen}
659
+ />
660
+
661
+ {/* Chat Main Area */}
662
+ <div className="flex-1 flex flex-col overflow-hidden">
663
+ {/* Mobile Header */}
664
+ <div className="md:hidden p-2 flex items-center border-b">
665
+ <Button variant="ghost" size="icon" onClick={() => setIsSidebarOpen(true)}>
666
+ <PanelLeft className="h-5 w-5" />
667
+ </Button>
668
+ <div className="mx-auto font-medium flex items-center">
669
+ <Bot className="h-4 w-4 mr-1.5" />
670
+ {activeChat?.title || "New Chat"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
671
  </div>
672
+ <Button variant="ghost" size="icon" onClick={createNewChat}>
673
+ <Plus className="h-5 w-5" />
674
+ </Button>
675
  </div>
676
 
677
+ {/* Chat Area */}
678
+ <div className="relative flex-1 overflow-hidden">
679
+ {!activeChat ? (
680
+ <WelcomeScreen onCreateNewChat={createNewChat} />
 
681
  ) : (
682
+ <>
683
+ <div className="h-full overflow-y-auto px-4 pb-32 pt-4">
684
+ <div className="max-w-3xl mx-auto space-y-4">
685
+ {activeChat.messages.map((message) => (
686
+ <ChatBubble
687
+ key={message.id}
688
+ message={message}
689
+ onViewSearchResults={onOpenSources}
690
+ onRetry={handleRetryMessage}
691
+ onRegenerate={handleRegenerateMessage}
692
+ onDelete={handleDeleteMessage}
693
+ onSelectVariation={handleSelectVariation}
694
+ />
695
+ ))}
696
+ <div ref={messagesEndRef} />
 
 
 
 
 
 
 
 
 
 
697
  </div>
 
 
 
 
 
 
 
 
698
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
 
700
+ {/* Input Area */}
701
+ <ChatInputArea
702
+ inputRef={inputRef}
703
+ inputValue={inputValue}
704
+ setInputValue={setInputValue}
705
+ handleSendMessage={handleSendMessage}
706
+ isLoading={isLoading}
707
+ />
708
+ </>
709
+ )}
710
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  </div>
712
  </div>
713
+
714
+ {/* Profile Modal */}
715
+ <ProfileModal isOpen={isProfileModalOpen} onClose={() => setIsProfileModalOpen(false)} />
716
+
717
+ {/* Delete Chat Confirmation Dialog */}
718
+ <DeleteChatDialog
719
+ isOpen={chatToDelete !== null}
720
+ onOpenChange={() => setChatToDelete(null)}
721
+ onDelete={() => chatToDelete && deleteChat(chatToDelete)}
722
+ />
723
+ </>
724
  );
725
  };
 
 
frontend/src/components/chat/ChatMessage.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  // src/components/ChatMessage.tsx
2
  import React, { useState, useMemo } from 'react'
3
  import ReactMarkdown from 'react-markdown'
@@ -16,9 +17,18 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
16
  className,
17
  }) => {
18
 
19
- // ←––– THIS MEMO does the magic replace
20
- const mdWithBadges = useMemo(() => {
21
- return content.replace(
 
 
 
 
 
 
 
 
 
22
  /<source\s+path=["'](.+?)["']\s*\/>/g,
23
  (_match, path) => {
24
  const filename = path
@@ -33,8 +43,8 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
33
  </svg>
34
  </a>`
35
  }
36
- )
37
- }, [content])
38
 
39
  return (
40
  <div
@@ -65,7 +75,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
65
  ),
66
  }}
67
  >
68
- {mdWithBadges}
69
  </ReactMarkdown>
70
  </div>
71
  )
 
1
+
2
  // src/components/ChatMessage.tsx
3
  import React, { useState, useMemo } from 'react'
4
  import ReactMarkdown from 'react-markdown'
 
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
 
43
  </svg>
44
  </a>`
45
  }
46
+ );
47
+ }, [content]);
48
 
49
  return (
50
  <div
 
75
  ),
76
  }}
77
  >
78
+ {processedContent}
79
  </ReactMarkdown>
80
  </div>
81
  )
frontend/src/components/chat/ChatSidebar.tsx ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from "react";
3
+ import { format } from "date-fns";
4
+ import { Bot, TrashIcon, User, FileText, Settings, PanelLeft, Plus } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { cn } from "@/lib/utils";
7
+ import { Chat } from "@/types/chat";
8
+ import { ModeToggle } from "@/components/layout/ModeToggle";
9
+
10
+ interface ChatSidebarProps {
11
+ chats: Chat[];
12
+ activeChat: Chat | null;
13
+ isGeneratingTitle: boolean;
14
+ createNewChat: () => void;
15
+ selectChat: (id: string) => void;
16
+ onRequestDelete: (id: string) => void;
17
+ onOpenSettings: () => void;
18
+ onOpenSources: () => void;
19
+ openProfileModal: () => void;
20
+ isSidebarOpen: boolean;
21
+ setIsSidebarOpen: (open: boolean) => void;
22
+ }
23
+
24
+ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
25
+ chats,
26
+ activeChat,
27
+ isGeneratingTitle,
28
+ createNewChat,
29
+ selectChat,
30
+ onRequestDelete,
31
+ onOpenSettings,
32
+ onOpenSources,
33
+ openProfileModal,
34
+ isSidebarOpen,
35
+ setIsSidebarOpen,
36
+ }) => {
37
+ return (
38
+ <div className={cn(
39
+ "fixed top-0 bottom-0 left-0 z-20 bg-background/90 backdrop-blur-lg",
40
+ "transition-transform duration-300 ease-in-out",
41
+ isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
42
+ "w-72 lg:w-80 border-r border-border/50 flex-shrink-0",
43
+ "md:relative md:inset-auto h-full md:z-0"
44
+ )}>
45
+ <div className="flex flex-col h-full">
46
+ <div className="p-4 pb-0">
47
+ <div className="flex justify-between items-center">
48
+ <div className="flex items-center space-x-2">
49
+ <div className="h-8 w-8 bg-primary/90 rounded-md flex items-center justify-center">
50
+ <span className="text-white font-bold text-lg">AI</span>
51
+ </div>
52
+ <h1 className="text-xl font-semibold">
53
+ Insight AI
54
+ </h1>
55
+ </div>
56
+ <Button className="md:hidden" variant="ghost" size="icon" onClick={() => setIsSidebarOpen(false)}>
57
+ <PanelLeft className="h-5 w-5" />
58
+ </Button>
59
+ </div>
60
+
61
+ <div className="py-4">
62
+ <Button
63
+ onClick={createNewChat}
64
+ className="w-full justify-start gap-2"
65
+ variant="outline"
66
+ >
67
+ <Plus className="h-4 w-4" />
68
+ New Chat
69
+ </Button>
70
+ </div>
71
+
72
+ <div className="flex items-center justify-between">
73
+ <div className="flex items-center gap-2">
74
+ <Bot className="h-5 w-5 text-primary" />
75
+ <span className="font-medium">Recent Chats</span>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <div className="flex-1 overflow-y-auto p-2 space-y-1 scrollbar-thin">
81
+ {chats.length === 0 ? (
82
+ <div className="text-center text-muted-foreground p-4">
83
+ No conversations yet. Start a new one!
84
+ </div>
85
+ ) : (
86
+ chats.map(chat => (
87
+ <div
88
+ key={chat.id}
89
+ onClick={() => selectChat(chat.id)}
90
+ className={cn(
91
+ "flex items-center justify-between p-2 px-3 rounded-lg cursor-pointer group transition-all",
92
+ activeChat?.id === chat.id
93
+ ? "bg-primary/10 border border-primary/20"
94
+ : "hover:bg-muted/50 border border-transparent"
95
+ )}
96
+ >
97
+ <div className="flex-1 truncate">
98
+ <div className={cn(
99
+ "font-medium truncate flex items-center",
100
+ activeChat?.id === chat.id && "text-primary"
101
+ )}>
102
+ <Bot className="h-3.5 w-3.5 mr-1.5 opacity-70" />
103
+ {chat.title}
104
+ {chat.id === activeChat?.id && isGeneratingTitle && (
105
+ <span className="ml-1.5 inline-block h-2 w-2 rounded-full bg-primary/70 animate-pulse"></span>
106
+ )}
107
+ </div>
108
+ <div className="text-xs text-muted-foreground">
109
+ {chat.messages.filter(m => m.sender === "user").length} messages • {format(new Date(chat.updatedAt), "MMM d")}
110
+ </div>
111
+ </div>
112
+ <Button
113
+ variant="ghost"
114
+ size="icon"
115
+ className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-destructive/10 hover:text-destructive"
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ onRequestDelete(chat.id);
119
+ }}
120
+ >
121
+ <TrashIcon className="h-3.5 w-3.5" />
122
+ </Button>
123
+ </div>
124
+ ))
125
+ )}
126
+ </div>
127
+
128
+ {/* Sidebar Footer */}
129
+ <div className="p-3 space-y-2">
130
+ <Button
131
+ onClick={openProfileModal}
132
+ variant="ghost"
133
+ className="w-full justify-start gap-2 text-muted-foreground hover:text-foreground"
134
+ size="sm"
135
+ >
136
+ <User className="h-4 w-4" />
137
+ Profile
138
+ </Button>
139
+
140
+ <Button
141
+ onClick={onOpenSources}
142
+ variant="ghost"
143
+ className="w-full justify-start gap-2 text-muted-foreground hover:text-foreground"
144
+ size="sm"
145
+ >
146
+ <FileText className="h-4 w-4" />
147
+ View Sources
148
+ </Button>
149
+
150
+ <Button
151
+ onClick={onOpenSettings}
152
+ variant="ghost"
153
+ className="w-full justify-start gap-2 text-muted-foreground hover:text-foreground"
154
+ size="sm"
155
+ >
156
+ <Settings className="h-4 w-4" />
157
+ Settings
158
+ </Button>
159
+
160
+ <div className="flex items-center justify-between pt-2 border-t">
161
+ <span className="text-xs text-muted-foreground">Theme</span>
162
+ <ModeToggle />
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ );
168
+ };
frontend/src/components/chat/DeleteChatDialog.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 DeleteChatDialogProps {
15
+ isOpen: boolean;
16
+ onOpenChange: (open: boolean) => void;
17
+ onDelete: () => void;
18
+ }
19
+
20
+ export const DeleteChatDialog: React.FC<DeleteChatDialogProps> = ({
21
+ isOpen,
22
+ onOpenChange,
23
+ onDelete,
24
+ }) => {
25
+ return (
26
+ <AlertDialog open={isOpen} onOpenChange={onOpenChange}>
27
+ <AlertDialogContent>
28
+ <AlertDialogHeader>
29
+ <AlertDialogTitle>Delete Chat</AlertDialogTitle>
30
+ <AlertDialogDescription>
31
+ Are you sure you want to delete this chat? 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/ThinkingAnimation.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>
50
+ </span>
51
+ </div>
52
+ );
53
+ };
frontend/src/components/chat/WelcomeScreen.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from "react";
3
+ import { Bot } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+
6
+ interface WelcomeScreenProps {
7
+ onCreateNewChat: () => void;
8
+ }
9
+
10
+ export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({ onCreateNewChat }) => {
11
+ return (
12
+ <div className="h-full flex flex-col items-center justify-center">
13
+ <div className="text-center max-w-md p-8 bg-card/40 backdrop-blur-sm rounded-2xl border border-border/50">
14
+ <Bot className="h-10 w-10 mx-auto mb-4 text-primary" />
15
+ <h2 className="text-2xl font-bold mb-2">Welcome to Insight AI</h2>
16
+ <p className="text-muted-foreground mb-6">
17
+ Ask me anything, and I'll do my best to help you
18
+ </p>
19
+ <Button onClick={onCreateNewChat} className="animate-fade-in">
20
+ Start a new conversation
21
+ </Button>
22
+ </div>
23
+ </div>
24
+ );
25
+ };
frontend/src/components/layout/ChatLayout.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { useState } from "react";
3
+ import { ChatInterface } from "@/components/chat/ChatInterface";
4
+ import { SettingsModal } from "@/components/modals/SettingsModal";
5
+ import { SourcesModal } from "@/components/modals/SourcesModal";
6
+
7
+ const ChatLayout = () => {
8
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false);
9
+ const [isSourcesOpen, setIsSourcesOpen] = useState(false);
10
+
11
+ return (
12
+ <div className="flex h-[100dvh] md:h-screen w-full">
13
+ <ChatInterface
14
+ onOpenSettings={() => setIsSettingsOpen(true)}
15
+ onOpenSources={() => setIsSourcesOpen(true)}
16
+ />
17
+
18
+ <SettingsModal
19
+ open={isSettingsOpen}
20
+ onOpenChange={setIsSettingsOpen}
21
+ />
22
+
23
+ <SourcesModal
24
+ open={isSourcesOpen}
25
+ onOpenChange={setIsSourcesOpen}
26
+ />
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export default ChatLayout;
frontend/src/components/layout/MainLayout.tsx DELETED
@@ -1,24 +0,0 @@
1
-
2
- import { ReactNode } from "react";
3
- import { useIsMobile } from "@/hooks/use-mobile";
4
- import { useLocation } from "react-router-dom";
5
-
6
- interface MainLayoutProps {
7
- children: ReactNode;
8
- }
9
-
10
- const MainLayout = ({ children }: MainLayoutProps) => {
11
- const isMobile = useIsMobile();
12
- const location = useLocation();
13
- const isHomePage = location.pathname === "/";
14
-
15
- return (
16
- <div className="flex h-screen w-full">
17
- <main className="flex-1 overflow-y-auto">
18
- {children}
19
- </main>
20
- </div>
21
- );
22
- };
23
-
24
- export default MainLayout;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/layout/ModeToggle.tsx CHANGED
@@ -1,33 +1,30 @@
1
-
2
  import { Moon, Sun } from "lucide-react";
3
  import { useEffect, useState } from "react";
4
  import { Button } from "@/components/ui/button";
 
5
 
6
  export function ModeToggle() {
7
  const [theme, setTheme] = useState(() => {
8
- // Check local storage or default to system preference
9
- const storedTheme = localStorage.getItem("theme");
10
  if (storedTheme) return storedTheme;
11
-
12
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
13
  });
14
-
15
  useEffect(() => {
16
  const root = window.document.documentElement;
17
-
18
  if (theme === "dark") {
19
  root.classList.add("dark");
20
  } else {
21
  root.classList.remove("dark");
22
  }
23
-
24
- localStorage.setItem("theme", theme);
25
  }, [theme]);
26
-
27
  const toggleTheme = () => {
28
  setTheme(theme === "light" ? "dark" : "light");
29
  };
30
-
31
  return (
32
  <Button variant="outline" size="icon" onClick={toggleTheme} className="h-9 w-9">
33
  <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
 
 
1
  import { Moon, Sun } from "lucide-react";
2
  import { useEffect, useState } from "react";
3
  import { Button } from "@/components/ui/button";
4
+ import { storage, STORAGE_KEYS } from "@/lib/storage";
5
 
6
  export function ModeToggle() {
7
  const [theme, setTheme] = useState(() => {
8
+ // Use storage lib or default to system preference
9
+ const storedTheme = storage.get<string>(STORAGE_KEYS.THEME);
10
  if (storedTheme) return storedTheme;
 
11
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
12
  });
13
+
14
  useEffect(() => {
15
  const root = window.document.documentElement;
 
16
  if (theme === "dark") {
17
  root.classList.add("dark");
18
  } else {
19
  root.classList.remove("dark");
20
  }
21
+ storage.set(STORAGE_KEYS.THEME, theme);
 
22
  }, [theme]);
23
+
24
  const toggleTheme = () => {
25
  setTheme(theme === "light" ? "dark" : "light");
26
  };
27
+
28
  return (
29
  <Button variant="outline" size="icon" onClick={toggleTheme} className="h-9 w-9">
30
  <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
frontend/src/components/modals/ProfileModal.tsx ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useMemo } from "react";
2
+ import { Button } from "@/components/ui/button";
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "@/components/ui/dialog";
11
+ import { Input } from "@/components/ui/input";
12
+ import { Label } from "@/components/ui/label";
13
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
14
+ import { Switch } from "@/components/ui/switch";
15
+ import { User, Mail, Shield, LogOut } from "lucide-react";
16
+ import { Separator } from "@/components/ui/separator";
17
+ import { toast } from "@/components/ui/sonner";
18
+ import { storage, STORAGE_KEYS } from "@/lib/storage";
19
+ import {
20
+ AlertDialog,
21
+ AlertDialogAction,
22
+ AlertDialogCancel,
23
+ AlertDialogContent,
24
+ AlertDialogDescription,
25
+ AlertDialogFooter,
26
+ AlertDialogHeader,
27
+ AlertDialogTitle,
28
+ } from "@/components/ui/alert-dialog";
29
+
30
+ // Use the ESM build of multiavatar to generate SVG strings
31
+ import multiavatar from "@multiavatar/multiavatar/esm";
32
+
33
+ interface ProfileModalProps {
34
+ isOpen: boolean;
35
+ onClose: () => void;
36
+ }
37
+
38
+ interface ProfileSettings {
39
+ name: string;
40
+ email: string;
41
+ avatarUrl?: string; // now optional
42
+ saveHistory: boolean;
43
+ }
44
+
45
+ const PROFILE_STORAGE_KEY = STORAGE_KEYS.PROFILE_STORAGE_KEY;
46
+
47
+ export const ProfileModal = ({ isOpen, onClose }: ProfileModalProps) => {
48
+ const [signOutDialogOpen, setSignOutDialogOpen] = useState(false);
49
+
50
+ const [profileSettings, setProfileSettings] = useState<ProfileSettings>(() => {
51
+ // load, or default (no external URL here)
52
+ return (
53
+ storage.get<ProfileSettings>(PROFILE_STORAGE_KEY) || {
54
+ name: "John Doe",
55
+ email: "[email protected]",
56
+ avatarUrl: undefined,
57
+ saveHistory: true,
58
+ }
59
+ );
60
+ });
61
+
62
+ // reload from storage whenever modal re-opens
63
+ useEffect(() => {
64
+ if (isOpen) {
65
+ const saved = storage.get<ProfileSettings>(PROFILE_STORAGE_KEY);
66
+ if (saved) setProfileSettings(saved);
67
+ }
68
+ }, [isOpen]);
69
+
70
+ // memoize the SVG data URI so it only regenerates when the name changes
71
+ const defaultAvatarDataUri = useMemo(() => {
72
+ const svg = multiavatar(profileSettings.name);
73
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
74
+ }, [profileSettings.name]);
75
+
76
+ // pick either the custom URL or the inline SVG
77
+ const avatarSrc = profileSettings.avatarUrl || defaultAvatarDataUri;
78
+
79
+ const handleSaveChanges = () => {
80
+ storage.set(PROFILE_STORAGE_KEY, profileSettings);
81
+ toast.success("Profile settings saved successfully");
82
+ onClose();
83
+ };
84
+
85
+ const handleSignOut = () => {
86
+ toast.success("Signed out successfully");
87
+ setSignOutDialogOpen(false);
88
+ onClose();
89
+ // clear auth tokens, etc.
90
+ };
91
+
92
+ return (
93
+ <>
94
+ <Dialog open={isOpen} onOpenChange={onClose}>
95
+ <DialogContent className="sm:max-w-[500px] max-h-[85vh] overflow-y-auto">
96
+ <DialogHeader>
97
+ <DialogTitle className="text-xl flex items-center">
98
+ <User className="mr-2 h-5 w-5" />
99
+ Profile Settings
100
+ </DialogTitle>
101
+ <DialogDescription>
102
+ Manage your profile information and preferences.
103
+ </DialogDescription>
104
+ </DialogHeader>
105
+ <Separator />
106
+
107
+ <div className="flex flex-col space-y-6 py-4">
108
+ {/* Profile section */}
109
+ <div className="flex flex-col space-y-4">
110
+ <div className="flex items-center space-x-4">
111
+ <Avatar className="h-20 w-20">
112
+ <AvatarImage src={avatarSrc} alt="User avatar" />
113
+ <AvatarFallback className="bg-primary/10 text-lg">
114
+ <User className="h-8 w-8 text-primary" />
115
+ </AvatarFallback>
116
+ </Avatar>
117
+ <div className="space-y-1">
118
+ <h3 className="font-medium text-lg">{profileSettings.name}</h3>
119
+ <p className="text-sm text-muted-foreground">{profileSettings.email}</p>
120
+ <Button variant="outline" size="sm" className="mt-2">
121
+ Change picture
122
+ </Button>
123
+ </div>
124
+ </div>
125
+
126
+ <Separator />
127
+
128
+ <div className="grid gap-4">
129
+ {/* Name */}
130
+ <div className="grid grid-cols-4 items-center gap-4">
131
+ <Label htmlFor="name" className="text-right">
132
+ Name
133
+ </Label>
134
+ <Input
135
+ id="name"
136
+ value={profileSettings.name}
137
+ onChange={(e) =>
138
+ setProfileSettings({ ...profileSettings, name: e.target.value })
139
+ }
140
+ className="col-span-3"
141
+ />
142
+ </div>
143
+ {/* Email */}
144
+ <div className="grid grid-cols-4 items-center gap-4">
145
+ <Label htmlFor="email" className="text-right">
146
+ Email
147
+ </Label>
148
+ <Input
149
+ id="email"
150
+ value={profileSettings.email}
151
+ onChange={(e) =>
152
+ setProfileSettings({ ...profileSettings, email: e.target.value })
153
+ }
154
+ className="col-span-3"
155
+ />
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ {/* Privacy */}
161
+ <div className="space-y-4">
162
+ <h3 className="text-lg font-medium flex items-center gap-2">
163
+ <Shield className="h-5 w-5" />
164
+ Privacy and Data
165
+ </h3>
166
+ <Separator />
167
+
168
+ <div className="flex items-center justify-between">
169
+ <div className="space-y-0.5">
170
+ <Label htmlFor="chat-history">Save Chat History</Label>
171
+ <p className="text-sm text-muted-foreground">
172
+ Keep your chat history saved
173
+ </p>
174
+ </div>
175
+ <Switch
176
+ id="chat-history"
177
+ checked={profileSettings.saveHistory}
178
+ onCheckedChange={(saveHistory) =>
179
+ setProfileSettings({ ...profileSettings, saveHistory })
180
+ }
181
+ />
182
+ </div>
183
+ </div>
184
+ </div>
185
+
186
+ <DialogFooter className="flex items-center justify-between mt-6">
187
+ <Button
188
+ variant="destructive"
189
+ size="sm"
190
+ onClick={() => setSignOutDialogOpen(true)}
191
+ >
192
+ <LogOut className="mr-2 h-4 w-4" />
193
+ Sign Out
194
+ </Button>
195
+ <Button onClick={handleSaveChanges}>Save changes</Button>
196
+ </DialogFooter>
197
+ </DialogContent>
198
+ </Dialog>
199
+
200
+ {/* Sign-out confirmation */}
201
+ <AlertDialog open={signOutDialogOpen} onOpenChange={setSignOutDialogOpen}>
202
+ <AlertDialogContent>
203
+ <AlertDialogHeader>
204
+ <AlertDialogTitle>Sign Out</AlertDialogTitle>
205
+ <AlertDialogDescription>
206
+ Are you sure you want to sign out? You will need to sign in again to
207
+ access your account.
208
+ </AlertDialogDescription>
209
+ </AlertDialogHeader>
210
+ <AlertDialogFooter>
211
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
212
+ <AlertDialogAction
213
+ onClick={handleSignOut}
214
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
215
+ >
216
+ Sign Out
217
+ </AlertDialogAction>
218
+ </AlertDialogFooter>
219
+ </AlertDialogContent>
220
+ </AlertDialog>
221
+ </>
222
+ );
223
+ };
frontend/src/{pages/SettingsPage.tsx → components/modals/SettingsModal.tsx} RENAMED
@@ -1,30 +1,49 @@
1
- import { useState, useRef } from "react";
2
- import { Settings, Moon, Sun, Globe, Shield, Database, Cloud } from "lucide-react";
3
  import { Button } from "@/components/ui/button";
4
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
  import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
6
  import { Label } from "@/components/ui/label";
7
- import { Separator } from "@/components/ui/separator";
8
  import { Input } from "@/components/ui/input";
 
9
  import { toast } from "@/components/ui/sonner";
10
  import { storage, STORAGE_KEYS } from "@/lib/storage";
 
 
 
 
 
 
11
 
12
- const SettingsPage = () => {
 
 
 
 
 
 
 
13
  const [theme, setTheme] = useState(() => {
14
  return storage.get<string>(STORAGE_KEYS.THEME) || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
15
  });
16
-
17
- const [apiEndpoint, setApiEndpoint] = useState(
18
- storage.get<string>(STORAGE_KEYS.API_ENDPOINT) || "http://localhost:8000"
19
  );
20
-
21
  const fileInputRef = useRef<HTMLInputElement>(null);
22
 
 
 
 
 
 
 
 
 
23
  const handleThemeChange = (newTheme: string) => {
24
  setTheme(newTheme);
25
-
26
  const root = window.document.documentElement;
27
-
28
  if (newTheme === "dark") {
29
  root.classList.add("dark");
30
  } else if (newTheme === "light") {
@@ -37,33 +56,31 @@ const SettingsPage = () => {
37
  root.classList.remove("dark");
38
  }
39
  }
40
-
41
  storage.set(STORAGE_KEYS.THEME, newTheme);
42
  toast.success("Theme updated successfully");
43
  };
44
-
45
  const handleSaveEndpoint = () => {
46
  storage.set(STORAGE_KEYS.API_ENDPOINT, apiEndpoint);
47
  toast.success("API endpoint saved successfully");
48
  };
49
-
50
  const handleClearChats = () => {
51
  storage.set(STORAGE_KEYS.CHATS, []);
52
  toast.success("Chat history cleared successfully");
 
53
  };
54
-
55
  const handleClearSources = () => {
56
  storage.set(STORAGE_KEYS.SOURCES, []);
57
  toast.success("Sources cleared successfully");
58
  };
59
-
60
  const handleResetSettings = () => {
61
- storage.remove(STORAGE_KEYS.THEME);
62
- storage.remove(STORAGE_KEYS.API_ENDPOINT);
63
-
64
- setApiEndpoint("http://localhost:8000");
65
- setTheme(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
66
-
67
  toast.success("Settings reset to defaults");
68
  };
69
 
@@ -101,172 +118,138 @@ const SettingsPage = () => {
101
  reader.readAsText(file);
102
  event.target.value = "";
103
  };
104
-
105
  return (
106
- <div className="container mx-auto px-4 py-8">
107
- <div className="max-w-4xl mx-auto">
108
- <h1 className="text-2xl font-bold mb-6 text-gradient flex items-center">
109
- <Settings className="mr-2 h-5 w-5" />
110
- System Settings
111
- </h1>
112
-
113
- <div className="grid gap-6">
114
- <Card className="glass-effect">
115
- <CardHeader className="border-b border-border/40 pb-4">
116
- <CardTitle className="flex items-center">
117
- <Moon className="mr-2 h-4 w-4" />
118
- Appearance
119
- </CardTitle>
120
- <CardDescription>
121
- Customize how Insight AI looks
122
- </CardDescription>
123
- </CardHeader>
124
- <CardContent className="pt-4">
125
- <RadioGroup
126
- value={theme}
127
- onValueChange={handleThemeChange}
128
- className="space-y-4"
129
- >
130
- <div className="flex items-center space-x-2">
131
- <RadioGroupItem value="light" id="light" />
132
- <Label htmlFor="light" className="flex items-center cursor-pointer">
133
- <Sun className="h-4 w-4 mr-2" />
134
- Light
135
- </Label>
136
- </div>
137
- <div className="flex items-center space-x-2">
138
- <RadioGroupItem value="dark" id="dark" />
139
- <Label htmlFor="dark" className="flex items-center cursor-pointer">
140
- <Moon className="h-4 w-4 mr-2" />
141
- Dark
142
- </Label>
143
- </div>
144
- <div className="flex items-center space-x-2">
145
- <RadioGroupItem value="system" id="system" />
146
- <Label htmlFor="system" className="flex items-center cursor-pointer">
147
- <Globe className="h-4 w-4 mr-2" />
148
- System
149
- </Label>
150
- </div>
151
- </RadioGroup>
152
- </CardContent>
153
- </Card>
154
-
155
- <Card className="glass-effect">
156
- <CardHeader className="border-b border-border/40 pb-4">
157
- <CardTitle className="flex items-center">
158
- <Database className="mr-2 h-4 w-4" />
159
- API Configuration
160
- </CardTitle>
161
- <CardDescription>
162
- Configure connection to the financial rulings database
163
- </CardDescription>
164
- </CardHeader>
165
- <CardContent className="space-y-4 pt-4">
166
- <div className="space-y-2">
167
- <Label htmlFor="api-endpoint">API Endpoint</Label>
168
- <div className="flex space-x-2">
169
- <Input
170
- id="api-endpoint"
171
- value={apiEndpoint}
172
- onChange={(e) => setApiEndpoint(e.target.value)}
173
- placeholder="http://localhost:8000"
174
- className="glass-input"
175
- />
176
- <Button onClick={handleSaveEndpoint} variant="outline" className="tech-button">
177
- <Cloud className="h-4 w-4 mr-2" />
178
- Save
179
- </Button>
180
- </div>
181
- </div>
182
- </CardContent>
183
- </Card>
184
-
185
- <Card className="glass-effect">
186
- <CardHeader className="border-b border-border/40 pb-4">
187
- <CardTitle className="flex items-center">
188
- <Shield className="mr-2 h-4 w-4" />
189
- Privacy & Data
190
- </CardTitle>
191
- <CardDescription>
192
- Manage your data and privacy settings
193
- </CardDescription>
194
- </CardHeader>
195
- <CardContent className="pt-4 space-y-4">
196
- <div className="space-y-2">
197
- <h3 className="text-sm font-medium">Data Management</h3>
198
- <div className="flex flex-wrap gap-3">
199
- <Button
200
- variant="outline"
201
- onClick={handleClearChats}
202
- className="tech-button"
203
- >
204
- Clear Chat History
205
- </Button>
206
-
207
- <Button
208
- variant="outline"
209
- onClick={handleClearSources}
210
- className="tech-button"
211
- >
212
- Clear Source References
213
- </Button>
214
-
215
- <Button
216
- variant="destructive"
217
- onClick={handleResetSettings}
218
- className="tech-button"
219
  >
220
- Reset All Settings
221
- </Button>
 
 
222
  </div>
223
- </div>
224
- </CardContent>
225
- </Card>
226
 
227
- <Card className="glass-effect">
228
- <CardHeader className="border-b border-border/40 pb-4">
229
- <CardTitle className="flex items-center">
230
- <Cloud className="mr-2 h-4 w-4" />
231
- Storage Export / Import
232
- </CardTitle>
233
- <CardDescription>
234
- Backup or restore your application data
235
- </CardDescription>
236
- </CardHeader>
237
- <CardContent className="pt-4 space-y-4">
238
- <div className="flex flex-wrap gap-3">
239
- <Button
240
- variant="outline"
241
- onClick={handleExportStorage}
242
- className="tech-button"
243
- >
244
- Export Storage
245
- </Button>
246
- <Button
247
- variant="outline"
248
- onClick={() => fileInputRef.current?.click()}
249
- className="tech-button"
250
- >
251
- Import Storage
252
- </Button>
253
- <input
254
- ref={fileInputRef}
255
- type="file"
256
- accept="application/json"
257
- style={{ display: "none" }}
258
- onChange={handleImportStorage}
259
  />
 
 
 
 
260
  </div>
261
- <div className="text-xs text-muted-foreground">
262
- Export will download your data as a JSON file. Import will overwrite your current data with the file contents.
263
- </div>
264
- </CardContent>
265
- </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  </div>
267
- </div>
268
- </div>
269
  );
270
  };
271
-
272
- export default SettingsPage;
 
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
3
  import { Button } from "@/components/ui/button";
 
4
  import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
5
  import { Label } from "@/components/ui/label";
 
6
  import { Input } from "@/components/ui/input";
7
+ import { Separator } from "@/components/ui/separator";
8
  import { toast } from "@/components/ui/sonner";
9
  import { storage, STORAGE_KEYS } from "@/lib/storage";
10
+ import { X, Settings, Moon, Sun, Globe, Shield, Database, Cloud } from "lucide-react";
11
+
12
+ interface SettingsModalProps {
13
+ open: boolean;
14
+ onOpenChange: (open: boolean) => void;
15
+ }
16
 
17
+ const THEME_OPTIONS = [
18
+ { value: "light", label: "Light", icon: Sun },
19
+ { value: "dark", label: "Dark", icon: Moon },
20
+ { value: "system", label: "System", icon: Globe },
21
+ ];
22
+
23
+ export const SettingsModal = ({ open, onOpenChange }: SettingsModalProps) => {
24
+ // Load settings from storage on mount
25
  const [theme, setTheme] = useState(() => {
26
  return storage.get<string>(STORAGE_KEYS.THEME) || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
27
  });
28
+ const [apiEndpoint, setApiEndpoint] = useState(() =>
29
+ storage.get<string>(STORAGE_KEYS.API_ENDPOINT) || "https://insight-ai-api.hf.space"
 
30
  );
31
+
32
  const fileInputRef = useRef<HTMLInputElement>(null);
33
 
34
+ // Sync settings from storage when modal opens
35
+ useEffect(() => {
36
+ if (open) {
37
+ setTheme(storage.get<string>(STORAGE_KEYS.THEME));
38
+ setApiEndpoint(storage.get<string>(STORAGE_KEYS.API_ENDPOINT));
39
+ }
40
+ }, [open]);
41
+
42
  const handleThemeChange = (newTheme: string) => {
43
  setTheme(newTheme);
44
+
45
  const root = window.document.documentElement;
46
+
47
  if (newTheme === "dark") {
48
  root.classList.add("dark");
49
  } else if (newTheme === "light") {
 
56
  root.classList.remove("dark");
57
  }
58
  }
59
+
60
  storage.set(STORAGE_KEYS.THEME, newTheme);
61
  toast.success("Theme updated successfully");
62
  };
63
+
64
  const handleSaveEndpoint = () => {
65
  storage.set(STORAGE_KEYS.API_ENDPOINT, apiEndpoint);
66
  toast.success("API endpoint saved successfully");
67
  };
68
+
69
  const handleClearChats = () => {
70
  storage.set(STORAGE_KEYS.CHATS, []);
71
  toast.success("Chat history cleared successfully");
72
+ window.location.reload();
73
  };
74
+
75
  const handleClearSources = () => {
76
  storage.set(STORAGE_KEYS.SOURCES, []);
77
  toast.success("Sources cleared successfully");
78
  };
79
+
80
  const handleResetSettings = () => {
81
+ // Reset all keys to their default values using storage lib
82
+ storage.resetToDefaults();
83
+ window.location.reload();
 
 
 
84
  toast.success("Settings reset to defaults");
85
  };
86
 
 
118
  reader.readAsText(file);
119
  event.target.value = "";
120
  };
121
+
122
  return (
123
+ <Dialog open={open} onOpenChange={onOpenChange}>
124
+ <DialogContent className="w-full max-w-3xl">
125
+ <DialogHeader>
126
+ <DialogTitle className="flex items-center text-xl">
127
+ <Settings className="mr-2 h-5 w-5" />
128
+ Settings
129
+ </DialogTitle>
130
+ </DialogHeader>
131
+ <Separator />
132
+ <div className="max-h-[80dvh] md:max-h-[75vh] overflow-y-auto grid gap-6">
133
+ <div className="space-y-4">
134
+ <h3 className="text-sm font-medium flex items-center">
135
+ <Moon className="mr-2 h-4 w-4" />
136
+ Appearance
137
+ </h3>
138
+ <RadioGroup
139
+ value={theme}
140
+ onValueChange={handleThemeChange}
141
+ className="grid grid-cols-3 gap-2"
142
+ >
143
+ {THEME_OPTIONS.map(({ value, label, icon: Icon }) => (
144
+ <div key={value} className="flex flex-col items-center space-y-2">
145
+ <Label
146
+ htmlFor={value}
147
+ className={
148
+ `flex flex-col items-center justify-center w-full h-20 border rounded-md cursor-pointer transition-colors
149
+ ${theme === value
150
+ ? "bg-financial-accent/30 border-financial-accent/30 ring-2 ring-financial-accent"
151
+ : "hover:bg-financial-accent/30 hover:border-financial-accent/30"}
152
+ `
153
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  >
155
+ <Icon className="h-8 w-8 mb-2" />
156
+ {label}
157
+ </Label>
158
+ <RadioGroupItem value={value} id={value} className="sr-only" />
159
  </div>
160
+ ))}
161
+ </RadioGroup>
162
+ </div>
163
 
164
+ <Separator />
165
+
166
+ <div className="space-y-4">
167
+ <h3 className="text-sm font-medium flex items-center">
168
+ <Database className="mr-2 h-4 w-4" />
169
+ API Configuration
170
+ </h3>
171
+ <div className="space-y-2">
172
+ <Label htmlFor="api-endpoint">API Endpoint</Label>
173
+ <div className="flex space-x-2">
174
+ <Input
175
+ id="api-endpoint"
176
+ value={apiEndpoint}
177
+ onChange={(e) => setApiEndpoint(e.target.value)}
178
+ placeholder="Ex: http://localhost:8000"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  />
180
+ <Button onClick={handleSaveEndpoint} variant="outline">
181
+ <Cloud className="h-4 w-4 mr-2" />
182
+ Save
183
+ </Button>
184
  </div>
185
+ </div>
186
+ </div>
187
+
188
+ <Separator />
189
+
190
+ <div className="space-y-4">
191
+ <h3 className="text-sm font-medium flex items-center">
192
+ <Shield className="mr-2 h-4 w-4" />
193
+ Data Management
194
+ </h3>
195
+ <div className="flex flex-wrap gap-3">
196
+ <Button
197
+ variant="outline"
198
+ onClick={handleClearChats}
199
+ >
200
+ Clear Chat History
201
+ </Button>
202
+
203
+ <Button
204
+ variant="outline"
205
+ onClick={handleClearSources}
206
+ >
207
+ Clear Sources
208
+ </Button>
209
+
210
+ <Button
211
+ variant="destructive"
212
+ onClick={handleResetSettings}
213
+ >
214
+ Reset All Settings
215
+ </Button>
216
+ </div>
217
+ </div>
218
+
219
+ <Separator />
220
+
221
+ <div className="space-y-4">
222
+ <h3 className="text-sm font-medium flex items-center">
223
+ <Cloud className="mr-2 h-4 w-4" />
224
+ Backup & Restore
225
+ </h3>
226
+ <div className="flex flex-wrap gap-3">
227
+ <Button
228
+ variant="outline"
229
+ onClick={handleExportStorage}
230
+ >
231
+ Export Data
232
+ </Button>
233
+ <Button
234
+ variant="outline"
235
+ onClick={() => fileInputRef.current?.click()}
236
+ >
237
+ Import Data
238
+ </Button>
239
+ <input
240
+ ref={fileInputRef}
241
+ type="file"
242
+ accept="application/json"
243
+ style={{ display: "none" }}
244
+ onChange={handleImportStorage}
245
+ />
246
+ </div>
247
+ <div className="text-xs text-muted-foreground">
248
+ Export will download your data as a JSON file. Import will overwrite your current data.
249
+ </div>
250
+ </div>
251
  </div>
252
+ </DialogContent>
253
+ </Dialog>
254
  );
255
  };
 
 
frontend/src/components/modals/SourcesModal.tsx ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react";
2
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
7
+ import { Calendar as CalendarComponent } from "@/components/ui/calendar";
8
+ import { cn } from "@/lib/utils";
9
+ import { format } from "date-fns";
10
+ import { DateRange } from "@/types";
11
+ import { RetrievedSource } from "@/services/apiService";
12
+ import { storage, STORAGE_KEYS } from "@/lib/storage";
13
+ import {
14
+ Search,
15
+ ArrowUpDown,
16
+ Calendar,
17
+ FileText,
18
+ ChevronDown,
19
+ ChevronUp,
20
+ ExternalLink
21
+ } from "lucide-react";
22
+
23
+ interface SourcesModalProps {
24
+ open: boolean;
25
+ onOpenChange: (open: boolean) => void;
26
+ }
27
+
28
+ export const SourcesModal = ({ open, onOpenChange }: SourcesModalProps) => {
29
+ const [searchTerm, setSearchTerm] = useState("");
30
+ const [selectedSource, setSelectedSource] = useState<string>("");
31
+ const [selectedCategory, setSelectedCategory] = useState<string>("");
32
+ const [date, setDate] = useState<DateRange>({
33
+ from: undefined,
34
+ to: undefined,
35
+ });
36
+ const [sortBy, setSortBy] = useState<string>("date-desc");
37
+ const [sources, setSources] = useState<RetrievedSource[]>([]);
38
+ const [sourceTypes, setSourceTypes] = useState<string[]>([]);
39
+ const [categories, setCategories] = useState<string[]>([]);
40
+
41
+ // Load sources from storage
42
+ useEffect(() => {
43
+ if (open) {
44
+ const storedSources = storage.get<RetrievedSource[]>(STORAGE_KEYS.SOURCES) || [];
45
+ setSources(storedSources);
46
+
47
+ // Extract unique source types and categories
48
+ const types = Array.from(new Set(storedSources.map(s => s.metadata?.source).filter(Boolean)));
49
+ setSourceTypes(types as string[]);
50
+
51
+ const cats = Array.from(new Set(storedSources.map(s => {
52
+ // For this example, we'll use the first word of the content as a mock category
53
+ const firstWord = s.content_snippet.split(' ')[0];
54
+ return firstWord.length > 3 ? firstWord : "General";
55
+ })));
56
+ setCategories(cats);
57
+ }
58
+ }, [open]);
59
+
60
+ // Filter sources based on search term, source, category, and date
61
+ const filteredSources = sources.filter(source => {
62
+ const matchesSearch = !searchTerm ||
63
+ source.content_snippet.toLowerCase().includes(searchTerm.toLowerCase()) ||
64
+ (source.metadata?.source || "").toLowerCase().includes(searchTerm.toLowerCase());
65
+
66
+ const matchesSource = !selectedSource || selectedSource === "All" || source.metadata?.source === selectedSource;
67
+
68
+ // Mock category matching based on first word of content
69
+ const sourceCategory = source.content_snippet.split(' ')[0].length > 3 ?
70
+ source.content_snippet.split(' ')[0] : "General";
71
+ const matchesCategory = !selectedCategory || selectedCategory === "All" || sourceCategory === selectedCategory;
72
+
73
+ let matchesDate = true;
74
+ if (date.from && source.metadata?.ruling_date) {
75
+ matchesDate = matchesDate && new Date(source.metadata.ruling_date) >= date.from;
76
+ }
77
+ if (date.to && source.metadata?.ruling_date) {
78
+ matchesDate = matchesDate && new Date(source.metadata.ruling_date) <= date.to;
79
+ }
80
+
81
+ return matchesSearch && matchesSource && matchesCategory && matchesDate;
82
+ });
83
+
84
+ // Sort sources
85
+ const sortedSources = [...filteredSources].sort((a, b) => {
86
+ if (sortBy === "date-desc") {
87
+ return new Date(b.metadata?.ruling_date || "").getTime() -
88
+ new Date(a.metadata?.ruling_date || "").getTime();
89
+ } else if (sortBy === "date-asc") {
90
+ return new Date(a.metadata?.ruling_date || "").getTime() -
91
+ new Date(b.metadata?.ruling_date || "").getTime();
92
+ } else if (sortBy === "relevance-desc") {
93
+ return b.content_snippet.length - a.content_snippet.length;
94
+ }
95
+ return 0;
96
+ });
97
+
98
+ const resetFilters = () => {
99
+ setSearchTerm("");
100
+ setSelectedSource("");
101
+ setSelectedCategory("");
102
+ setDate({ from: undefined, to: undefined });
103
+ };
104
+
105
+ const [expandedSources, setExpandedSources] = useState<Record<number, boolean>>({});
106
+
107
+ const toggleSource = (index: number) => {
108
+ setExpandedSources(prev => ({
109
+ ...prev,
110
+ [index]: !prev[index]
111
+ }));
112
+ };
113
+
114
+ return (
115
+ <Dialog open={open} onOpenChange={onOpenChange}>
116
+ <DialogContent className="w-full max-w-4xl">
117
+ <DialogHeader>
118
+ <DialogTitle className="flex items-center text-xl">
119
+ <FileText className="mr-2 h-5 w-5" />
120
+ Knowledge Sources
121
+ </DialogTitle>
122
+ </DialogHeader>
123
+
124
+ <div>
125
+ <div className="flex flex-col md:flex-row md:items-center justify-between mb-2">
126
+ <div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2">
127
+ <Select value={sortBy} onValueChange={setSortBy}>
128
+ <SelectTrigger className="w-[180px]">
129
+ <div className="flex items-center">
130
+ <ArrowUpDown className="mr-2 h-3.5 w-3.5 text-muted-foreground" />
131
+ <span>Sort By</span>
132
+ </div>
133
+ </SelectTrigger>
134
+ <SelectContent>
135
+ <SelectItem value="date-desc">Date (Newest)</SelectItem>
136
+ <SelectItem value="date-asc">Date (Oldest)</SelectItem>
137
+ <SelectItem value="relevance-desc">Relevance</SelectItem>
138
+ </SelectContent>
139
+ </Select>
140
+ </div>
141
+ </div>
142
+
143
+ <div className="bg-accent/30 rounded-lg p-1 mb-2">
144
+ <div className="flex flex-col md:flex-row space-y-1 md:space-y-0 md:space-x-2">
145
+ <div className="relative flex-1">
146
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
147
+ <Input
148
+ type="text"
149
+ placeholder="Search sources..."
150
+ value={searchTerm}
151
+ onChange={(e) => setSearchTerm(e.target.value)}
152
+ className="pl-10"
153
+ />
154
+ </div>
155
+
156
+ <Select value={selectedSource} onValueChange={setSelectedSource}>
157
+ <SelectTrigger className="w-full md:w-[180px]">
158
+ <span className="truncate">
159
+ {selectedSource || "All Sources"}
160
+ </span>
161
+ </SelectTrigger>
162
+ <SelectContent>
163
+ <SelectItem value="All">All Sources</SelectItem>
164
+ {sourceTypes.map(type => (
165
+ <SelectItem key={type} value={type}>{type}</SelectItem>
166
+ ))}
167
+ </SelectContent>
168
+ </Select>
169
+
170
+ <Select value={selectedCategory} onValueChange={setSelectedCategory}>
171
+ <SelectTrigger className="w-full md:w-[180px]">
172
+ <span className="truncate">
173
+ {selectedCategory || "All Categories"}
174
+ </span>
175
+ </SelectTrigger>
176
+ <SelectContent>
177
+ <SelectItem value="All">All Categories</SelectItem>
178
+ {categories.map(category => (
179
+ <SelectItem key={category} value={category}>{category}</SelectItem>
180
+ ))}
181
+ </SelectContent>
182
+ </Select>
183
+
184
+ <Popover>
185
+ <PopoverTrigger asChild>
186
+ <Button variant="outline" className="w-full md:w-[180px] justify-start text-left">
187
+ <Calendar className="mr-2 h-4 w-4" />
188
+ <span>
189
+ {date.from || date.to ? (
190
+ <>
191
+ {date.from ? format(date.from, "LLL dd, y") : "From"} - {" "}
192
+ {date.to ? format(date.to, "LLL dd, y") : "To"}
193
+ </>
194
+ ) : (
195
+ "Date Range"
196
+ )}
197
+ </span>
198
+ </Button>
199
+ </PopoverTrigger>
200
+ <PopoverContent className="w-auto p-0">
201
+ <CalendarComponent
202
+ mode="range"
203
+ selected={date}
204
+ onSelect={(value: DateRange | undefined) => {
205
+ if (value) setDate(value);
206
+ }}
207
+ className="p-3"
208
+ />
209
+ </PopoverContent>
210
+ </Popover>
211
+
212
+ <Button
213
+ variant="outline"
214
+ className="md:w-auto"
215
+ onClick={resetFilters}
216
+ >
217
+ Reset
218
+ </Button>
219
+ </div>
220
+ </div>
221
+
222
+ <div className="max-h-[45dvh] md:max-h-[60vh] overflow-y-auto space-y-4">
223
+ {sortedSources.length > 0 ? (
224
+ sortedSources.map((source, index) => (
225
+ <div key={index} className="border rounded-lg overflow-hidden transition-all duration-300">
226
+ <div className="p-4 bg-card">
227
+ <div className="flex items-start justify-between">
228
+ <div>
229
+ <h3 className="font-medium">
230
+ {source.metadata?.source || "Source"}
231
+ </h3>
232
+ <div className="flex items-center mt-1 text-sm text-muted-foreground">
233
+ <FileText className="h-3.5 w-3.5 mr-1.5" />
234
+ <span>{source.metadata?.source || "Unknown Source"}</span>
235
+ {source.metadata?.ruling_date && (
236
+ <>
237
+ <span className="mx-1.5">•</span>
238
+ <Calendar className="h-3.5 w-3.5 mr-1.5" />
239
+ <span>{new Date(source.metadata.ruling_date).toLocaleDateString()}</span>
240
+ </>
241
+ )}
242
+ </div>
243
+ </div>
244
+ <Button
245
+ variant="ghost"
246
+ size="sm"
247
+ onClick={() => toggleSource(index)}
248
+ className="p-0 h-8 w-8 hover:bg-accent/10"
249
+ >
250
+ {expandedSources[index] ? (
251
+ <ChevronUp className="h-4 w-4" />
252
+ ) : (
253
+ <ChevronDown className="h-4 w-4" />
254
+ )}
255
+ </Button>
256
+ </div>
257
+ <div className={expandedSources[index] ? "" : "max-h-10 md:max-h-16 overflow-hidden relative"}>
258
+ <p className="text-sm text-muted-foreground mt-2">
259
+ {source.content_snippet}
260
+ </p>
261
+ {!expandedSources[index] && (
262
+ <div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-card to-transparent"></div>
263
+ )}
264
+ </div>
265
+ {expandedSources[index] && (
266
+ <div className="mt-4 text-sm flex justify-end">
267
+ <Button variant="link" size="sm" className="h-8 p-0 text-primary">
268
+ <ExternalLink className="h-3 w-3 mr-1" />
269
+ View source
270
+ </Button>
271
+ </div>
272
+ )}
273
+ </div>
274
+ </div>
275
+ ))
276
+ ) : (
277
+ <div className="text-center p-8">
278
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-muted/30 flex items-center justify-center">
279
+ <FileText className="h-8 w-8 text-muted-foreground" />
280
+ </div>
281
+ <h3 className="text-lg font-medium mb-2">No sources found</h3>
282
+ <p className="text-muted-foreground">
283
+ {sources.length > 0
284
+ ? "No sources match your current search criteria. Try adjusting your filters."
285
+ : "Chat with Insight AI to get information with source citations."}
286
+ </p>
287
+ <Button
288
+ variant="outline"
289
+ className="mt-4"
290
+ onClick={resetFilters}
291
+ >
292
+ Reset Filters
293
+ </Button>
294
+ </div>
295
+ )}
296
+ </div>
297
+ </div>
298
+ </DialogContent>
299
+ </Dialog>
300
+ );
301
+ };
frontend/src/index.css CHANGED
@@ -1,10 +1,34 @@
1
-
2
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
3
 
4
  @tailwind base;
5
  @tailwind components;
6
  @tailwind utilities;
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  @layer base {
9
  :root {
10
  --background: 222 45% 98%;
@@ -176,6 +200,122 @@
176
  .message-bubble-user {
177
  border-top-right-radius: 4px;
178
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  .typing-indicator span {
181
  @apply inline-block h-2 w-2 rounded-full bg-current;
@@ -297,3 +437,96 @@ pre code {
297
  transform: translateX(100%);
298
  transition: transform 0.5s;
299
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
2
 
3
  @tailwind base;
4
  @tailwind components;
5
  @tailwind utilities;
6
 
7
+ @layer utilities {
8
+ /* Custom scrollbar utility */
9
+ .scrollbar-thin {
10
+ scrollbar-width: thin;
11
+ }
12
+
13
+ .scrollbar-thin::-webkit-scrollbar {
14
+ width: 5px;
15
+ height: 5px;
16
+ }
17
+
18
+ .scrollbar-thin::-webkit-scrollbar-track {
19
+ background: transparent;
20
+ }
21
+
22
+ .scrollbar-thin::-webkit-scrollbar-thumb {
23
+ background: rgba(var(--muted-foreground), 0.3);
24
+ border-radius: 9999px;
25
+ }
26
+
27
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
28
+ background: rgba(var(--muted-foreground), 0.5);
29
+ }
30
+ }
31
+
32
  @layer base {
33
  :root {
34
  --background: 222 45% 98%;
 
200
  .message-bubble-user {
201
  border-top-right-radius: 4px;
202
  }
203
+ /* Thinking animation */
204
+ .thinking-brain {
205
+ position: relative;
206
+ width: 32px;
207
+ height: 32px;
208
+ margin-bottom: 4px;
209
+ display: flex;
210
+ align-items: center;
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;
223
+ right: 0;
224
+ bottom: 0;
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;
254
+ }
255
+
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
+ }
269
+
270
+ .thinking-dots-container {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ }
274
+
275
+ .thinking-dot {
276
+ width: 4px;
277
+ height: 4px;
278
+ border-radius: 50%;
279
+ background-color: var(--muted-foreground);
280
+ margin: 0 2px;
281
+ opacity: 0.3;
282
+ transition: opacity 0.2s ease;
283
+ }
284
+
285
+ .thinking-dot-active {
286
+ opacity: 1;
287
+ }
288
+
289
+ /* Variations UI */
290
+ .variations-container {
291
+ margin-top: 0.5rem;
292
+ border-top: 1px solid var(--border);
293
+ padding-top: 0.5rem;
294
+ }
295
+
296
+ .variation-tabs {
297
+ display: flex;
298
+ gap: 0.5rem;
299
+ }
300
+
301
+ .variation-tab {
302
+ font-size: 0.75rem;
303
+ padding: 0.25rem 0.5rem;
304
+ border-radius: 0.25rem;
305
+ border: 1px solid var(--border);
306
+ cursor: pointer;
307
+ transition: background-color 0.2s, border-color 0.2s;
308
+ }
309
+
310
+ .variation-tab:hover {
311
+ background-color: var(--accent);
312
+ }
313
+
314
+ .variation-tab.active {
315
+ background-color: var(--financial-accent);
316
+ color: white;
317
+ border-color: var(--financial-accent);
318
+ }
319
 
320
  .typing-indicator span {
321
  @apply inline-block h-2 w-2 rounded-full bg-current;
 
437
  transform: translateX(100%);
438
  transition: transform 0.5s;
439
  }
440
+
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 {
448
+ 0%, 100% { transform: scale(0.8); opacity: 0.5; }
449
+ 50% { transform: scale(1.2); opacity: 0.8; }
450
+ }
451
+
452
+ .animate-pulse {
453
+ animation: pulse 1.5s infinite ease-in-out;
454
+ }
455
+
456
+ /* Thinking text styles */
457
+ .thinking-text {
458
+ @apply bg-gradient-to-r from-financial-accent to-financial-light-accent bg-clip-text text-transparent px-2 py-1 rounded;
459
+ animation: thinking-glow 2s infinite alternate;
460
+ }
461
+
462
+ @keyframes thinking-glow {
463
+ from { box-shadow: 0 0 5px rgba(var(--accent), 0.2); }
464
+ to { box-shadow: 0 0 15px rgba(var(--accent), 0.5); }
465
+ }
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);
473
+ }
474
+
475
+ .think-block::before {
476
+ content: '';
477
+ position: absolute;
478
+ left: 0;
479
+ top: 0;
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 {
487
+ content: '💭';
488
+ position: absolute;
489
+ right: 10px;
490
+ bottom: 5px;
491
+ opacity: 0.2;
492
+ font-size: 1.5rem;
493
+ }
494
+
495
+ /* Message variations styling */
496
+ .variations-container {
497
+ @apply border-t border-border/30 mt-2 pt-2;
498
+ }
499
+
500
+ .variation-tabs {
501
+ @apply flex gap-1 overflow-x-auto pb-2 scrollbar-thin;
502
+ }
503
+
504
+ .variation-tab {
505
+ @apply text-xs px-3 py-1.5 rounded-full bg-muted/50 text-muted-foreground hover:bg-muted transition-all cursor-pointer flex-shrink-0;
506
+ }
507
+
508
+ .variation-tab.active {
509
+ @apply bg-financial-accent/20 text-financial-accent border border-financial-accent/30 font-medium;
510
+ }
511
+
512
+ @keyframes pulse-border {
513
+ from { opacity: 0.5; }
514
+ to { opacity: 1; }
515
+ }
516
+
517
+ /* Regeneration button pulse effect */
518
+ .regenerate-pulse {
519
+ animation: regenerate-pulse 2s infinite;
520
+ }
521
+
522
+ @keyframes regenerate-pulse {
523
+ 0% {
524
+ box-shadow: 0 0 0 0 rgba(var(--accent), 0.4);
525
+ }
526
+ 70% {
527
+ box-shadow: 0 0 0 6px rgba(var(--accent), 0);
528
+ }
529
+ 100% {
530
+ box-shadow: 0 0 0 0 rgba(var(--accent), 0);
531
+ }
532
+ }
frontend/src/lib/storage.ts CHANGED
@@ -10,6 +10,7 @@ export const STORAGE_KEYS = {
10
  API_ENDPOINT: 'apiEndpoint',
11
  THEME: 'fis-theme',
12
  SOURCES: 'fis-sources',
 
13
  } as const;
14
 
15
  // Types for our storage
@@ -24,24 +25,40 @@ export type StorageValue = string | object | number | boolean | null | undefined
24
  * Currently uses localStorage, but can be extended to use external storage in the future
25
  */
26
  class StorageService {
27
- // Get item from storage with automatic parsing
 
 
 
 
 
 
 
28
  get<T = any>(key: string): T | null {
29
  try {
30
  const item = localStorage.getItem(key);
31
- if (!item) return null;
32
-
33
- // Parse the stored value
 
 
 
 
34
  const { value, expires } = JSON.parse(item);
35
-
36
- // Check if the value has expired
37
  if (expires && expires < Date.now()) {
38
  this.remove(key);
 
 
 
 
39
  return null;
40
  }
41
-
42
  return value as T;
43
  } catch (error) {
44
  console.error(`Error getting item from storage: ${key}`, error);
 
 
 
 
45
  return null;
46
  }
47
  }
@@ -131,7 +148,27 @@ class StorageService {
131
  return false;
132
  }
133
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
135
 
136
  // Create and export a singleton instance
137
  export const storage = new StorageService();
 
 
 
 
 
 
 
 
10
  API_ENDPOINT: 'apiEndpoint',
11
  THEME: 'fis-theme',
12
  SOURCES: 'fis-sources',
13
+ PROFILE_STORAGE_KEY: 'fis-profile'
14
  } as const;
15
 
16
  // Types for our storage
 
25
  * Currently uses localStorage, but can be extended to use external storage in the future
26
  */
27
  class StorageService {
28
+ private defaults: Partial<Record<string, StorageValue>> = {};
29
+
30
+ // Set default values for storage keys
31
+ setDefaults(defaults: Partial<Record<string, StorageValue>>) {
32
+ this.defaults = defaults;
33
+ }
34
+
35
+ // Get item from storage with automatic parsing and default fallback
36
  get<T = any>(key: string): T | null {
37
  try {
38
  const item = localStorage.getItem(key);
39
+ if (!item) {
40
+ // Return default if available
41
+ if (key in this.defaults) {
42
+ return this.defaults[key] as T;
43
+ }
44
+ return null;
45
+ }
46
  const { value, expires } = JSON.parse(item);
 
 
47
  if (expires && expires < Date.now()) {
48
  this.remove(key);
49
+ // Return default if available
50
+ if (key in this.defaults) {
51
+ return this.defaults[key] as T;
52
+ }
53
  return null;
54
  }
 
55
  return value as T;
56
  } catch (error) {
57
  console.error(`Error getting item from storage: ${key}`, error);
58
+ // Return default if available
59
+ if (key in this.defaults) {
60
+ return this.defaults[key] as T;
61
+ }
62
  return null;
63
  }
64
  }
 
148
  return false;
149
  }
150
  }
151
+
152
+ // Reset all keys to their default values
153
+ resetToDefaults(): boolean {
154
+ try {
155
+ Object.entries(this.defaults).forEach(([key, value]) => {
156
+ this.set(key, value);
157
+ });
158
+ return true;
159
+ } catch (error) {
160
+ console.error('Error resetting storage to defaults', error);
161
+ return false;
162
+ }
163
+ }
164
  }
165
 
166
  // Create and export a singleton instance
167
  export const storage = new StorageService();
168
+ storage.setDefaults({
169
+ [STORAGE_KEYS.CHATS]: [],
170
+ [STORAGE_KEYS.SETTINGS]: {},
171
+ [STORAGE_KEYS.API_ENDPOINT]: "https://insight-ai-api.hf.space",
172
+ [STORAGE_KEYS.THEME]: "dark",
173
+ [STORAGE_KEYS.SOURCES]: [],
174
+ });
frontend/src/pages/Index.tsx DELETED
@@ -1,8 +0,0 @@
1
-
2
- import { Navigate } from "react-router-dom";
3
-
4
- const Index = () => {
5
- return <Navigate to="/" replace />;
6
- };
7
-
8
- export default Index;
 
 
 
 
 
 
 
 
 
frontend/src/pages/SourcesPage.tsx DELETED
@@ -1,286 +0,0 @@
1
-
2
- import { useState, useEffect } from "react";
3
- import { FileText, Search, Filter, Calendar, ArrowUpDown, ChevronDown, ChevronUp, ExternalLink } from "lucide-react";
4
- import { Button } from "@/components/ui/button";
5
- import { Input } from "@/components/ui/input";
6
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
7
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
8
- import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
9
- import { Calendar as CalendarComponent } from "@/components/ui/calendar";
10
- import { cn } from "@/lib/utils";
11
- import { Separator } from "@/components/ui/separator";
12
- import { format } from "date-fns";
13
- import { DateRange } from "@/types";
14
- import { RetrievedSource } from "@/services/rulingService";
15
- import { storage, STORAGE_KEYS } from "@/lib/storage";
16
-
17
- const SourcesPage = () => {
18
- const [searchTerm, setSearchTerm] = useState("");
19
- const [selectedSource, setSelectedSource] = useState<string>("");
20
- const [selectedCategory, setSelectedCategory] = useState<string>("");
21
- const [date, setDate] = useState<DateRange>({
22
- from: undefined,
23
- to: undefined,
24
- });
25
- const [sortBy, setSortBy] = useState<string>("date-desc");
26
- const [sources, setSources] = useState<RetrievedSource[]>([]);
27
- const [sourceTypes, setSourceTypes] = useState<string[]>([]);
28
- const [categories, setCategories] = useState<string[]>([]);
29
-
30
- // Load sources from storage
31
- useEffect(() => {
32
- const storedSources = storage.get<RetrievedSource[]>(STORAGE_KEYS.SOURCES) || [];
33
- setSources(storedSources);
34
-
35
- // Extract unique source types and categories
36
- const types = Array.from(new Set(storedSources.map(s => s.metadata?.source).filter(Boolean)));
37
- setSourceTypes(types as string[]);
38
-
39
- const cats = Array.from(new Set(storedSources.map(s => {
40
- // For this example, we'll use the first word of the content as a mock category
41
- // In a real app, you'd use a proper category field from metadata
42
- const firstWord = s.content_snippet.split(' ')[0];
43
- return firstWord.length > 3 ? firstWord : "General";
44
- })));
45
- setCategories(cats);
46
- }, []);
47
-
48
- // Filter sources based on search term, source, category, and date
49
- const filteredSources = sources.filter(source => {
50
- const matchesSearch = !searchTerm ||
51
- source.content_snippet.toLowerCase().includes(searchTerm.toLowerCase()) ||
52
- (source.metadata?.source || "").toLowerCase().includes(searchTerm.toLowerCase());
53
-
54
- const matchesSource = !selectedSource || source.metadata?.source === selectedSource;
55
-
56
- // Mock category matching based on first word of content
57
- const sourceCategory = source.content_snippet.split(' ')[0].length > 3 ?
58
- source.content_snippet.split(' ')[0] : "General";
59
- const matchesCategory = !selectedCategory || sourceCategory === selectedCategory;
60
-
61
- let matchesDate = true;
62
- if (date.from && source.metadata?.ruling_date) {
63
- matchesDate = matchesDate && new Date(source.metadata.ruling_date) >= date.from;
64
- }
65
- if (date.to && source.metadata?.ruling_date) {
66
- matchesDate = matchesDate && new Date(source.metadata.ruling_date) <= date.to;
67
- }
68
-
69
- return matchesSearch && matchesSource && matchesCategory && matchesDate;
70
- });
71
-
72
- // Sort sources
73
- const sortedSources = [...filteredSources].sort((a, b) => {
74
- if (sortBy === "date-desc") {
75
- return new Date(b.metadata?.ruling_date || "").getTime() -
76
- new Date(a.metadata?.ruling_date || "").getTime();
77
- } else if (sortBy === "date-asc") {
78
- return new Date(a.metadata?.ruling_date || "").getTime() -
79
- new Date(b.metadata?.ruling_date || "").getTime();
80
- } else if (sortBy === "relevance-desc") {
81
- return b.content_snippet.length - a.content_snippet.length;
82
- }
83
- return 0;
84
- });
85
-
86
- const resetFilters = () => {
87
- setSearchTerm("");
88
- setSelectedSource("");
89
- setSelectedCategory("");
90
- setDate({ from: undefined, to: undefined });
91
- };
92
-
93
- const [expandedSources, setExpandedSources] = useState<Record<number, boolean>>({});
94
-
95
- const toggleSource = (index: number) => {
96
- setExpandedSources(prev => ({
97
- ...prev,
98
- [index]: !prev[index]
99
- }));
100
- };
101
-
102
- return (
103
- <div className="container mx-auto px-4 py-8 animate-fade-in">
104
- <div className="flex flex-col md:flex-row md:items-center justify-between mb-6">
105
- <h1 className="text-3xl font-bold mb-4 md:mb-0 text-financial-navy dark:text-white bg-clip-text text-transparent bg-gradient-to-r from-financial-accent to-financial-light-accent">
106
- Financial Sources
107
- </h1>
108
-
109
- <div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2">
110
- <Select value={sortBy} onValueChange={setSortBy}>
111
- <SelectTrigger className="w-[180px] glass-effect">
112
- <div className="flex items-center">
113
- <ArrowUpDown className="mr-2 h-3.5 w-3.5 text-muted-foreground" />
114
- <span>Sort By</span>
115
- </div>
116
- </SelectTrigger>
117
- <SelectContent>
118
- <SelectItem value="date-desc">Date (Newest)</SelectItem>
119
- <SelectItem value="date-asc">Date (Oldest)</SelectItem>
120
- <SelectItem value="relevance-desc">Relevance</SelectItem>
121
- </SelectContent>
122
- </Select>
123
- </div>
124
- </div>
125
-
126
- <div className="glass-effect rounded-lg p-6 mb-8 shadow-lg">
127
- <div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2">
128
- <div className="relative flex-1">
129
- <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
130
- <Input
131
- type="text"
132
- placeholder="Search sources..."
133
- value={searchTerm}
134
- onChange={(e) => setSearchTerm(e.target.value)}
135
- className="pl-10 glass-input"
136
- />
137
- </div>
138
-
139
- <Select value={selectedSource} onValueChange={setSelectedSource}>
140
- <SelectTrigger className="w-full md:w-[180px] glass-input">
141
- <span className="truncate">
142
- {selectedSource || "All Sources"}
143
- </span>
144
- </SelectTrigger>
145
- <SelectContent>
146
- <SelectItem value="All">All Sources</SelectItem>
147
- {sourceTypes.map(type => (
148
- <SelectItem key={type} value={type}>{type}</SelectItem>
149
- ))}
150
- </SelectContent>
151
- </Select>
152
-
153
- <Select value={selectedCategory} onValueChange={setSelectedCategory}>
154
- <SelectTrigger className="w-full md:w-[180px] glass-input">
155
- <span className="truncate">
156
- {selectedCategory || "All Categories"}
157
- </span>
158
- </SelectTrigger>
159
- <SelectContent>
160
- <SelectItem value="All">All Categories</SelectItem>
161
- {categories.map(category => (
162
- <SelectItem key={category} value={category}>{category}</SelectItem>
163
- ))}
164
- </SelectContent>
165
- </Select>
166
-
167
- <Popover>
168
- <PopoverTrigger asChild>
169
- <Button variant="outline" className="w-full md:w-[180px] justify-start text-left glass-input">
170
- <Calendar className="mr-2 h-4 w-4" />
171
- <span>
172
- {date.from || date.to ? (
173
- <>
174
- {date.from ? format(date.from, "LLL dd, y") : "From"} - {" "}
175
- {date.to ? format(date.to, "LLL dd, y") : "To"}
176
- </>
177
- ) : (
178
- "Date Range"
179
- )}
180
- </span>
181
- </Button>
182
- </PopoverTrigger>
183
- <PopoverContent className="w-auto p-0">
184
- <CalendarComponent
185
- mode="range"
186
- selected={date}
187
- onSelect={(value: DateRange | undefined) => {
188
- if (value) setDate(value);
189
- }}
190
- className={cn("p-3 pointer-events-auto")}
191
- />
192
- </PopoverContent>
193
- </Popover>
194
-
195
- <Button
196
- variant="outline"
197
- className="md:w-auto glass-input"
198
- onClick={resetFilters}
199
- >
200
- Reset
201
- </Button>
202
- </div>
203
- </div>
204
-
205
- <div className="space-y-4">
206
- {sortedSources.length > 0 ? (
207
- sortedSources.map((source, index) => (
208
- <Card key={index} className="result-card glass-effect overflow-hidden transition-all duration-300">
209
- <CardHeader className="pb-2">
210
- <div className="flex items-start justify-between">
211
- <div>
212
- <CardTitle className="text-lg text-gradient">
213
- {source.metadata?.source || "Financial Ruling"}
214
- </CardTitle>
215
- <div className="flex items-center mt-1 text-sm text-muted-foreground">
216
- <FileText className="h-3.5 w-3.5 mr-1.5" />
217
- <span>{source.metadata?.source || "Unknown Source"}</span>
218
- {source.metadata?.ruling_date && (
219
- <>
220
- <span className="mx-1.5">•</span>
221
- <Calendar className="h-3.5 w-3.5 mr-1.5" />
222
- <span>{new Date(source.metadata.ruling_date).toLocaleDateString()}</span>
223
- </>
224
- )}
225
- </div>
226
- </div>
227
- <Button
228
- variant="ghost"
229
- size="sm"
230
- onClick={() => toggleSource(index)}
231
- className="p-0 h-8 w-8 hover:bg-accent/10"
232
- >
233
- {expandedSources[index] ? (
234
- <ChevronUp className="h-4 w-4" />
235
- ) : (
236
- <ChevronDown className="h-4 w-4" />
237
- )}
238
- </Button>
239
- </div>
240
- </CardHeader>
241
- <CardContent className={expandedSources[index] ? "pb-2" : "max-h-24 overflow-hidden relative"}>
242
- <p className="text-sm text-muted-foreground">
243
- {expandedSources[index]
244
- ? source.content_snippet
245
- : source.content_snippet.substring(0, 150) + "..."}
246
- </p>
247
- {!expandedSources[index] && (
248
- <div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-card to-transparent"></div>
249
- )}
250
- </CardContent>
251
- {expandedSources[index] && (
252
- <CardFooter className="pt-0 text-sm flex justify-end">
253
- <Button variant="link" size="sm" className="h-8 p-0 text-financial-accent">
254
- <ExternalLink className="h-3 w-3 mr-1" />
255
- View source
256
- </Button>
257
- </CardFooter>
258
- )}
259
- </Card>
260
- ))
261
- ) : (
262
- <div className="glass-effect rounded-lg p-8 text-center shadow-lg">
263
- <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-muted/30 flex items-center justify-center">
264
- <FileText className="h-8 w-8 text-muted-foreground" />
265
- </div>
266
- <h3 className="text-lg font-medium mb-2 text-gradient">No sources found</h3>
267
- <p className="text-muted-foreground">
268
- {sources.length > 0
269
- ? "No sources match your current search criteria. Try adjusting your filters."
270
- : "Chat with Insight AI to get information with source citations."}
271
- </p>
272
- <Button
273
- variant="outline"
274
- className="mt-4 glass-input"
275
- onClick={resetFilters}
276
- >
277
- Reset Filters
278
- </Button>
279
- </div>
280
- )}
281
- </div>
282
- </div>
283
- );
284
- };
285
-
286
- export default SourcesPage;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/services/{rulingService.ts → apiService.ts} RENAMED
@@ -32,25 +32,22 @@ export interface QueryRequest {
32
  query: string;
33
  chat_history?: Message[];
34
  filters?: Record<string, any>;
 
35
  }
36
 
37
  // Add a delay function for better UX when showing loading states
38
  const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
39
 
40
- export const rulingService = {
41
  async getApiUrl(): Promise<string> {
42
- // Get API URL from storage or use default
43
- let API_URL = "http://localhost:8000";
44
  const storedUrl = storage.get<string>(STORAGE_KEYS.API_ENDPOINT);
45
 
46
  if (storedUrl) {
47
- API_URL = storedUrl;
48
  } else {
49
  // Set default if not found
50
- storage.set(STORAGE_KEYS.API_ENDPOINT, API_URL);
51
  }
52
-
53
- return API_URL;
54
  },
55
 
56
  async queryRulings(request: QueryRequest): Promise<QueryResponse> {
@@ -60,6 +57,12 @@ export const rulingService = {
60
 
61
  const API_URL = await this.getApiUrl();
62
 
 
 
 
 
 
 
63
  const response = await fetch(`${API_URL}/query`, {
64
  method: "POST",
65
  headers: {
@@ -101,8 +104,8 @@ export const rulingService = {
101
 
102
  return result;
103
  } catch (error) {
104
- console.error("Failed to query rulings:", error);
105
- const errorMessage = error instanceof Error ? error.message : "Failed to query rulings";
106
  toast.error(errorMessage);
107
  throw error;
108
  }
@@ -112,6 +115,11 @@ export const rulingService = {
112
  try {
113
  const API_URL = await this.getApiUrl();
114
 
 
 
 
 
 
115
  const response = await fetch(`${API_URL}/generate-title`, {
116
  method: "POST",
117
  headers: {
 
32
  query: string;
33
  chat_history?: Message[];
34
  filters?: Record<string, any>;
35
+ regenerate?: boolean;
36
  }
37
 
38
  // Add a delay function for better UX when showing loading states
39
  const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
40
 
41
+ export const apiService = {
42
  async getApiUrl(): Promise<string> {
 
 
43
  const storedUrl = storage.get<string>(STORAGE_KEYS.API_ENDPOINT);
44
 
45
  if (storedUrl) {
46
+ return storedUrl;
47
  } else {
48
  // Set default if not found
49
+ return null;
50
  }
 
 
51
  },
52
 
53
  async queryRulings(request: QueryRequest): Promise<QueryResponse> {
 
57
 
58
  const API_URL = await this.getApiUrl();
59
 
60
+ if (!API_URL) {
61
+ return {
62
+ answer: "<think>The user hasn't configured an API endpoint yet. I should provide a helpful response.</think>\n\nIt looks like you haven't configured the API endpoint yet. Please go to Settings and enter your API URL to connect to your data source."
63
+ };
64
+ }
65
+
66
  const response = await fetch(`${API_URL}/query`, {
67
  method: "POST",
68
  headers: {
 
104
 
105
  return result;
106
  } catch (error) {
107
+ console.error("Failed to query AI:", error);
108
+ const errorMessage = error instanceof Error ? error.message : "Failed to get a response";
109
  toast.error(errorMessage);
110
  throw error;
111
  }
 
115
  try {
116
  const API_URL = await this.getApiUrl();
117
 
118
+ // If no API URL is configured, return a generic title
119
+ if (!API_URL) {
120
+ return { title: query.slice(0, 30) + (query.length > 30 ? '...' : '') };
121
+ }
122
+
123
  const response = await fetch(`${API_URL}/generate-title`, {
124
  method: "POST",
125
  headers: {
frontend/src/types/chat.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ export interface Message {
3
+ id: string;
4
+ content: string;
5
+ sender: "user" | "system";
6
+ timestamp: Date;
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 {
15
+ id: string;
16
+ title: string;
17
+ messages: Message[];
18
+ createdAt: Date;
19
+ updatedAt: Date;
20
+ }