Spaces:
Sleeping
Sleeping
File size: 5,870 Bytes
d0878a6 | 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 | /**
* Build citation-oriented search queries and URLs for advisor answers.
*
* Websites cannot read the browser's default search engine. Web search opens
* in a new tab (the user's default browser); the engine is configurable via
* localStorage key `webSearchEngine` (google | ddg | bing | brave).
*/
export const SEARCH_QUERY_MAX_CHARS = 1700;
export const WEB_SEARCH_ENGINE_KEY = 'webSearchEngine';
export const WEB_SEARCH_ENGINES = {
google: {
id: 'google',
label: 'Google',
url: 'https://www.google.com/search?q=',
},
ddg: {
id: 'ddg',
label: 'DuckDuckGo',
url: 'https://duckduckgo.com/?q=',
},
bing: {
id: 'bing',
label: 'Bing',
url: 'https://www.bing.com/search?q=',
},
brave: {
id: 'brave',
label: 'Brave',
url: 'https://search.brave.com/search?q=',
},
};
const CLAIM_HINT =
/\b(must|should|require|requires|nist|iso|cve|cisa|owasp|because|therefore|according|standard|framework|control|risk|attack|vulnerability|encrypt|mfa|zero[- ]trust|gdpr|hipaa|pci)\b/i;
export function stripSearchMarkdown(md) {
if (!md) return '';
return String(md)
.replace(/```[\s\S]*?```/g, ' ')
.replace(/`([^`]+)`/g, '$1')
.replace(/#{1,6}\s?/g, '')
.replace(/[*_~]{1,3}([^*_~]+)[*_~]{1,3}/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/!\[[^\]]*\]\([^)]+\)/g, '$1')
.replace(/^\s*[-*+]\s+/gm, '')
.replace(/^\s*\d+\.\s+/gm, '')
.replace(/\s+/g, ' ')
.trim();
}
function splitAssertions(text) {
if (!text) return [];
return text
.replace(/([.!?])\s+/g, '$1\n')
.split(/\n+/)
.map((s) => s.replace(/^[\s•●▪◦-]+/, '').trim())
.filter((s) => s.length >= 28 && s.length <= 280)
.filter((s) => !/^(hi|hello|thanks|thank you)\b/i.test(s));
}
export function extractAssertions(text, { maxClaims = 6 } = {}) {
const cleaned = stripSearchMarkdown(text);
const parts = splitAssertions(cleaned);
const scored = parts.map((claim, index) => ({
claim,
index,
score: (CLAIM_HINT.test(claim) ? 3 : 0) + Math.min(2, Math.floor(claim.length / 80)),
}));
scored.sort((a, b) => b.score - a.score || a.index - b.index);
const picked = [];
const seen = new Set();
for (const item of scored) {
const key = item.claim.slice(0, 48).toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
picked.push(item.claim.replace(/[.]+$/, ''));
if (picked.length >= maxClaims) break;
}
if (picked.length === 0 && cleaned) {
picked.push(cleaned.slice(0, 220).trim());
}
return picked;
}
export function truncateQuery(text, maxChars = SEARCH_QUERY_MAX_CHARS) {
const value = String(text || '').trim();
if (value.length <= maxChars) return value;
const sliced = value.slice(0, maxChars);
const lastBreak = Math.max(sliced.lastIndexOf(' '), sliced.lastIndexOf(','), sliced.lastIndexOf(';'));
return `${(lastBreak > 40 ? sliced.slice(0, lastBreak) : sliced).trim()}…`;
}
export function buildReferenceSearchQuery({
advisorText = '',
userQuestion = '',
advisorName = '',
} = {}) {
const assertions = extractAssertions(advisorText);
const claimBlock = assertions.map((c, i) => `${i + 1}. ${c}`).join(' ');
const who = advisorName ? ` in the ${advisorName} advisor answer` : ' in the advisor answer';
const context = userQuestion
? ` User question for context: "${stripSearchMarkdown(userQuestion).slice(0, 220)}".`
: '';
const query = [
'Find primary sources, standards, and reputable citations that support or challenge these claims',
`${who}:`,
claimBlock,
context,
'Prefer NIST, CISA, ISO, OWASP, RFCs, vendor advisories, and peer-reviewed sources over blogs.',
].filter(Boolean).join(' ');
return truncateQuery(query);
}
export function getWebSearchEngine(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
try {
const id = storage?.getItem?.(WEB_SEARCH_ENGINE_KEY);
if (id && WEB_SEARCH_ENGINES[id]) return WEB_SEARCH_ENGINES[id];
} catch {
/* ignore quota / private mode */
}
return WEB_SEARCH_ENGINES.google;
}
export function setWebSearchEngine(id, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const engine = WEB_SEARCH_ENGINES[id] || WEB_SEARCH_ENGINES.google;
try {
storage?.setItem?.(WEB_SEARCH_ENGINE_KEY, engine.id);
} catch {
/* ignore */
}
return engine;
}
export function buildWebSearchUrl(query, engineId) {
const engine = (engineId && WEB_SEARCH_ENGINES[engineId]) || getWebSearchEngine();
return `${engine.url}${encodeURIComponent(query || '')}`;
}
export function buildPerplexityUrl(query) {
return `https://www.perplexity.ai/?q=${encodeURIComponent(query || '')}`;
}
export function formatReferenceSnippet(text) {
const trimmed = String(text || '').trim();
return `Here are references I found:\n\n${trimmed}`;
}
export function wrapReferencesForAdvisorContext(snippet) {
const body = String(snippet || '').trim();
if (!body) return '';
return [
'[User-provided references from web search. Treat these as supporting sources; use them to support or challenge claims in your answer.]',
body,
].join('\n\n');
}
export async function readClipboardText() {
if (typeof navigator === 'undefined' || !navigator.clipboard?.readText) {
const err = new Error('Clipboard API unavailable');
err.code = 'unavailable';
throw err;
}
try {
const text = await navigator.clipboard.readText();
return { text: String(text || ''), via: 'clipboard' };
} catch (cause) {
const err = new Error(cause?.message || 'Clipboard permission denied');
err.code = 'denied';
err.cause = cause;
throw err;
}
}
|