Spaces:
Build error
Build error
| import { useState, useEffect, type KeyboardEvent } from 'react'; | |
| import { PageParseAPI } from '../services/api'; | |
| import { Code, Sparkles, Copy, Check, FileCode, Terminal, Braces, Globe, Database, Cpu, Coffee, Wifi, WifiOff, Settings, RefreshCw } from 'lucide-react'; | |
| const LANGUAGES = [ | |
| { value: 'auto', label: 'Auto-Detect' }, | |
| { value: 'python', label: 'Python' }, | |
| { value: 'javascript', label: 'JavaScript' }, | |
| { value: 'typescript', label: 'TypeScript' }, | |
| { value: 'java', label: 'Java' }, | |
| { value: 'cpp', label: 'C/C++' }, | |
| { value: 'rust', label: 'Rust' }, | |
| { value: 'go', label: 'Go' }, | |
| { value: 'ruby', label: 'Ruby' }, | |
| { value: 'bash', label: 'Bash/Shell' }, | |
| { value: 'html', label: 'HTML' }, | |
| { value: 'css', label: 'CSS' }, | |
| { value: 'sql', label: 'SQL' }, | |
| ]; | |
| const LANG_ICONS: Record<string, typeof Braces> = { | |
| python: Terminal, | |
| javascript: Braces, | |
| typescript: Braces, | |
| java: Coffee, | |
| cpp: FileCode, | |
| rust: Cpu, | |
| go: Globe, | |
| html: Globe, | |
| css: FileCode, | |
| sql: Database, | |
| bash: Terminal, | |
| }; | |
| interface CodeAnalysisResult { | |
| language: string; | |
| explanation: string; | |
| summary: string; | |
| } | |
| export const CodeEditor = () => { | |
| const [code, setCode] = useState(''); | |
| const [selectedLanguage, setSelectedLanguage] = useState('auto'); | |
| const [result, setResult] = useState<CodeAnalysisResult | null>(null); | |
| const [isAnalyzing, setIsAnalyzing] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const [copied, setCopied] = useState(false); | |
| const [ollamaConnected, setOllamaConnected] = useState(false); | |
| const [ollamaModel, setOllamaModel] = useState(''); | |
| const [ollamaUrl, setOllamaUrl] = useState('http://localhost:11434'); | |
| const [ollamaVersion, setOllamaVersion] = useState(''); | |
| const [modelAvailable, setModelAvailable] = useState(false); | |
| const [checkingStatus, setCheckingStatus] = useState(true); | |
| const [showConfig, setShowConfig] = useState(false); | |
| const [configUrl, setConfigUrl] = useState(''); | |
| const [configModel, setConfigModel] = useState(''); | |
| const [availableModels, setAvailableModels] = useState<string[]>([]); | |
| const [saving, setSaving] = useState(false); | |
| const [configSaved, setConfigSaved] = useState(false); | |
| const checkStatus = async () => { | |
| setCheckingStatus(true); | |
| try { | |
| const status = await PageParseAPI.checkOllamaStatus(); | |
| setOllamaConnected(status.connected); | |
| setOllamaModel(status.model); | |
| setOllamaUrl(status.url); | |
| setOllamaVersion(status.version); | |
| setModelAvailable(status.model_available); | |
| setAvailableModels(status.models || []); | |
| setConfigUrl(status.url); | |
| setConfigModel(status.model); | |
| } catch { | |
| setOllamaConnected(false); | |
| } | |
| setCheckingStatus(false); | |
| }; | |
| useEffect(() => { checkStatus(); }, []); | |
| const handleSaveConfig = async () => { | |
| setSaving(true); | |
| try { | |
| const res = await PageParseAPI.configureOllama(configUrl, configModel); | |
| setOllamaUrl(res.url); | |
| setOllamaModel(res.model); | |
| setConfigSaved(true); | |
| setTimeout(() => setConfigSaved(false), 2000); | |
| setTimeout(() => checkStatus(), 1000); | |
| } catch (err: any) { | |
| setError(err.message || 'Failed to save config'); | |
| } | |
| setSaving(false); | |
| }; | |
| const handleAnalyze = async () => { | |
| if (!code.trim()) return; | |
| setIsAnalyzing(true); | |
| setError(null); | |
| setResult(null); | |
| try { | |
| const res = await PageParseAPI.analyzeCode(code, selectedLanguage); | |
| setResult(res); | |
| } catch (err: any) { | |
| setError(err.message || 'Analysis failed'); | |
| } | |
| setIsAnalyzing(false); | |
| }; | |
| const handleCopy = async () => { | |
| if (!result) return; | |
| const text = `Language: ${result.language}\n\nExplanation:\n${result.explanation}\n\nSummary:\n${result.summary}`; | |
| await navigator.clipboard.writeText(text); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| }; | |
| const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { | |
| e.preventDefault(); | |
| handleAnalyze(); | |
| } | |
| }; | |
| const snippetCount = code.trim() ? code.trim().split(/\s+/).length : 0; | |
| const lineCount = code ? code.split('\n').length : 0; | |
| const IconComponent = result && LANG_ICONS[result.language] ? LANG_ICONS[result.language] : null; | |
| return ( | |
| <div className="section"> | |
| <div className="section-header"> | |
| <h3><Code size={16} className="text-secondary" /> Code Paste & Analyze</h3> | |
| <div className="flex gap-3 items-center"> | |
| <select value={selectedLanguage} onChange={e => setSelectedLanguage(e.target.value)} className="select"> | |
| {LANGUAGES.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)} | |
| </select> | |
| </div> | |
| </div> | |
| <div className="section-body"> | |
| <div className="ollama-status-bar"> | |
| <div className="flex items-center gap-2"> | |
| {checkingStatus ? ( | |
| <span className="text-xs text-tertiary flex items-center gap-1"> | |
| <span className="animate-pulse">Checking Ollama connection...</span> | |
| </span> | |
| ) : ollamaConnected ? ( | |
| <span className="text-xs flex items-center gap-1" style={{ color: 'var(--success)' }}> | |
| <Wifi size={12} /> | |
| <span> | |
| Ollama {ollamaVersion && `v${ollamaVersion}`} connected | |
| {modelAvailable | |
| ? ` (${ollamaModel})` | |
| : ` — model "${ollamaModel}" not pulled yet`} | |
| </span> | |
| {!modelAvailable && ( | |
| <span className="text-xs" style={{ color: 'var(--warning)' }}> | |
| Run: ollama pull {ollamaModel} | |
| </span> | |
| )} | |
| </span> | |
| ) : ( | |
| <span className="text-xs flex items-center gap-1" style={{ color: 'var(--danger)' }}> | |
| <WifiOff size={12} /> | |
| <span>Ollama not reachable at {ollamaUrl} — using local fallback</span> | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button className="btn btn-sm" onClick={checkStatus} title="Refresh connection status"> | |
| <RefreshCw size={12} /> | |
| </button> | |
| <button className="btn btn-sm" onClick={() => setShowConfig(!showConfig)} title="Configure Ollama"> | |
| <Settings size={12} /> | |
| </button> | |
| </div> | |
| </div> | |
| {showConfig && ( | |
| <div className="ollama-config-panel"> | |
| <div className="flex flex-col gap-3"> | |
| <div> | |
| <label className="text-xs font-semibold text-tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.5px' }}>Ollama URL</label> | |
| <input className="input" value={configUrl} onChange={e => setConfigUrl(e.target.value)} placeholder="http://localhost:11434" style={{ width: '100%', marginTop: 4 }} /> | |
| </div> | |
| <div> | |
| <label className="text-xs font-semibold text-tertiary" style={{ textTransform: 'uppercase', letterSpacing: '0.5px' }}>Model Name</label> | |
| <div className="flex gap-2 items-center" style={{ marginTop: 4 }}> | |
| <input className="input" value={configModel} onChange={e => setConfigModel(e.target.value)} placeholder="llama3.2:1b" style={{ flex: 1 }} list="ollama-models" /> | |
| <datalist id="ollama-models"> | |
| {availableModels.map(m => <option key={m} value={m} />)} | |
| </datalist> | |
| <button className="btn btn-primary btn-sm" onClick={handleSaveConfig} disabled={saving}> | |
| {saving ? 'Saving...' : configSaved ? <><Check size={12} /> Saved</> : 'Save'} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div className="code-editor-container" style={{ marginTop: 8 }}> | |
| <div className="code-editor-header"> | |
| <span className="text-xs text-tertiary"> | |
| {lineCount} lines · {snippetCount} tokens | |
| </span> | |
| <span className="text-xs text-tertiary"> | |
| {code.length > 0 && `${code.length} chars`} | |
| </span> | |
| </div> | |
| <textarea | |
| className="code-textarea" | |
| value={code} | |
| onChange={e => setCode(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder={`Paste or type code here...\n\nThe SLM will explain what it does and summarize its purpose.\n\nSupports: Python, JavaScript, TypeScript, Java, C/C++, Rust, Go, Ruby, Bash, HTML, CSS, SQL, and more.`} | |
| spellCheck={false} | |
| rows={14} | |
| /> | |
| <div className="code-editor-actions"> | |
| <button | |
| className="btn btn-primary" | |
| onClick={handleAnalyze} | |
| disabled={!code.trim() || isAnalyzing} | |
| > | |
| {isAnalyzing ? ( | |
| <span className="flex items-center gap-2"> | |
| <span className="animate-pulse">Analyzing...</span> | |
| </span> | |
| ) : ( | |
| <span className="flex items-center gap-2"> | |
| <Sparkles size={14} /> | |
| Explain & Summarize | |
| </span> | |
| )} | |
| </button> | |
| <span className="text-xs text-tertiary">Ctrl+Enter to run</span> | |
| </div> | |
| </div> | |
| {error && ( | |
| <div className="alert alert-error" style={{ marginTop: 16 }}> | |
| <span>{error}</span> | |
| </div> | |
| )} | |
| {result && ( | |
| <div className="code-analysis-result"> | |
| <div className="result-header"> | |
| <div className="result-language-badge"> | |
| {IconComponent ? ( | |
| <span className="flex items-center gap-1"> | |
| <IconComponent size={14} /> | |
| <span>{result.language.charAt(0).toUpperCase() + result.language.slice(1)}</span> | |
| </span> | |
| ) : ( | |
| <span className="flex items-center gap-1"> | |
| <FileCode size={14} /> | |
| <span>{result.language.charAt(0).toUpperCase() + result.language.slice(1)}</span> | |
| </span> | |
| )} | |
| </div> | |
| <button className="btn btn-sm" onClick={handleCopy} title="Copy analysis"> | |
| {copied ? <Check size={14} /> : <Copy size={14} />} | |
| </button> | |
| </div> | |
| <div className="result-summary"> | |
| <div className="result-label">Summary</div> | |
| <p className="result-text">{result.summary}</p> | |
| </div> | |
| <div className="result-explanation"> | |
| <div className="result-label">Explanation</div> | |
| <p className="result-text">{result.explanation}</p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |