claude-code-proxy/web/app/utils/formatters.ts
Seif Ghazi ae71ec4f72
Ready
2025-06-29 20:50:04 -04:00

174 lines
No EOL
6.1 KiB
TypeScript

/**
* Utility functions for formatting and displaying data
*/
/**
* Safely converts any value to a formatted string for display
*/
export function formatValue(value: any): string {
if (value === null) return 'null';
if (value === undefined) return 'undefined';
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
try {
return JSON.stringify(value, null, 2);
} catch (error) {
return String(value);
}
}
/**
* Formats JSON with proper indentation and returns a formatted string
*/
export function formatJSON(obj: any, maxLength: number = 1000): string {
try {
const jsonString = JSON.stringify(obj, null, 2);
if (jsonString.length > maxLength) {
return jsonString.substring(0, maxLength) + '...';
}
return jsonString;
} catch (error) {
return String(obj);
}
}
/**
* Escapes HTML characters to prevent XSS
*/
export function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Formats large text with proper line breaks and structure, optimized for the new conversation flow
*/
export function formatLargeText(text: string): string {
if (!text) return '';
// Escape HTML first
const escaped = escapeHtml(text);
// Format the text with proper spacing and structure
return escaped
// Preserve existing double line breaks
.replace(/\n\n/g, '<br><br>')
// Convert single line breaks to single <br> tags
.replace(/\n/g, '<br>')
// Format bullet points with modern styling
.replace(/^(\s*)([-*•])\s+(.+)$/gm, '$1<span class="inline-flex items-center space-x-2"><span class="w-1.5 h-1.5 bg-blue-500 rounded-full flex-shrink-0"></span><span>$3</span></span>')
// Format numbered lists with modern styling
.replace(/^(\s*)(\d+)\.\s+(.+)$/gm, '$1<span class="inline-flex items-center space-x-2"><span class="w-5 h-5 bg-blue-100 text-blue-700 rounded-full flex items-center justify-center text-xs font-semibold">$2</span><span>$3</span></span>')
// Format headers with better typography
.replace(/^([A-Z][^<\n]*:)(<br>|$)/gm, '<div class="font-semibold text-gray-900 mt-4 mb-2 border-b border-gray-200 pb-1">$1</div>$2')
// Format code blocks with better styling
.replace(/\b([A-Z_]{3,})\b/g, '<code class="bg-gradient-to-r from-gray-100 to-blue-50 border border-gray-200 px-2 py-0.5 rounded-md text-xs text-blue-700 font-mono font-medium">$1</code>')
// Format file paths and technical terms
.replace(/\b([a-zA-Z0-9_-]+\.[a-zA-Z]{2,4})\b/g, '<span class="bg-slate-100 text-slate-700 px-1.5 py-0.5 rounded text-xs font-mono border border-slate-200">$1</span>')
// Format URLs with modern link styling
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" class="text-blue-600 hover:text-blue-800 underline underline-offset-2 decoration-blue-300 hover:decoration-blue-500 transition-colors font-medium" target="_blank" rel="noopener noreferrer">$1</a>')
// Format quoted text
.replace(/^(\s*)([""](.+?)[""])/gm, '$1<blockquote class="border-l-4 border-blue-200 bg-blue-50 pl-4 py-2 my-2 italic text-gray-700 rounded-r">$3</blockquote>')
// Add proper spacing around paragraphs
.replace(/(<br><br>)/g, '<div class="my-4"></div>')
// Clean up any excessive spacing
.replace(/(<br>\s*){3,}/g, '<br><br>')
// Format emphasis patterns
.replace(/\*\*([^*]+)\*\*/g, '<strong class="font-semibold text-gray-900">$1</strong>')
.replace(/\*([^*]+)\*/g, '<em class="italic text-gray-700">$1</em>')
// Format inline code
.replace(/`([^`]+)`/g, '<code class="bg-gray-100 text-gray-800 px-1.5 py-0.5 rounded text-sm font-mono border border-gray-200">$1</code>');
}
/**
* Determines if a value is a complex object that should be JSON-formatted
*/
export function isComplexObject(value: any): boolean {
return value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
Object.keys(value).length > 0;
}
/**
* Truncates text to a specified length with ellipsis
*/
export function truncateText(text: string, maxLength: number = 200): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
/**
* Formats timestamp for display in the conversation flow
*/
export function formatTimestamp(timestamp: string | Date): string {
try {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
// Less than a minute ago
if (diff < 60000) {
return 'Just now';
}
// Less than an hour ago
if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
return `${minutes}m ago`;
}
// Less than a day ago
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours}h ago`;
}
// More than a day ago - show time
return date.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: true
});
} catch {
return String(timestamp);
}
}
/**
* Formats file size for display
*/
export function formatFileSize(bytes: number): string {
const sizes = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
}
/**
* Creates a content preview for message summaries
*/
export function createContentPreview(content: any, maxLength: number = 100): string {
if (typeof content === 'string') {
return content.length > maxLength ? content.substring(0, maxLength) + '...' : content;
}
if (Array.isArray(content)) {
const textContent = content.find(c => c.type === 'text')?.text || '';
if (textContent) {
return textContent.length > maxLength ? textContent.substring(0, maxLength) + '...' : textContent;
}
return `${content.length} content blocks`;
}
if (content && typeof content === 'object') {
if (content.text) {
return content.text.length > maxLength ? content.text.substring(0, maxLength) + '...' : content.text;
}
return 'Complex content';
}
return 'No content';
}