diff --git a/README.md b/README.md index 58e230e..15a0382 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Get AI-driven analysis of your code covering: Fix bugs with confidence. The **Diff View** shows exactly what the AI changed, side-by-side comparison so you understand every modification before accepting. #### 🔒 Air-Gapped Privacy -Powered by **Phi-3 Mini** (2.3GB model) running locally through **Ollama**. Your code never touches the internet. +Powered by **Qwen2.5-Coder** (1.5GB model) running locally through **Ollama**. Your code never touches the internet. --- @@ -69,17 +69,21 @@ cd codepapi-ai docker-compose up -d ``` -That's it! Docker will automatically: -1. Pull and run the Ollama AI engine -2. Download the Phi-3 Mini model (first run only, ~2.3GB) -3. Start the NestJS backend API -4. Launch the React frontend UI +### First Launch Setup -### First Launch +> ⚠️ **Important:** The first startup requires downloading AI models. Ensure you have a stable internet connection. -> ⚠️ **Note:** The first startup will download the Phi-3 Mini model (~2.3GB). This is a one-time operation. Ensure you have a stable internet connection. +After starting the containers, pull the required models: -Once the containers are running: +```bash +# Pull Qwen2.5 Coder (primary model, ~1.5GB) +docker exec ollama ollama pull qwen2.5-coder:1.5b + +# Pull Phi-3 Mini (optional, ~2.3GB alternative model) +docker exec ollama ollama pull phi3:mini +``` + +Once the models are downloaded and containers are running: - **🖥️ Frontend:** Open http://localhost in your browser - **🔌 API:** Backend runs at http://localhost:3000 - **🤖 AI Engine:** Ollama API available at http://localhost:11434 @@ -103,7 +107,7 @@ Once the containers are running: | Component | Technology | Purpose | | --- | --- | --- | -| **AI Engine** | [Ollama](https://ollama.ai/) + Phi-3 Mini | Local LLM inference | +| **AI Engine** | [Ollama](https://ollama.ai/) + Qwen2.5-Coder | Local LLM inference | | **Orchestration** | LangChain.js | AI workflow management | | **Backend** | NestJS (Node.js) | REST API & business logic | | **Frontend** | React + TailwindCSS + Lucide | Modern, responsive UI | @@ -255,7 +259,7 @@ Before submitting a PR, ensure: While formal unit tests are encouraged: - **Manual testing** is acceptable for UI changes - **Test in Docker** to ensure consistency across environments -- **Test with the Phi-3 Mini model** (not a different LLM) +- **Test with the Qwen2.5-Coder model** (not a different LLM) - **Document test steps** in your PR ### Review Process @@ -345,7 +349,7 @@ See `frontend/README.md` for detailed customization guides. - **Docker & Docker Compose** (recommended) or - **Node.js 18+** + **Ollama** (for local development) -- **Minimum 4GB RAM** recommended (Phi-3 Mini model size) +- **Minimum 2GB RAM** recommended (Qwen2.5-Coder model size) - **Stable internet** for initial model download - **macOS, Linux, or Windows** (with WSL2) diff --git a/backend/src/converter/converter.service.ts b/backend/src/converter/converter.service.ts index 171a87a..6afa752 100644 --- a/backend/src/converter/converter.service.ts +++ b/backend/src/converter/converter.service.ts @@ -8,29 +8,22 @@ export class ConverterService { constructor() { this.model = new ChatOllama({ baseUrl: process.env.OLLAMA_URL || 'http://localhost:11434', - model: 'phi3:mini', + model: 'qwen2.5-coder:1.5b', + temperature: 0.1, + numPredict: 2048, + numCtx: 4096, + topK: 40, + topP: 0.9, + repeatPenalty: 1.1, + }); } // --- 1. CODE TRANSLATION --- async convertCode(code: string, from: string, to: string) { - // Check if "to" is a migration preset - const isMigration = to.includes('-') || from.includes('-'); - const prompt = ` - You are an expert software architect specializing in ${isMigration ? 'code migration' : 'code translation'}. + You are an expert software architect specializing in code translation. Task: Convert the input from ${from} to ${to}. - - ${ - isMigration - ? ` - SPECIFIC INSTRUCTIONS FOR MIGRATION: - - If moving from Class to Functional components, use React Hooks (useState, useEffect). - - If moving to TypeScript, add proper interfaces and types. - - If moving between frameworks (e.g., React to Vue), map lifecycle methods and state management accurately. - ` - : '' - } RULES: - Return ONLY raw code. diff --git a/docker-compose.yml b/docker-compose.yml index d27ecf2..96393e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,8 +9,7 @@ services: - "11434:11434" volumes: - ollama_data:/root/.ollama - entrypoint: /bin/sh - command: -c "ollama serve & sleep 5 && ollama pull phi3:mini && wait" + # 2. NestJS Backend backend: @@ -34,7 +33,7 @@ services: ports: - "80:80" depends_on: - - backend + - backend volumes: ollama_data: \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9e70de4..868c4c2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,8 +21,8 @@ function App() { // --- State --- const [sourceCode, setSourceCode] = useState('// Your code here...'); const [outputCode, setOutputCode] = useState(''); - const [sourceLang, setSourceLang] = useState('typescript'); - const [targetLang, setTargetLang] = useState(''); // Default to empty/placeholder + const [sourceLang, setSourceLang] = useState('javascript'); + const [targetLang, setTargetLang] = useState(''); const [mode, setMode] = useState('translate'); const [isProcessing, setIsProcessing] = useState(false); const [copied, setCopied] = useState(false); @@ -30,18 +30,10 @@ function App() { // --- Logic: Syncing & Placeholders --- useEffect(() => { - const isMigration = sourceLang.includes('-'); - + // If switching modes or source language, clear output when appropriate if (mode === 'translate') { - if (isMigration) { - // Auto-assign target language based on migration selection - if (sourceLang === 'react-ts') setTargetLang('typescript'); - else if (sourceLang === 'react-vue') setTargetLang('javascript'); - else setTargetLang('typescript'); // Default for most modern migrations - } else { - // If user switches back to a plain language, force them to pick a new target - setOutputCode(''); - } + // If user switches languages and there's no selected target, clear output + setOutputCode((prev) => (targetLang ? prev : '')); } // Set presentable text when switching modes @@ -54,17 +46,12 @@ function App() { } else if (mode === 'translate' && !targetLang) { setOutputCode(''); } - }, [sourceLang, mode, targetLang]); + }, [mode, targetLang]); // --- Helpers --- const getEditorLanguage = (langId: string) => { if (!langId) return 'javascript'; - if ( - langId.includes('react') || - langId.includes('vue') || - langId.includes('javascript') - ) - return 'typescript'; + if (langId === 'javascript' || langId === 'typescript') return 'typescript'; return langId; }; @@ -103,7 +90,7 @@ function App() { const handleAction = async () => { if (!sourceCode.trim() || sourceCode === '// Your code here...') return; if (mode === 'translate' && !targetLang) { - alert('Please select a target language or framework migration.'); + alert('Please select a target language.'); return; } @@ -116,16 +103,23 @@ function App() { } | null = null; if (mode === 'translate') { result = await translateCode(sourceCode, sourceLang, targetLang); - setOutputCode(stripCodeBlockFormatting(result.translatedCode)); + setOutputCode( + stripCodeBlockFormatting( + result?.translatedCode || 'No translation received.', + ), + ); } else if (mode === 'review') { result = await reviewCode(sourceCode, sourceLang); - setOutputCode(result.reviewContent); + setOutputCode(result?.reviewContent || 'No review content received.'); } else if (mode === 'fix') { result = await fixBugs(sourceCode, sourceLang); - setOutputCode(stripCodeBlockFormatting(result.fixedCode)); + setOutputCode( + stripCodeBlockFormatting( + result?.fixedCode || 'No fixed code received.', + ), + ); } } catch (error) { - console.error('API Error:', error); alert( 'AI Engine is currently unavailable. Ensure Docker containers are running.', ); @@ -171,7 +165,7 @@ function App() { className={`flex items-center gap-2 px-4 py-1.5 rounded-lg text-sm font-medium transition-all ${ mode === m.id ? 'bg-slate-800 text-blue-400 shadow-sm' - : 'text-slate-400 hover:text-slate-200' + : 'text-slate-400 hover:text-slate-200 cursor-pointer' }`} > {m.icon} {m.label} @@ -183,7 +177,7 @@ function App() { {/* Mobile menu toggle */} @@ -281,7 +275,7 @@ function App() { setOutputCode(''); setMobileNavOpen(false); }} - className="flex-1 py-2 rounded-md text-sm text-slate-300 hover:bg-slate-800" + className="flex-1 py-2 rounded-md text-sm text-slate-300 hover:bg-slate-800 cursor-pointer" > Reset @@ -292,7 +286,7 @@ function App() { setMobileNavOpen(false); }} disabled={isProcessing || (mode === 'translate' && !targetLang)} - className={`flex-1 py-2 rounded-md text-sm font-bold ${isProcessing || (mode === 'translate' && !targetLang) ? 'bg-slate-700 opacity-50' : 'bg-blue-600'}`} + className={`flex-1 py-2 rounded-md text-sm font-bold ${isProcessing || (mode === 'translate' && !targetLang) ? 'bg-slate-700 opacity-50' : 'bg-blue-600 cursor-pointer'}`} > Run AI @@ -309,7 +303,7 @@ function App() { className={isProcessing ? 'opacity-40 pointer-events-none' : ''} > @@ -325,7 +319,7 @@ function App() { type="button" onClick={handlePasteFromClipboard} disabled={isProcessing} - className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-800 text-sm" + className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-800 text-sm cursor-pointer disabled:cursor-not-allowed" title="Paste from clipboard" > Paste @@ -334,7 +328,7 @@ function App() { type="button" onClick={handleClearInput} disabled={isProcessing} - className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-800 text-sm" + className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-800 text-sm cursor-pointer disabled:cursor-not-allowed" title="Clear input" > Clear @@ -369,7 +363,7 @@ function App() { label="Target Language" value={targetLang} onChange={setTargetLang} - excludeId={sourceLang} // FILTERING: Can't pick the same lang as source + excludeId={sourceLang} /> ) : (
@@ -393,7 +387,7 @@ function App() { setCopied(true); setTimeout(() => setCopied(false), 2000); }} - className="mb-1 p-2.5 hover:bg-slate-800 rounded-lg transition-all text-slate-400 flex items-center gap-2 text-sm disabled:opacity-10" + className="mb-1 p-2.5 hover:bg-slate-800 rounded-lg transition-all text-slate-400 flex items-center gap-2 text-sm disabled:opacity-10 cursor-pointer disabled:cursor-not-allowed" > {copied ? ( @@ -443,6 +437,7 @@ function App() { ? getEditorLanguage(targetLang) : 'markdown' } + defaultLanguage="markdown" value={outputCode} options={{ readOnly: true, diff --git a/frontend/src/components/FileUploader.tsx b/frontend/src/components/FileUploader.tsx index 88f6e6a..6be4318 100644 --- a/frontend/src/components/FileUploader.tsx +++ b/frontend/src/components/FileUploader.tsx @@ -1,4 +1,5 @@ -import React, { useRef, useState } from 'react'; +import { UploadCloud } from 'lucide-react'; +import { useRef, useState } from 'react'; interface Props { onFileRead: (content: string, filename?: string) => void; @@ -56,27 +57,12 @@ const FileUploader = ({ onFileRead, disabled }: Props) => { }} disabled={disabled} className={`flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors text-sm font-medium ${ - disabled ? 'opacity-50 pointer-events-none' : 'hover:bg-slate-800' + disabled + ? 'opacity-50 pointer-events-none' + : 'hover:bg-slate-800 cursor-pointer' } ${dragOver ? 'bg-slate-800' : ''}`} > - - Upload file - - - - + {disabled ? 'Upload (disabled)' : 'Upload / Drop'}
diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx index 2fa5bf5..36d39fb 100644 --- a/frontend/src/components/LanguageSelector.tsx +++ b/frontend/src/components/LanguageSelector.tsx @@ -1,4 +1,4 @@ -import { LANGUAGES, MIGRATIONS } from '../constants/languages'; +import { LANGUAGES } from '../constants/languages'; interface Props { value: string; @@ -33,21 +33,11 @@ export const LanguageSelector = ({ > - - {LANGUAGES.filter((l) => l.id !== excludeId).map((lang) => ( - - ))} - - - - {MIGRATIONS.filter((m) => m.id !== excludeId).map((mig) => ( - - ))} - + {LANGUAGES.filter((l) => l.id !== excludeId).map((lang) => ( + + ))} ); diff --git a/frontend/src/constants/languages.ts b/frontend/src/constants/languages.ts index b048f64..befcf32 100644 --- a/frontend/src/constants/languages.ts +++ b/frontend/src/constants/languages.ts @@ -11,10 +11,3 @@ export const LANGUAGES = [ { id: 'ruby', name: 'Ruby' }, { id: 'swift', name: 'Swift' }, ]; - -export const MIGRATIONS = [ - { id: 'react-vue', name: 'React to Vue' }, - { id: 'react-ts', name: 'React JS to React TS' }, - { id: 'react-class-functional', name: 'React Class to Functional' }, - { id: 'angular-react', name: 'Angular to React' }, -];