File size: 14,398 Bytes
e078b1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55729b3
d703e0b
e078b1d
 
55729b3
e078b1d
e58b2bd
55729b3
 
 
e078b1d
 
 
 
e58b2bd
55729b3
e078b1d
 
 
55729b3
 
 
e078b1d
 
 
 
 
 
 
 
e58b2bd
 
55729b3
 
e078b1d
55729b3
e078b1d
 
 
 
55729b3
e078b1d
 
d703e0b
e078b1d
55729b3
e078b1d
55729b3
e078b1d
 
 
 
 
 
 
5fe6799
e078b1d
 
 
 
 
55729b3
e078b1d
 
7976e9d
e078b1d
55729b3
e078b1d
 
 
7976e9d
e078b1d
 
 
 
 
 
 
 
 
55729b3
e078b1d
 
55729b3
e078b1d
 
5fe6799
e078b1d
55729b3
e078b1d
 
 
55729b3
 
 
e078b1d
 
 
 
e58b2bd
 
 
e078b1d
 
 
 
 
 
 
a543f4f
 
 
 
e078b1d
a543f4f
 
e078b1d
 
 
5fe6799
55729b3
e078b1d
 
55729b3
5fe6799
e078b1d
 
55729b3
e078b1d
 
 
 
 
 
 
 
 
 
 
55729b3
e078b1d
 
 
55729b3
e078b1d
 
55729b3
e078b1d
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
import { useState, useMemo } from "react";
import { Copy, UploadCloud, FileText, Zap, Cpu, Sun, Layers, ArrowRight, Download, CheckCircle2 } from "lucide-react";

export const MODELS = [
  {
    id: "bart_large_cnn",
    name: "BART",
    badgeLabel: "Top Pick",
    badgeColor: "text-orange-600 bg-orange-100 border-orange-200 dark:text-orange-400 dark:border-orange-500/30 dark:bg-orange-500/10",
    speed: 2,
    icon: Cpu,
    description: "Meta's BART model fine-tuned on CNN/DailyMail. It produces highly abstractive, narrative-style summaries, making it the best overall for rewriting raw incident descriptions into fluent reports."
  },
  {
    id: "flan_t5_small",
    name: "Flan-T5",
    badgeLabel: "Fast",
    badgeColor: "text-blue-600 bg-blue-100 border-blue-200 dark:text-blue-400 dark:border-blue-500/30 dark:bg-blue-500/10",
    speed: 3,
    icon: Zap,
    description: "Google's instruction-tuned T5 model. It's incredibly fast and lightweight to run locally on CPU, though its summaries can occasionally be more concise and rigid than BART."
  },
  {
    id: "pegasus_cnn",
    name: "PEGASUS",
    badgeLabel: "Precise",
    badgeColor: "text-emerald-600 bg-emerald-100 border-emerald-200 dark:text-emerald-400 dark:border-emerald-500/30 dark:bg-emerald-500/10",
    speed: 1,
    icon: Layers,
    description: "Google's PEGASUS model designed specifically for summarization. It is highly precise but computationally heavy, resulting in slower generation times."
  },
  {
    id: "textrank",
    name: "TextRank",
    badgeLabel: "Offline",
    badgeColor: "text-purple-600 bg-purple-100 border-purple-200 dark:text-purple-400 dark:border-purple-500/30 dark:bg-purple-500/10",
    speed: 3,
    icon: Sun,
    description: "A classic non-neural extractive model based on PageRank. It works entirely offline without a GPU by identifying and extracting the most important exact sentences from the text."
  }
];

function downloadTextFile(filename, content) {
  const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  link.remove();
  URL.revokeObjectURL(url);
}

function extractTags(text) {
  if (!text) return [];
  try {
    const tags = [];

    // Vehicles
    const vehicleMatch = text.match(/\b(\d+|two|three|four|five|several|multiple)\s+(?:vehicles?|cars?|trucks?)\b/i);
    if (vehicleMatch) tags.push(vehicleMatch[1].toLowerCase() + " vehicles");

    // Duration (minutes)
    const minMatch = text.match(/\b(\d+)\s*(?:min|minutes?)\b/i);
    if (minMatch) tags.push(minMatch[1] + " min");

    // Distance (miles/km)
    const miMatch = text.match(/\b(\d+(?:\.\d+)?)\s*(?:miles?|mi|km)\b/i);
    if (miMatch) tags.push(miMatch[1] + " mi backup");

    // Road codes like D71, E11
    const codeMatches = text.match(/\b[A-Z]\d{1,3}\b/g);
    if (codeMatches) {
      codeMatches.forEach(c => { if (!tags.includes(c)) tags.push(c); });
    }

    // Named roads (safe version)
    const roadMatch = text.match(/\b(?:Sheikh\s+Zayed|Hessa|Al\s+Khail|E[0-9]+|Highway\s+\d+|I-\d+|Route\s+\d+)[^,.]*(?:Road|Rd|Street|St|Hwy)?\b/gi);
    if (roadMatch) {
      const shortRoad = roadMatch[0].trim().split(' ').slice(0, 3).join(' ');
      if (!tags.some(t => t.toLowerCase() === shortRoad.toLowerCase())) tags.push(shortRoad);
    }

    return Array.from(new Set(tags)).slice(0, 4);
  } catch (_) {
    return [];
  }
}

export default function SummarizerWidget({
  text,
  setText,
  modelChoice,
  setModelChoice,
  onSummarize,
  loading,
  summary
}) {
  const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
  const summaryWordCount = summary ? summary.split(/\s+/).filter(Boolean).length : 0;

  const [copied, setCopied] = useState(false);
  
  const extractedTags = useMemo(() => {
    return summary ? extractTags(summary + " " + text) : [];
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [summary]);

  const handleCopy = () => {
    if (!summary) return;
    navigator.clipboard.writeText(summary);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div className="w-full h-full flex flex-col">
      <div className="rounded-2xl border border-slate-300 dark:border-white/[0.07] bg-white dark:bg-[#0d1326] shadow-sm dark:shadow-2xl flex-1 flex flex-col">
        
        {/* Input and Output Split Area */}
        <div className="grid gap-0 lg:grid-cols-2 flex-1">
           {/* LEFT PANE: INPUT */}
           <div className="p-4 md:p-5 flex flex-col relative min-h-[320px]">
             <div className="flex items-center justify-between border-b border-slate-100 dark:border-white/[0.05] pb-2.5 mb-3">
               <h3 className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500">Original Incident</h3>
               <span className="rounded-full bg-slate-100 dark:bg-white/[0.05] px-2.5 py-0.5 text-[10px] font-bold text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-white/[0.06]">
                 {wordCount} words
               </span>
             </div>
             <textarea
               className="w-full resize-none bg-transparent p-0 text-lg leading-[1.85] text-slate-800 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-600 focus:outline-none focus:ring-0 flex-1"
               placeholder="Paste a traffic incident report here, or click a sample on the right..."
               value={text}
               onChange={(e) => setText(e.target.value)}
             />
             <div className="mt-2.5 pt-2.5 border-t border-slate-100 dark:border-white/[0.05] border-dashed">
                <button
                  className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-slate-700 dark:hover:text-slate-300 transition-colors"
                  onClick={() => setText("")}
                >
                  Clear Text
                </button>
             </div>
           </div>

           {/* RIGHT PANE: OUTPUT */}
           <div className="p-4 md:p-5 flex flex-col relative border-t lg:border-t-0 lg:border-l border-slate-100 dark:border-white/[0.05] bg-slate-50/50 dark:bg-white/[0.01] rounded-b-2xl lg:rounded-bl-none lg:rounded-tr-2xl min-h-[320px]">

             <div className="flex items-center justify-between border-b border-orange-300/40 dark:border-orange-400/20 pb-2.5 mb-3">
               <h3 className="text-xs font-bold uppercase tracking-[0.2em] text-orange-500 dark:text-orange-400">Generated Output · {modelChoice.replace(/_/g, ' ').toUpperCase()}</h3>
               {summary ? (
                 <span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-500/20 text-[9px] font-bold text-orange-400 border border-orange-500/30">
                   {summaryWordCount}
                 </span>
               ) : null}
             </div>
             <div className="flex flex-col custom-scroll">
                {summary ? (
                    <>
                      <p className="text-lg leading-[1.85] text-slate-800 dark:text-slate-100 whitespace-pre-wrap">{summary.replace(/<n>/gi, '\n\n').replace(/[ \t]+/g, ' ').trim()}</p>
                      {extractedTags.length > 0 && (
                        <div className="mt-3 flex flex-wrap gap-1.5">
                          {extractedTags.map((tag, idx) => (
                            <span key={idx} className="inline-flex items-center rounded-full border border-orange-500/20 bg-orange-500/5 px-2.5 py-0.5 text-[10px] font-bold text-orange-400 capitalize tracking-wide">
                              {tag}
                            </span>
                          ))}
                        </div>
                      )}
                    </>
                ) : (
                    <div className="h-full flex-1 flex items-center justify-center text-sm font-medium text-slate-400 dark:text-slate-600 italic">
                       {loading ? "Generating summary..." : "No summary generated yet."}
                    </div>
                )}
             </div>
             {summary && (
                <div className="mt-5 flex flex-wrap gap-3 border-t border-white/5 pt-4">
                  <button
                    onClick={handleCopy}
                    className="flex-1 flex justify-center items-center gap-2 rounded-xl bg-slate-100 dark:bg-white/[0.08] hover:bg-slate-200 dark:hover:bg-white/[0.12] py-3 text-sm font-bold text-slate-700 dark:text-white transition border border-slate-200 dark:border-white/10"
                  >
                    {copied ? <CheckCircle2 size={16}/> : <Copy size={16} />} {copied ? "Copied" : "Copy Summary"}
                  </button>
                  <button
                    onClick={() => downloadTextFile(`summary_${modelChoice}.txt`, summary)}
                    className="flex-1 flex justify-center items-center gap-2 rounded-xl border border-slate-200 dark:border-white/10 bg-white dark:bg-transparent py-3 text-sm font-bold text-slate-600 dark:text-slate-300 transition hover:bg-slate-50 dark:hover:bg-white/[0.05]"
                  >
                    <Download size={16} /> Save
                  </button>
                </div>
             )}
           </div>
        </div>

        {/* Separator */}
        <div className="h-px w-full bg-slate-100 dark:bg-white/[0.05]"></div>

        {/* Controls block */}
        <div className="p-4 md:p-6 space-y-5">
            {/* Model Selection */}
            <div className="space-y-4">
              <h3 className="text-xs font-bold text-slate-500 dark:text-slate-500 uppercase tracking-widest">Select Model</h3>

              <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
                {MODELS.map((model) => {
                  const Icon = model.icon;
                  const isSelected = modelChoice === model.id;
                  const cardCls = isSelected
                    ? 'ring-2 ring-orange-500 bg-orange-50 dark:bg-orange-500/[0.08] border-orange-400 dark:border-orange-500/40'
                    : 'bg-slate-50 dark:bg-white/[0.03] hover:bg-slate-100 dark:hover:bg-white/[0.06] border-slate-200 dark:border-white/[0.08] hover:border-slate-300 dark:hover:border-white/15';
                  
                  return (
                    <button
                      key={model.id}
                      onClick={() => !loading && setModelChoice(model.id)}
                      disabled={loading}
                      className={`relative flex flex-col items-start rounded-xl border p-4 text-left transition-all ${cardCls} ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
                    >
                      {isSelected && <div className="absolute top-4 right-4 h-2 w-2 rounded-full bg-orange-500 shadow-[0_0_8px_rgba(249,115,22,0.6)]" />}
                      <div className="flex w-full items-start justify-between mb-4">
                        <span className="flex h-10 w-10 items-center justify-center rounded-xl bg-white border border-slate-200 text-slate-700 shadow-sm dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300">
                          <Icon size={18} />
                        </span>
                        <div className="group/tooltip relative">
                           <div className="flex h-6 w-6 items-center justify-center rounded-full hover:bg-orange-50 dark:hover:bg-white/10 transition cursor-help">
                             <span className="font-serif italic border-2 border-slate-400 dark:border-slate-500 text-slate-500 dark:text-slate-400 hover:border-orange-500 hover:text-orange-600 dark:hover:text-white rounded-full w-4 h-4 flex items-center justify-center text-[10px] transition">
                               i
                             </span>
                           </div>
                           <div className="absolute right-0 lg:right-auto lg:left-0 top-8 z-50 w-64 opacity-0 scale-95 origin-top-right lg:origin-top-left transition-all group-hover/tooltip:opacity-100 group-hover/tooltip:scale-100 pointer-events-none group-hover/tooltip:pointer-events-auto rounded-xl bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 p-3 shadow-xl">
                             <p className="text-xs text-slate-700 dark:text-slate-300 leading-relaxed font-normal">{model.description}</p>
                           </div>
                        </div>
                      </div>
                      <h4 className="text-base font-bold text-slate-900 dark:text-white mb-1.5">{model.name}</h4>
                      <span className={`inline-block rounded-full border px-2 py-0.5 text-[9px] font-bold uppercase tracking-wider ${model.badgeColor}`}>
                        {model.badgeLabel}
                      </span>
                      <div className="mt-4 flex w-full items-center justify-between">
                        <span className="text-[10px] font-semibold text-slate-500 dark:text-slate-600">Speed</span>
                        <div className="flex gap-1">
                          {[1, 2, 3].map((i) => (
                            <div key={i} className={`h-1.5 w-5 rounded-full ${i <= model.speed ? 'bg-orange-500' : 'bg-white/10'}`} />
                          ))}
                        </div>
                      </div>
                    </button>
                  );
                })}
              </div>
            </div>


            {/* Button */}
            <div className="flex gap-4">
              <button
                onClick={onSummarize}
                disabled={loading}
                className="group flex-1 flex items-center justify-center gap-3 rounded-xl bg-gradient-to-r from-orange-500 to-orange-600 py-4 text-base font-bold text-white shadow-lg shadow-orange-500/25 transition hover:from-orange-400 hover:to-orange-500 focus:ring-4 focus:ring-orange-500/30 disabled:opacity-50"
              >
                {loading ? "Generating Output..." : "Summarize Now"}
                {!loading && <ArrowRight size={18} className="text-white/80 group-hover:translate-x-0.5 transition-transform" />}
              </button>
            </div>
        </div>
      </div>
    </div>
  );
}