Download src/App.js from snvskiit/resume: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/spaces/snvskiit/resume/resolve/main/src/App.js
- Command line
-
hf download hf://spaces/snvskiit/resume/src/App.js
-
curl -L -o App.js https://huggingface.co/spaces/snvskiit/resume/resolve/main/src/App.js
14 kB
| import { useState, useEffect, useRef, useCallback } from 'react'; | |
| import { Send, User, Bot, Loader, RefreshCw, Github, Linkedin, Phone, Mail, AlertCircle } from 'lucide-react'; | |
| import './App.css'; | |
| import config from './config'; | |
| // Helper function to check backend connectivity | |
| const checkBackendHealth = async () => { | |
| try { | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => { | |
| controller.abort(); | |
| }, 5000); // 5 seconds timeout | |
| const response = await fetch(config.apiUrl('/health'), { | |
| method: 'GET', | |
| signal: controller.signal | |
| }); | |
| clearTimeout(timeoutId); | |
| return response.ok; | |
| } catch (error) { | |
| return false; | |
| } | |
| }; | |
| // Helper function to render AI responses with basic markdown support | |
| const renderMessage = (content) => { | |
| if (typeof content !== 'string') return content; | |
| // Simple markdown-like formatting | |
| let formatted = content | |
| // Bold text | |
| .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') | |
| // Italic text | |
| .replace(/\*(.*?)\*/g, '<em>$1</em>') | |
| // Line breaks | |
| .replace(/\n/g, '<br />'); | |
| return <span dangerouslySetInnerHTML={{ __html: formatted }} />; | |
| }; | |
| function App() { | |
| const [messages, setMessages] = useState([]); | |
| const [inputMessage, setInputMessage] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [profile, setProfile] = useState(null); | |
| const [isConnected, setIsConnected] = useState(true); | |
| const [error, setError] = useState(null); | |
| const [isProfileLoading, setIsProfileLoading] = useState(true); | |
| const messagesEndRef = useRef(null); | |
| const scrollToBottom = useCallback(() => { | |
| requestAnimationFrame(() => { | |
| messagesEndRef.current?.scrollIntoView({ | |
| behavior: "smooth", | |
| block: "end", | |
| inline: "nearest" | |
| }); | |
| }); | |
| }, []); | |
| // Check backend health periodically | |
| useEffect(() => { | |
| const checkHealth = async () => { | |
| const healthy = await checkBackendHealth(); | |
| setIsConnected(healthy); | |
| }; | |
| checkHealth(); | |
| const interval = setInterval(checkHealth, 30000); // Check every 30 seconds | |
| return () => clearInterval(interval); | |
| }, []); | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages, isLoading, scrollToBottom]); | |
| useEffect(() => { | |
| // Fetch profile data with error handling | |
| const fetchProfile = async () => { | |
| try { | |
| setIsProfileLoading(true); | |
| const response = await fetch(config.apiUrl('/profile')); | |
| if (!response.ok) throw new Error('Failed to fetch profile'); | |
| // Check if response is actually JSON | |
| const contentType = response.headers.get('content-type'); | |
| if (!contentType || !contentType.includes('application/json')) { | |
| throw new Error('Invalid response format'); | |
| } | |
| const data = await response.json(); | |
| setProfile(data); | |
| setError(null); | |
| setIsConnected(true); | |
| } catch (err) { | |
| console.error('Error fetching profile:', err); | |
| // Only show demo data if backend is completely unavailable | |
| // Don't hardcode - this should be temporary fallback only | |
| if (err.message.includes('Failed to fetch') || err.message.includes('network')) { | |
| setError('Backend not available - please check backend service'); | |
| setIsConnected(false); | |
| // Minimal fallback - should be replaced with real API | |
| setProfile({ | |
| name: "Resume Loading...", | |
| title: "Please wait while backend loads", | |
| description: "Backend service is starting up. Please refresh in a moment.", | |
| skills: ["Loading..."], | |
| contact: null, | |
| expertise_areas: [], | |
| achievements: [] | |
| }); | |
| } else { | |
| // Backend is responding but with errors - show the error | |
| setError(`Backend error: ${err.message}`); | |
| setIsConnected(false); | |
| setProfile(null); | |
| } | |
| } finally { | |
| setIsProfileLoading(false); | |
| } | |
| }; | |
| fetchProfile(); | |
| }, []); | |
| const sendMessage = async (messageText) => { | |
| if (!messageText.trim()) return; | |
| const userMessage = messageText.trim(); | |
| setInputMessage(''); | |
| setIsLoading(true); | |
| setError(null); | |
| // Add user message to chat | |
| const newUserMessage = { type: 'user', content: userMessage, timestamp: new Date() }; | |
| setMessages(prev => [...prev, newUserMessage]); | |
| try { | |
| // Prepare history for API | |
| const history = messages.map(msg => ({ | |
| [msg.type]: msg.content | |
| })); | |
| const response = await fetch(config.apiUrl('/chat'), { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| message: userMessage, | |
| history: history | |
| }), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| // Check if response is actually JSON | |
| const contentType = response.headers.get('content-type'); | |
| if (!contentType || !contentType.includes('application/json')) { | |
| throw new Error('Invalid response format'); | |
| } | |
| const data = await response.json(); | |
| if (data.success) { | |
| const botMessage = { type: 'assistant', content: data.response, timestamp: new Date() }; | |
| setMessages(prev => [...prev, botMessage]); | |
| setIsConnected(true); | |
| } else { | |
| throw new Error(data.error || 'Failed to get response'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| setIsConnected(false); | |
| const errorMessage = { | |
| type: 'assistant', | |
| content: `I apologize, but I'm unable to connect to the backend service right now. The backend at https://snvskiit-resume-backend.hf.space appears to be unavailable.\n\nYour message: "${userMessage}"\n\nPlease:\n1. Check if the backend service is running\n2. Verify the backend URL is correct\n3. Try again in a few moments\n\nThis frontend is designed to work with a live backend API, not as a standalone demo.`, | |
| timestamp: new Date() | |
| }; | |
| setMessages(prev => [...prev, errorMessage]); | |
| setError('Backend API not available - please check backend service'); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleSubmit = (e) => { | |
| e.preventDefault(); | |
| sendMessage(inputMessage); | |
| }; | |
| const handleKeyDown = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| if (inputMessage.trim() && !isLoading) { | |
| sendMessage(inputMessage); | |
| } | |
| } | |
| }; | |
| const clearChat = () => { | |
| setMessages([]); | |
| }; | |
| return ( | |
| <> | |
| <div className={`connection-status ${isConnected ? 'connected' : 'disconnected'}`}> | |
| {isConnected ? 'π’ Connected' : 'π΄ Disconnected'} | |
| </div> | |
| <div className="app-container"> | |
| {/* Left Panel - Profile Information */} | |
| <div className="left-panel"> | |
| <div className="header"> | |
| {error && ( | |
| <div className="error-message"> | |
| <AlertCircle size={16} /> | |
| {error} | |
| </div> | |
| )} | |
| {isProfileLoading ? ( | |
| <div className="loading-message" style={{ padding: '2rem', textAlign: 'center' }}> | |
| <Loader size={24} className="animate-spin" /> | |
| <p>Loading profile...</p> | |
| </div> | |
| ) : profile ? ( | |
| <> | |
| {profile.photo_url ? ( | |
| <img | |
| className="profile-photo face-upper" | |
| src={config.staticUrl(profile.photo_url)} | |
| alt={profile.name} | |
| onError={(e) => { | |
| e.target.style.display = 'none'; | |
| const nextSibling = e.target.nextSibling; | |
| if (nextSibling) { | |
| nextSibling.style.display = 'flex'; | |
| } | |
| }} | |
| /> | |
| ) : ( | |
| <div className="profile-placeholder"> | |
| π¨βπ» | |
| </div> | |
| )} | |
| <div className="hidden-placeholder"> | |
| <div className="profile-placeholder"> | |
| π¨βπ» | |
| </div> | |
| </div> | |
| <h1 className="title">{profile.name}</h1> | |
| <p className="subtitle">{profile.title}</p> | |
| <p className="description">{profile.description}</p> | |
| {/* Contact Information - Prominent Display */} | |
| {profile.contact && ( | |
| <div className="contact-info"> | |
| <h3 className="section-title">πΌ Contact Information</h3> | |
| <div className="contact-row"> | |
| {profile.contact.email && ( | |
| <div className="contact-item"> | |
| <Mail /> | |
| <span>{profile.contact.email}</span> | |
| </div> | |
| )} | |
| {profile.contact.mobile && ( | |
| <div className="contact-item"> | |
| <Phone /> | |
| <span>{profile.contact.mobile}</span> | |
| </div> | |
| )} | |
| {profile.contact.github && ( | |
| <a className="social-link" href={profile.contact.github} target="_blank" rel="noopener noreferrer"> | |
| <Github /> | |
| <span>GitHub</span> | |
| </a> | |
| )} | |
| {profile.contact.linkedin && ( | |
| <a className="social-link" href={profile.contact.linkedin} target="_blank" rel="noopener noreferrer"> | |
| <Linkedin /> | |
| <span>LinkedIn</span> | |
| </a> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| <h3 className="section-title">β‘ Technical Skills</h3> | |
| <div className="skills-container"> | |
| {profile.skills.map((skill, index) => ( | |
| <span key={index} className="skill">{skill}</span> | |
| ))} | |
| </div> | |
| <h3 className="section-title">π― Areas of Expertise</h3> | |
| <div className="expertise-container"> | |
| {profile.expertise_areas.map((area, index) => ( | |
| <div key={index} className="expertise-card"> | |
| <div className="expertise-icon">{area.icon}</div> | |
| <h4 className="expertise-title">{area.area}</h4> | |
| <p className="expertise-description">{area.description}</p> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="achievements-container"> | |
| <h3 className="section-title">π Key Achievements</h3> | |
| {profile.achievements.map((achievement, index) => ( | |
| <div key={index} className="achievement-item"> | |
| {achievement} | |
| </div> | |
| ))} | |
| </div> | |
| </> | |
| ) : !error ? ( | |
| <div className="loading-message" style={{ padding: '2rem', textAlign: 'center' }}> | |
| <p>No profile data available</p> | |
| </div> | |
| ) : null} | |
| </div> | |
| </div> | |
| {/* Right Panel - Chat Interface */} | |
| <div className="chat-container"> | |
| {/* Messages */} | |
| <div className="messages-container"> | |
| {messages.map((message, index) => ( | |
| <div key={index} className={`message ${message.type === 'user' ? 'user' : ''}`}> | |
| <div className={`message-icon ${message.type === 'user' ? 'user' : 'bot'}`}> | |
| {message.type === 'user' ? <User size={18} /> : <Bot size={18} />} | |
| </div> | |
| <div className={`message-bubble ${message.type === 'user' ? 'user' : 'bot'}`}> | |
| {renderMessage(message.content)} | |
| </div> | |
| </div> | |
| ))} | |
| {isLoading && ( | |
| <div className="message"> | |
| <div className="message-icon bot"> | |
| <Bot size={18} /> | |
| </div> | |
| <div className="message-bubble bot"> | |
| <div className="loading-message"> | |
| <Loader size={16} className="animate-spin" /> | |
| Satish is thinking... | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| {/* Input */} | |
| <div className="input-container"> | |
| <form onSubmit={handleSubmit} className="form-container"> | |
| <input | |
| className="input" | |
| type="text" | |
| value={inputMessage} | |
| onChange={(e) => setInputMessage(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder="Type your message here... (Press Enter to send)" | |
| disabled={isLoading} | |
| autoComplete="off" | |
| autoCorrect="off" | |
| autoCapitalize="sentences" | |
| /> | |
| <button className="send-button" type="submit" disabled={isLoading || !inputMessage.trim()}> | |
| {isLoading ? <Loader size={18} className="animate-spin" /> : <Send size={18} />} | |
| <span>Send</span> | |
| </button> | |
| </form> | |
| {messages.length > 0 && ( | |
| <button className="clear-button" onClick={clearChat}> | |
| <RefreshCw size={18} /> | |
| <span>Clear</span> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </> | |
| ); | |
| } | |
| export default App; | |