Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand All @@ -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
Expand All @@ -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 |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
25 changes: 9 additions & 16 deletions backend/src/converter/converter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 2 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -34,7 +33,7 @@ services:
ports:
- "80:80"
depends_on:
- backend
- backend

volumes:
ollama_data:
69 changes: 32 additions & 37 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,19 @@ 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<Mode>('translate');
const [isProcessing, setIsProcessing] = useState(false);
const [copied, setCopied] = useState(false);
const [mobileNavOpen, setMobileNavOpen] = useState(false);

// --- 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
Expand All @@ -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;
};

Expand Down Expand Up @@ -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;
}

Expand All @@ -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.',
);
Expand Down Expand Up @@ -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}
Expand All @@ -183,7 +177,7 @@ function App() {
{/* Mobile menu toggle */}
<button
type="button"
className="sm:hidden p-2 rounded-md text-slate-300 hover:bg-slate-800"
className="sm:hidden p-2 rounded-md text-slate-300 hover:bg-slate-800 cursor-pointer"
onClick={() => setMobileNavOpen((v) => !v)}
aria-label="Toggle menu"
>
Expand Down Expand Up @@ -222,7 +216,7 @@ function App() {
setSourceCode('// Your code here...');
setOutputCode('');
}}
className="p-2 hover:bg-slate-800 rounded-lg transition-colors text-slate-400 disabled:opacity-20"
className="p-2 hover:bg-slate-800 rounded-lg transition-colors text-slate-400 disabled:opacity-20 cursor-pointer disabled:cursor-not-allowed"
title="Reset All"
>
<RotateCcw size={20} />
Expand All @@ -233,7 +227,7 @@ function App() {
className={`flex items-center gap-2 px-6 py-2 rounded-full font-bold transition-all ${
isProcessing || (mode === 'translate' && !targetLang)
? 'bg-slate-700 opacity-50 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-500 shadow-lg shadow-blue-900/20 active:scale-95'
: 'bg-blue-600 hover:bg-blue-500 shadow-lg shadow-blue-900/20 active:scale-95 cursor-pointer'
}`}
onClick={handleAction}
>
Expand Down Expand Up @@ -266,7 +260,7 @@ function App() {
setMode(m.id as Mode);
setMobileNavOpen(false);
}}
className={`flex-1 text-sm py-2 rounded-md ${mode === m.id ? 'bg-slate-800 text-blue-400' : 'text-slate-300'}`}
className={`flex-1 text-sm py-2 rounded-md ${mode === m.id ? 'bg-slate-800 text-blue-400' : 'text-slate-300'} cursor-pointer`}
>
{m.label}
</button>
Expand All @@ -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
</button>
Expand All @@ -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
</button>
Expand All @@ -309,7 +303,7 @@ function App() {
className={isProcessing ? 'opacity-40 pointer-events-none' : ''}
>
<LanguageSelector
label="Source Language / Framework"
label="Source Language"
value={sourceLang}
onChange={setSourceLang}
/>
Expand All @@ -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"
>
<Clipboard size={14} /> Paste
Expand All @@ -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
Expand Down Expand Up @@ -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}
/>
) : (
<div className="flex flex-col gap-1.5 w-full">
Expand All @@ -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 ? (
<Check size={16} className="text-green-500" />
Expand Down Expand Up @@ -443,6 +437,7 @@ function App() {
? getEditorLanguage(targetLang)
: 'markdown'
}
defaultLanguage="markdown"
value={outputCode}
options={{
readOnly: true,
Expand Down
26 changes: 6 additions & 20 deletions frontend/src/components/FileUploader.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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' : ''}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="text-slate-300"
role="img"
>
<title>Upload file</title>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 5 17 10" />
<line x1="12" y1="5" x2="12" y2="19" />
</svg>
<UploadCloud size={16} className="text-slate-300" role="img" />
<span>{disabled ? 'Upload (disabled)' : 'Upload / Drop'}</span>
</button>
</div>
Expand Down
Loading