-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.html
More file actions
417 lines (375 loc) · 27.7 KB
/
index.html
File metadata and controls
417 lines (375 loc) · 27.7 KB
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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Legal Boolean Search Builder</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
.concept-group {
transition: all 0.3s ease-in-out;
}
.remove-btn {
transition: opacity 0.2s ease-in-out;
}
.group-connector {
flex-shrink: 0;
}
.connector-btn.active {
background-color: #3b82f6; /* bg-blue-500 */
color: white;
}
.connector-btn.inactive {
background-color: #e5e7eb; /* bg-gray-200 */
color: #374151; /* text-gray-700 */
}
.connector-btn.inactive:hover {
background-color: #d1d5db; /* hover:bg-gray-300 */
}
/* Modal Styles */
#truncation-modal.hidden {
display: none;
}
</style>
</head>
<body class="bg-gray-50 text-gray-800">
<div class="container mx-auto p-4 md:p-8">
<header class="text-center mb-8">
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">Legal Boolean Search Builder</h1>
<p class="text-md text-gray-600 mt-2">Create precise legal search strings, step-by-step.</p>
<p> Created by <a href="https://www.linkedin.com/in/rebeccafordon/" class="text-blue-600 hover:underline hover:text-blue-800">Rebecca Fordon</a></p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left Column: Input Controls -->
<div class="bg-white p-6 rounded-2xl shadow-lg border border-gray-200">
<h2 class="text-2xl font-semibold mb-4 text-gray-800">1. Build Your Concepts</h2>
<p class="text-sm text-gray-600 -mt-2 mb-4">A concept is a main idea, party, action, or legal issue. This tool will help you group synonyms for the same concept together (e.g., <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">spectator OR fan</code>.</p>
<div id="concept-container" class="space-y-6">
<!-- Concept groups will be dynamically inserted here -->
</div>
<button id="add-concept-btn" class="mt-6 w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-4 rounded-lg transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
+ Add Concept Group
</button>
</div>
<!-- Right Column: Output & Tips -->
<div class="sticky top-8 self-start">
<div class="bg-white p-6 rounded-2xl shadow-lg border border-gray-200">
<h2 class="text-2xl font-semibold mb-4 text-gray-800">2. Your Search String</h2>
<p class="text-sm text-gray-600 -mt-2 mb-4">Copy the final string and paste it into the advanced search bar of a legal database like Westlaw, Lexis, or Bloomberg Law.</p>
<div class="relative">
<div id="output-string" class="w-full bg-gray-100 text-gray-700 p-4 rounded-lg text-lg font-mono break-words min-h-[100px] border border-gray-300"></div>
<button id="copy-btn" class="absolute top-3 right-3 bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-1 px-3 rounded-md text-sm transition duration-300">Copy</button>
</div>
<p id="copy-feedback" class="text-green-600 text-sm mt-2 h-4 transition-opacity duration-300 opacity-0">Copied to clipboard!</p>
</div>
<div class="bg-white p-6 rounded-2xl shadow-lg border border-gray-200 mt-6">
<div class="flex items-center mb-3">
<h3 class="text-xl font-semibold text-gray-800">Review Your String</h3>
</div>
<p class="text-sm text-gray-600 -mt-2 mb-3">A final check is crucial. The ultimate check is to run the search and see if you are getting expected results. If anything seems off, or if you just want to be sure, use this checklist:</p>
<div class="space-y-3">
<div class="flex items-start">
<input id="check-grouping" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1 flex-shrink-0">
<label for="check-grouping" class="ml-3 text-sm text-gray-700"><strong>Grouping:</strong> Are my <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">()</code> correctly separating each unique concept?</label>
</div>
<div class="flex items-start">
<input id="check-synonyms" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1 flex-shrink-0">
<label for="check-synonyms" class="ml-3 text-sm text-gray-700"><strong>Synonyms:</strong> Have I included all relevant alternate terms with <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">OR</code>?</label>
</div>
<div class="flex items-start">
<input id="check-truncation" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1 flex-shrink-0">
<label for="check-truncation" class="ml-3 text-sm text-gray-700"><strong>Truncation:</strong> Is my use of <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">!</code> capturing all variations? Be careful that the root isn't too <strong>broad</strong> (e.g., <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">defin!</code> finds <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">definition</code> and <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">define</code>, but also <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">definite</code> and <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">definitive</code>) or too <strong>narrow</strong> (e.g., <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">migrat!</code> finds <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">migration</code> and <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">migrate</code>, but misses <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">migrant</code>).</label>
</div>
<div class="flex items-start">
<input id="check-connectors" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1 flex-shrink-0">
<label for="check-connectors" class="ml-3 text-sm text-gray-700"><strong>Connectors:</strong> Is the main connector (<code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">AND</code>, <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">/p</code>, <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">/s</code>) linking my concepts appropriately for the results I need?</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Truncation Builder Modal -->
<div id="truncation-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50">
<div class="relative mx-auto p-6 border w-full max-w-md shadow-lg rounded-xl bg-white">
<div class="text-center">
<h3 class="text-2xl font-semibold text-gray-900">Truncation Builder</h3>
<div class="mt-2 px-7 py-3">
<p class="text-sm text-gray-500">
Enter different forms of a word (e.g., <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">assume</code>, <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">assumed</code>, <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">assuming</code>),separated by a space, to find the common root.
</p>
<input id="modal-variants-input" type="text" class="mt-4 w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-400" placeholder="Type variations separated by space or comma...">
</div>
<div id="modal-suggestion-area" class="mt-2 h-14">
<!-- Suggestion will be injected here -->
</div>
<div class="items-center px-4 py-3">
<button id="modal-close-btn" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 w-full text-base font-medium">
Close
</button>
</div>
</div>
</div>
</div>
<footer class="text-center py-8 text-sm text-gray-500">
<p>
<a href="https://github.com/rlfordon/BooleanBuilder" target="_blank" rel="noopener noreferrer" class="hover:underline text-blue-600">View on GitHub</a>
<span class="mx-2">|</span>
Released under the <a href="https://github.com/rlfordon/BooleanBuilder/blob/main/LICENSE" target="_blank" rel="noopener noreferrer" class="hover:underline text-blue-600">MIT License</a>.
</p>
</footer>
<script>
const conceptContainer = document.getElementById('concept-container');
const addConceptBtn = document.getElementById('add-concept-btn');
const outputString = document.getElementById('output-string');
const copyBtn = document.getElementById('copy-btn');
const copyFeedback = document.getElementById('copy-feedback');
// Modal elements
const truncationModal = document.getElementById('truncation-modal');
const modalVariantsInput = document.getElementById('modal-variants-input');
const modalSuggestionArea = document.getElementById('modal-suggestion-area');
const modalCloseBtn = document.getElementById('modal-close-btn');
const findCommonPrefix = (strs) => {
if (!strs || strs.length === 0) return "";
strs = strs.filter(s => s && typeof s === 'string' && s.length > 0);
if (strs.length < 2) return "";
strs.sort();
const firstStr = strs[0];
const lastStr = strs[strs.length - 1];
let i = 0;
while (i < firstStr.length && i < lastStr.length && firstStr[i] === lastStr[i]) {
i++;
}
return firstStr.substring(0, i);
};
const renumberConcepts = () => {
const groups = conceptContainer.querySelectorAll('.concept-group');
groups.forEach((group, index) => {
const title = group.querySelector('h3');
if (title) {
title.textContent = `Concept ${index + 1}`;
}
});
};
const createConceptGroup = () => {
const groupId = `group-${Date.now()}`;
const groupWrapper = document.createElement('div');
groupWrapper.id = groupId;
groupWrapper.className = 'concept-group bg-gray-50 p-4 rounded-xl border border-gray-200';
if (conceptContainer.children.length > 0) {
const connectorHtml = `
<div class="my-4 p-4 rounded-lg bg-blue-50 border border-blue-200 text-center group-connector" data-connector="AND">
<label class="block text-sm font-bold text-gray-700 mb-3">Choose how to connect concepts:</label>
<div class="flex justify-center space-x-2 connector-buttons">
<div class="relative group"><button class="connector-btn px-4 py-2 text-sm font-semibold rounded-md transition-colors duration-200 active" data-value="AND">AND</button><div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-56 bg-gray-800 text-white text-center text-xs rounded-lg py-2 px-3 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-opacity duration-300 pointer-events-none z-10"><strong>Broadest search.</strong> Finds documents with your concepts anywhere in the text.<svg class="absolute text-gray-800 h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255" xml:space="preserve"><polygon class="fill-current" points="0,0 127.5,127.5 255,0"/></svg></div></div>
<div class="relative group"><button class="connector-btn px-4 py-2 text-sm font-semibold rounded-md transition-colors duration-200 inactive" data-value="/p">/p (paragraph)</button><div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-56 bg-gray-800 text-white text-center text-xs rounded-lg py-2 px-3 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-opacity duration-300 pointer-events-none z-10">Finds concepts in the <strong>same paragraph</strong>. A good balance of precision and recall.<svg class="absolute text-gray-800 h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255" xml:space="preserve"><polygon class="fill-current" points="0,0 127.5,127.5 255,0"/></svg></div></div>
<div class="relative group"><button class="connector-btn px-4 py-2 text-sm font-semibold rounded-md transition-colors duration-200 inactive" data-value="/s">/s (sentence)</button><div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-56 bg-gray-800 text-white text-center text-xs rounded-lg py-2 px-3 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-opacity duration-300 pointer-events-none z-10"><strong>Most restrictive.</strong> Finds concepts in the <strong>same sentence</strong>.<svg class="absolute text-gray-800 h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255" xml:space="preserve"><polygon class="fill-current" points="0,0 127.5,127.5 255,0"/></svg></div></div>
</div>
</div>
`;
const lastGroup = conceptContainer.lastElementChild;
if (lastGroup) {
lastGroup.insertAdjacentHTML('afterend', connectorHtml);
}
}
const groupHtml = `
<div class="flex justify-between items-center mb-3">
<h3 class="text-lg font-semibold text-gray-700"></h3>
<button class="remove-group-btn text-red-500 hover:text-red-700 font-semibold text-sm opacity-50 hover:opacity-100" data-group-id="${groupId}">Remove</button>
</div>
<div class="flex items-start text-xs text-gray-500 mb-3 -mt-2">
<p>Tip: Use <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">!</code> at the end of a word root for truncation (e.g., <code class="bg-gray-200 text-gray-700 font-mono text-xs px-1.5 py-0.5 rounded">injur!</code> finds injury, injured, etc.). Need help finding a word's root? <button class="open-truncation-modal-btn text-blue-600 hover:text-blue-800 font-semibold underline">Build a Truncated Term</button></p>
</div>
<div class="space-y-2 synonym-inputs">
</div>
<div class="flex items-center mt-3">
<button class="add-synonym-btn text-sm font-medium text-blue-600 hover:text-blue-800">+ Add alternate term (OR)</button>
<div class="relative flex items-center ml-2 group"><button class="h-5 w-5 bg-gray-200 text-gray-600 rounded-full flex items-center justify-center text-xs font-bold group-hover:bg-gray-300 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-400">?</button><div class="absolute top-full left-0 mt-2 w-64 bg-gray-800 text-white text-left text-xs rounded-lg py-2 px-3 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-opacity duration-300 pointer-events-none z-10"><p class="font-bold mb-1 text-sm">Brainstorming Terms</p><p>Consider adding:</p><ul class="list-disc list-inside pl-2 mt-1 space-y-1"><li><strong>Synonyms:</strong> e.g., child OR minor OR infant</li><li><strong>Broader terms:</strong> e.g., car OR vehicle</li><li><strong>Specific examples:</strong> e.g., "social media" OR twitter OR facebook OR snapchat</li><li><strong>Narrower terms:</strong> e.g., accident OR "slip and fall"</li><li><strong>Related concepts:</strong> e.g., marriage OR divorce</li></ul><svg class="absolute text-gray-800 h-2 w-full left-0 bottom-full" x="0px" y="0px" viewBox="0 0 255 255" xml:space="preserve"><polygon class="fill-current" points="0,255 127.5,127.5 255,255"/></svg></div></div>
</div>
`;
groupWrapper.innerHTML = groupHtml;
addTermInput(groupWrapper.querySelector('.synonym-inputs'), true);
conceptContainer.appendChild(groupWrapper);
groupWrapper.querySelector('.add-synonym-btn').addEventListener('click', () => addTermInput(groupWrapper.querySelector('.synonym-inputs'), false));
groupWrapper.querySelector('.remove-group-btn').addEventListener('click', (e) => removeConceptGroup(e.target.dataset.groupId));
groupWrapper.querySelector('.open-truncation-modal-btn').addEventListener('click', openTruncationModal);
renumberConcepts();
updateSearchString();
};
const addTermInput = (container, isFirst = false) => {
const termWrapper = document.createElement('div');
termWrapper.className = 'term-wrapper';
const inputWrapperClass = isFirst ? '' : 'space-x-2';
termWrapper.innerHTML = `
<div class="flex items-center ${inputWrapperClass}">
${isFirst ? '' : '<button class="remove-synonym-btn text-gray-400 hover:text-gray-600 p-1 remove-btn">×</button>'}
<div class="relative flex-grow flex items-center">
<input type="text" class="concept-input w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-400" placeholder="${isFirst ? 'Enter term or phrase' : 'Alternate term'}">
<div class="phrase-suggester-container absolute right-2 top-1/2 -translate-y-1/2"></div>
</div>
</div>
`;
container.appendChild(termWrapper);
const inputElement = termWrapper.querySelector('.concept-input');
inputElement.addEventListener('input', (e) => {
updateSearchString();
handlePhraseSuggestions(e.target);
});
if (!isFirst) {
termWrapper.querySelector('.remove-synonym-btn').addEventListener('click', (e) => {
e.target.closest('.term-wrapper').remove();
updateSearchString();
});
}
if (!isFirst) inputElement.focus();
};
const removeConceptGroup = (groupId) => {
const group = document.getElementById(groupId);
let previousElement = group.previousElementSibling;
if (previousElement && previousElement.classList.contains('group-connector')) {
previousElement.remove();
}
group.remove();
renumberConcepts();
updateSearchString();
};
const generateProximityString = (term) => {
term = term.trim();
if (!term) return '';
const stopWords = new Set(['a', 'an', 'and', 'the', 'of', 'in', 'to', 'or']);
const words = term.split(/\s+/).filter(word => !stopWords.has(word.toLowerCase()));
return words.length > 1 ? words.join(' /3 ') : words.join('');
};
const handlePhraseSuggestions = (inputElement) => {
const value = inputElement.value.trim();
const suggesterContainer = inputElement.parentElement.querySelector('.phrase-suggester-container');
suggesterContainer.innerHTML = '';
if (value.includes(' ') && !value.match(/\s\/\d+\s/)) {
const suggestion = generateProximityString(value);
if(!suggestion) return;
const suggesterWrapper = document.createElement('div');
suggesterWrapper.className = 'flex items-center space-x-2';
const suggestionBtn = document.createElement('button');
suggestionBtn.textContent = `Use: ${suggestion}`;
suggestionBtn.className = "text-xs bg-blue-100 text-blue-800 hover:bg-blue-200 rounded-md px-2 py-1 transition-all";
suggestionBtn.onclick = () => {
inputElement.value = suggestion;
suggesterContainer.innerHTML = '';
updateSearchString();
};
const tooltipHtml = `<div class="relative flex items-center group"><button class="h-5 w-5 bg-gray-200 text-gray-600 rounded-full flex items-center justify-center text-xs font-bold group-hover:bg-gray-300 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-400">?</button><div class="absolute bottom-full right-0 mb-2 w-72 bg-gray-800 text-white text-left text-xs rounded-lg py-2 px-3 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-opacity duration-300 pointer-events-none z-10"><p class="font-bold mb-1 text-sm">Phrase Searching</p><p>Multi-word terms default to an exact phrase search (e.g., <strong>"assumption of risk"</strong>). For more flexibility, click the suggestion. This automatically removes common words (like 'of', 'the') and connects the rest within a few words of each other (e.g., creating <strong>assumption /3 risk</strong>). You can then manually add truncation where needed.</p><svg class="absolute text-gray-800 h-2 w-full left-0 top-full" x="0px" y="0px" viewBox="0 0 255 255" xml:space="preserve"><polygon class="fill-current" points="0,0 127.5,127.5 255,0"/></svg></div></div>`;
suggesterWrapper.appendChild(suggestionBtn);
suggesterWrapper.insertAdjacentHTML('beforeend', tooltipHtml);
suggesterContainer.appendChild(suggesterWrapper);
}
};
const processTerm = (term) => {
term = term.trim();
if (!term) return '';
if (term.match(/\s\/\d+\s/)) return `(${term})`;
if (term.includes(' ')) return `"${term.replace(/"/g, '\\"')}"`;
return term;
};
const updateSearchString = () => {
const groups = conceptContainer.querySelectorAll('.concept-group');
const connectors = conceptContainer.querySelectorAll('.group-connector');
let finalString = '';
groups.forEach((group, index) => {
const terms = Array.from(group.querySelectorAll('.concept-input'))
.map(input => input.value.trim())
.filter(value => value !== '')
.map(processTerm);
if (terms.length > 0) {
const groupString = `(${terms.join(' OR ')})`;
if (index > 0) {
const connectorEl = connectors[index - 1];
finalString += connectorEl ? ` ${connectorEl.dataset.connector} ` : ' AND ';
}
finalString += groupString;
}
});
outputString.textContent = finalString.trim();
};
const copyToClipboard = (textToCopy, feedbackEl) => {
if(!textToCopy) return;
const textArea = document.createElement("textarea");
textArea.value = textToCopy;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
if (feedbackEl) {
const originalText = feedbackEl.innerHTML;
feedbackEl.textContent = 'Copied!';
feedbackEl.disabled = true;
setTimeout(() => {
feedbackEl.innerHTML = originalText;
feedbackEl.disabled = false;
}, 1500);
} else {
copyFeedback.classList.remove('opacity-0');
setTimeout(() => copyFeedback.classList.add('opacity-0'), 2000);
}
} catch (err) {
console.error('Failed to copy: ', err);
if (feedbackEl) feedbackEl.textContent = 'Copy Failed!';
}
document.body.removeChild(textArea);
};
// Modal Logic
const openTruncationModal = () => {
modalVariantsInput.value = '';
modalSuggestionArea.innerHTML = '';
truncationModal.classList.remove('hidden');
modalVariantsInput.focus();
};
const closeTruncationModal = () => truncationModal.classList.add('hidden');
modalVariantsInput.addEventListener('input', () => {
const variants = modalVariantsInput.value.trim().toLowerCase().split(/[\s,]+/).filter(Boolean);
modalSuggestionArea.innerHTML = '';
if (variants.length >= 2) {
const prefix = findCommonPrefix(variants);
if (prefix.length > 2) {
const suggestion = prefix + '!';
const suggestionBtn = document.createElement('button');
suggestionBtn.className = "text-sm bg-green-100 text-green-800 hover:bg-green-200 rounded-md px-3 py-1.5 transition-all w-full text-left";
suggestionBtn.innerHTML = `Copy suggested root: <strong class="font-semibold">${suggestion}</strong>`;
suggestionBtn.onclick = () => copyToClipboard(suggestion, suggestionBtn);
modalSuggestionArea.appendChild(suggestionBtn);
}
}
});
modalCloseBtn.addEventListener('click', closeTruncationModal);
truncationModal.addEventListener('click', (e) => {
if (e.target === truncationModal) closeTruncationModal();
});
document.addEventListener('keydown', (e) => {
if (e.key === "Escape" && !truncationModal.classList.contains('hidden')) {
closeTruncationModal();
}
});
addConceptBtn.addEventListener('click', createConceptGroup);
copyBtn.addEventListener('click', () => copyToClipboard(outputString.textContent));
conceptContainer.addEventListener('change', updateSearchString);
conceptContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('connector-btn')) {
const button = e.target;
const connectorGroup = button.closest('.group-connector');
connectorGroup.dataset.connector = button.dataset.value;
connectorGroup.querySelectorAll('.connector-btn').forEach(btn => {
btn.classList.remove('active', 'inactive');
btn.classList.add(btn === button ? 'active' : 'inactive');
});
updateSearchString();
} else if (e.target.classList.contains('remove-group-btn') || e.target.classList.contains('remove-synonym-btn')) {
updateSearchString();
}
});
createConceptGroup();
</script>
</body>
</html>