| 34 | } |
| 35 | |
| 36 | function parseUserAgent(ua: string): string { |
| 37 | // Check for common browsers in order of specificity |
| 38 | // Edge (Chromium-based) |
| 39 | const edgeMatch = ua.match(/Edg(?:e|A|iOS)?\/(\d+)/); |
| 40 | if (edgeMatch) return `Edge ${edgeMatch[1]}`; |
| 41 | |
| 42 | // Opera |
| 43 | const operaMatch = ua.match(/(?:OPR|Opera)\/(\d+)/); |
| 44 | if (operaMatch) return `Opera ${operaMatch[1]}`; |
| 45 | |
| 46 | // Chrome (must check after Edge/Opera since they include Chrome in UA) |
| 47 | const chromeMatch = ua.match(/Chrome\/(\d+)/); |
| 48 | if (chromeMatch && !ua.includes('Edg') && !ua.includes('OPR')) { |
| 49 | return `Chrome ${chromeMatch[1]}`; |
| 50 | } |
| 51 | |
| 52 | // Safari (must check after Chrome since Chrome includes Safari in UA) |
| 53 | const safariMatch = ua.match(/Version\/(\d+(?:\.\d+)?)\s+Safari/); |
| 54 | if (safariMatch) return `Safari ${safariMatch[1]}`; |
| 55 | |
| 56 | // Firefox |
| 57 | const firefoxMatch = ua.match(/Firefox\/(\d+)/); |
| 58 | if (firefoxMatch) return `Firefox ${firefoxMatch[1]}`; |
| 59 | |
| 60 | // Claude Desktop / Electron apps |
| 61 | if (ua.includes('Claude')) return 'Claude Desktop'; |
| 62 | if (ua.includes('Electron')) return 'Electron App'; |
| 63 | |
| 64 | // Cursor |
| 65 | if (ua.includes('Cursor')) return 'Cursor'; |
| 66 | |
| 67 | // Generic fallback - try to extract something useful |
| 68 | const genericMatch = ua.match(/^(\w+)\/[\d.]+/); |
| 69 | if (genericMatch) return genericMatch[1]; |
| 70 | |
| 71 | return ua.length > 20 ? ua.substring(0, 20) + '...' : ua; |
| 72 | } |
| 73 | |
| 74 | function SqlTooltip({ sql, children }: { sql: string; children: React.ReactElement }) { |
| 75 | return ( |