Paramjit Singh commited on
Commit
c66ac65
·
unverified ·
2 Parent(s): 3e08504831cbae

Merge pull request #252 from nishantchaubeyy/NIshuCommits

Browse files
frontend/src/components/chat/SourceCard.tsx CHANGED
@@ -13,6 +13,77 @@ import { ChevronDown, ChevronUp, FileText, Eye, TextQuote } from "lucide-react";
13
 
14
  const EXCERPT_THRESHOLD = 200;
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  interface Props {
17
  sources: SourceChunk[];
18
  onPageClick: (page: number) => void;
@@ -36,34 +107,37 @@ export default function SourceCard({ sources = [], onPageClick }: Props) {
36
 
37
  return (
38
  <div className="rounded-lg border border-border/50 bg-card/50 overflow-hidden">
39
- {/* ── Header ──────────────────────────────────── */}
40
- <button
41
- onClick={() => setExpanded(!expanded)}
42
- className="w-full flex items-center justify-between px-3 py-2 text-xs hover:bg-accent/30 transition-colors"
43
- >
44
- <span className="flex items-center gap-1.5 text-muted-foreground">
45
- <FileText className="w-3.5 h-3.5" />
46
- {sources.length} source{sources.length > 1 ? "s" : ""} cited
47
- </span>
48
- {expanded ? (
49
- <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" />
50
- ) : (
51
- <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
52
- )}
53
- </button>
54
-
55
- {/* ── Collapsed: Mini badges with hover preview ── */}
56
- {!expanded && (
57
- <div className="px-3 pb-2 flex flex-wrap gap-1">
58
- {sources.map((src, i) => (
 
 
 
59
  <Tooltip key={i}>
60
  <TooltipTrigger className="inline-flex">
61
  <Badge
62
- variant="secondary"
63
- className="text-[10px] h-5 cursor-pointer hover:bg-primary/20 transition-colors"
64
  onClick={() => onPageClick(src.page + 1)}
65
  >
66
- p.{src.page + 1} {src.confidence}%
67
  </Badge>
68
  </TooltipTrigger>
69
  <TooltipContent
@@ -71,74 +145,68 @@ export default function SourceCard({ sources = [], onPageClick }: Props) {
71
  align="center"
72
  className="max-w-xs p-2"
73
  >
 
 
 
 
74
  <p className="text-[11px] leading-relaxed line-clamp-6">
75
  {src.text}
76
  </p>
77
  </TooltipContent>
78
  </Tooltip>
79
- ))}
80
- </div>
81
- )}
 
82
 
83
- {/* ── Expanded: Full source cards ─────────────── */}
84
- {expanded && (
85
- <div className="border-t border-border/30">
86
- {sources.map((src, i) => (
87
- <div
88
- key={i}
89
- className="px-3 py-2.5 border-b border-border/20 last:border-b-0 hover:bg-accent/20 transition-colors"
90
- >
91
- <div className="flex items-center justify-between mb-1.5">
92
- <div className="flex items-center gap-2">
93
- <span className="text-[10px] font-medium text-muted-foreground">
94
- {src.filename}
95
- </span>
96
- <Badge variant="outline" className="text-[9px] h-4 px-1.5">
97
- Page {src.page + 1}
98
- </Badge>
99
- <Badge
100
- variant="secondary"
101
- className={`text-[9px] h-4 px-1.5 ${
102
- src.confidence >= 80
103
- ? "text-emerald-400 bg-emerald-400/10"
104
- : src.confidence >= 50
105
- ? "text-yellow-400 bg-yellow-400/10"
106
- : "text-muted-foreground"
107
- }`}
108
- >
109
- {src.confidence}% match
110
- </Badge>
111
- </div>
112
- <Button
113
- variant="ghost"
114
- size="sm"
115
- className="h-6 px-2 text-[10px]"
116
- onClick={() => onPageClick(src.page + 1)}
117
- >
118
- <Eye className="w-3 h-3 mr-1" />
119
- View
120
- </Button>
121
  </div>
122
- <p
123
- className={`text-[11px] text-muted-foreground leading-relaxed ${
124
- excerptOpen.has(i) ? "" : "line-clamp-3"
125
- }`}
 
126
  >
127
- {src.text}
128
- </p>
129
- {src.text.length > EXCERPT_THRESHOLD && (
130
- <button
131
- onClick={() => toggleExcerpt(i)}
132
- className="mt-1.5 flex items-center gap-1 text-[10px] text-primary/70 hover:text-primary transition-colors"
133
- >
134
- <TextQuote className="w-3 h-3" />
135
- {excerptOpen.has(i) ? "Hide excerpt" : "Show excerpt"}
136
- </button>
137
- )}
138
  </div>
139
- ))}
140
- </div>
141
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  </div>
143
  );
144
  }
 
13
 
14
  const EXCERPT_THRESHOLD = 200;
15
 
16
+ type ConfidenceLevel = "High" | "Medium" | "Low" | "Unknown";
17
+
18
+ interface ConfidenceBadgeMeta {
19
+ label: ConfidenceLevel;
20
+ className: string;
21
+ }
22
+
23
+ const normalizeMetricValue = (value?: number) => {
24
+ if (typeof value !== "number" || Number.isNaN(value)) return undefined;
25
+ return value > 1 ? value / 100 : value;
26
+ };
27
+
28
+ const formatMetricValue = (value?: number) => {
29
+ const normalizedValue = normalizeMetricValue(value);
30
+ if (normalizedValue === undefined) return "N/A";
31
+ return `${Math.round(normalizedValue * 100)}%`;
32
+ };
33
+
34
+ const getConfidenceBadgeMeta = (value?: number): ConfidenceBadgeMeta => {
35
+ const normalizedValue = normalizeMetricValue(value);
36
+
37
+ if (normalizedValue === undefined) {
38
+ return {
39
+ label: "Unknown",
40
+ className: "border-muted bg-muted/40 text-muted-foreground",
41
+ };
42
+ }
43
+
44
+ if (normalizedValue >= 0.8) {
45
+ return {
46
+ label: "High",
47
+ className: "border-emerald-500/30 bg-emerald-500/10 text-emerald-600",
48
+ };
49
+ }
50
+
51
+ if (normalizedValue >= 0.5) {
52
+ return {
53
+ label: "Medium",
54
+ className: "border-amber-500/30 bg-amber-500/10 text-amber-600",
55
+ };
56
+ }
57
+
58
+ return {
59
+ label: "Low",
60
+ className: "border-red-500/30 bg-red-500/10 text-red-600",
61
+ };
62
+ };
63
+
64
+ const getPrimarySourceMetric = (source: SourceChunk) =>
65
+ source.confidence ?? source.score;
66
+
67
+ const MetricBadge = ({
68
+ label,
69
+ value,
70
+ }: {
71
+ label: "Score" | "Confidence";
72
+ value?: number;
73
+ }) => {
74
+ const badgeMeta = getConfidenceBadgeMeta(value);
75
+
76
+ return (
77
+ <Badge
78
+ variant="outline"
79
+ className={`h-5 px-1.5 text-[9px] font-medium ${badgeMeta.className}`}
80
+ title={`${label}: ${formatMetricValue(value)}`}
81
+ >
82
+ {label}: {badgeMeta.label}
83
+ </Badge>
84
+ );
85
+ };
86
+
87
  interface Props {
88
  sources: SourceChunk[];
89
  onPageClick: (page: number) => void;
 
107
 
108
  return (
109
  <div className="rounded-lg border border-border/50 bg-card/50 overflow-hidden">
110
+ <button
111
+ onClick={() => setExpanded(!expanded)}
112
+ className="w-full flex items-center justify-between px-3 py-2 text-xs hover:bg-accent/30 transition-colors"
113
+ >
114
+ <span className="flex items-center gap-1.5 text-muted-foreground">
115
+ <FileText className="w-3.5 h-3.5" />
116
+ {sources.length} source{sources.length > 1 ? "s" : ""} cited
117
+ </span>
118
+ {expanded ? (
119
+ <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" />
120
+ ) : (
121
+ <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
122
+ )}
123
+ </button>
124
+
125
+ {!expanded && (
126
+ <div className="px-3 pb-2 flex flex-wrap gap-1">
127
+ {sources.map((src, i) => {
128
+ const badgeMeta = getConfidenceBadgeMeta(
129
+ getPrimarySourceMetric(src)
130
+ );
131
+
132
+ return (
133
  <Tooltip key={i}>
134
  <TooltipTrigger className="inline-flex">
135
  <Badge
136
+ variant="outline"
137
+ className={`text-[10px] h-5 cursor-pointer hover:bg-primary/20 transition-colors ${badgeMeta.className}`}
138
  onClick={() => onPageClick(src.page + 1)}
139
  >
140
+ p.{src.page + 1} - {badgeMeta.label}
141
  </Badge>
142
  </TooltipTrigger>
143
  <TooltipContent
 
145
  align="center"
146
  className="max-w-xs p-2"
147
  >
148
+ <div className="mb-1 flex flex-wrap gap-1">
149
+ <MetricBadge label="Score" value={src.score} />
150
+ <MetricBadge label="Confidence" value={src.confidence} />
151
+ </div>
152
  <p className="text-[11px] leading-relaxed line-clamp-6">
153
  {src.text}
154
  </p>
155
  </TooltipContent>
156
  </Tooltip>
157
+ );
158
+ })}
159
+ </div>
160
+ )}
161
 
162
+ {expanded && (
163
+ <div className="border-t border-border/30">
164
+ {sources.map((src, i) => (
165
+ <div
166
+ key={i}
167
+ className="px-3 py-2.5 border-b border-border/20 last:border-b-0 hover:bg-accent/20 transition-colors"
168
+ >
169
+ <div className="flex items-center justify-between gap-2 mb-1.5">
170
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
171
+ <span className="truncate text-[10px] font-medium text-muted-foreground">
172
+ {src.filename}
173
+ </span>
174
+ <Badge variant="outline" className="h-5 px-1.5 text-[9px]">
175
+ Page {src.page + 1}
176
+ </Badge>
177
+ <MetricBadge label="Score" value={src.score} />
178
+ <MetricBadge label="Confidence" value={src.confidence} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  </div>
180
+ <Button
181
+ variant="ghost"
182
+ size="sm"
183
+ className="h-6 shrink-0 px-2 text-[10px]"
184
+ onClick={() => onPageClick(src.page + 1)}
185
  >
186
+ <Eye className="w-3 h-3 mr-1" />
187
+ View
188
+ </Button>
 
 
 
 
 
 
 
 
189
  </div>
190
+ <p
191
+ className={`text-[11px] text-muted-foreground leading-relaxed ${
192
+ excerptOpen.has(i) ? "" : "line-clamp-3"
193
+ }`}
194
+ >
195
+ {src.text}
196
+ </p>
197
+ {src.text.length > EXCERPT_THRESHOLD && (
198
+ <button
199
+ onClick={() => toggleExcerpt(i)}
200
+ className="mt-1.5 flex items-center gap-1 text-[10px] text-primary/70 hover:text-primary transition-colors"
201
+ >
202
+ <TextQuote className="w-3 h-3" />
203
+ {excerptOpen.has(i) ? "Hide excerpt" : "Show excerpt"}
204
+ </button>
205
+ )}
206
+ </div>
207
+ ))}
208
+ </div>
209
+ )}
210
  </div>
211
  );
212
  }
frontend/src/store/chat-store.ts CHANGED
@@ -7,8 +7,8 @@ export interface SourceChunk {
7
  text: string;
8
  filename: string;
9
  page: number;
10
- score: number;
11
- confidence: number;
12
  }
13
 
14
  export interface ChatMsg {
 
7
  text: string;
8
  filename: string;
9
  page: number;
10
+ score?: number;
11
+ confidence?: number;
12
  }
13
 
14
  export interface ChatMsg {