Skip to content

Commit be5b71e

Browse files
authored
Merge pull request #6 from CodePapi/fe-modularization
Fe modularization
2 parents 62d21c5 + 2f77fe4 commit be5b71e

File tree

7 files changed

+71
-111
lines changed

7 files changed

+71
-111
lines changed

README.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Get AI-driven analysis of your code covering:
4646
Fix bugs with confidence. The **Diff View** shows exactly what the AI changed, side-by-side comparison so you understand every modification before accepting.
4747

4848
#### 🔒 Air-Gapped Privacy
49-
Powered by **Phi-3 Mini** (2.3GB model) running locally through **Ollama**. Your code never touches the internet.
49+
Powered by **Qwen2.5-Coder** (1.5GB model) running locally through **Ollama**. Your code never touches the internet.
5050

5151
---
5252

@@ -69,17 +69,21 @@ cd codepapi-ai
6969
docker-compose up -d
7070
```
7171

72-
That's it! Docker will automatically:
73-
1. Pull and run the Ollama AI engine
74-
2. Download the Phi-3 Mini model (first run only, ~2.3GB)
75-
3. Start the NestJS backend API
76-
4. Launch the React frontend UI
72+
### First Launch Setup
7773

78-
### First Launch
74+
> ⚠️ **Important:** The first startup requires downloading AI models. Ensure you have a stable internet connection.
7975
80-
> ⚠️ **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.
76+
After starting the containers, pull the required models:
8177

82-
Once the containers are running:
78+
```bash
79+
# Pull Qwen2.5 Coder (primary model, ~1.5GB)
80+
docker exec ollama ollama pull qwen2.5-coder:1.5b
81+
82+
# Pull Phi-3 Mini (optional, ~2.3GB alternative model)
83+
docker exec ollama ollama pull phi3:mini
84+
```
85+
86+
Once the models are downloaded and containers are running:
8387
- **🖥️ Frontend:** Open http://localhost in your browser
8488
- **🔌 API:** Backend runs at http://localhost:3000
8589
- **🤖 AI Engine:** Ollama API available at http://localhost:11434
@@ -103,7 +107,7 @@ Once the containers are running:
103107

104108
| Component | Technology | Purpose |
105109
| --- | --- | --- |
106-
| **AI Engine** | [Ollama](https://ollama.ai/) + Phi-3 Mini | Local LLM inference |
110+
| **AI Engine** | [Ollama](https://ollama.ai/) + Qwen2.5-Coder | Local LLM inference |
107111
| **Orchestration** | LangChain.js | AI workflow management |
108112
| **Backend** | NestJS (Node.js) | REST API & business logic |
109113
| **Frontend** | React + TailwindCSS + Lucide | Modern, responsive UI |
@@ -255,7 +259,7 @@ Before submitting a PR, ensure:
255259
While formal unit tests are encouraged:
256260
- **Manual testing** is acceptable for UI changes
257261
- **Test in Docker** to ensure consistency across environments
258-
- **Test with the Phi-3 Mini model** (not a different LLM)
262+
- **Test with the Qwen2.5-Coder model** (not a different LLM)
259263
- **Document test steps** in your PR
260264

261265
### Review Process
@@ -345,7 +349,7 @@ See `frontend/README.md` for detailed customization guides.
345349

346350
- **Docker & Docker Compose** (recommended) or
347351
- **Node.js 18+** + **Ollama** (for local development)
348-
- **Minimum 4GB RAM** recommended (Phi-3 Mini model size)
352+
- **Minimum 2GB RAM** recommended (Qwen2.5-Coder model size)
349353
- **Stable internet** for initial model download
350354
- **macOS, Linux, or Windows** (with WSL2)
351355

backend/src/converter/converter.service.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,22 @@ export class ConverterService {
88
constructor() {
99
this.model = new ChatOllama({
1010
baseUrl: process.env.OLLAMA_URL || 'http://localhost:11434',
11-
model: 'phi3:mini',
11+
model: 'qwen2.5-coder:1.5b',
12+
temperature: 0.1,
13+
numPredict: 2048,
14+
numCtx: 4096,
15+
topK: 40,
16+
topP: 0.9,
17+
repeatPenalty: 1.1,
18+
1219
});
1320
}
1421

1522
// --- 1. CODE TRANSLATION ---
1623
async convertCode(code: string, from: string, to: string) {
17-
// Check if "to" is a migration preset
18-
const isMigration = to.includes('-') || from.includes('-');
19-
2024
const prompt = `
21-
You are an expert software architect specializing in ${isMigration ? 'code migration' : 'code translation'}.
25+
You are an expert software architect specializing in code translation.
2226
Task: Convert the input from ${from} to ${to}.
23-
24-
${
25-
isMigration
26-
? `
27-
SPECIFIC INSTRUCTIONS FOR MIGRATION:
28-
- If moving from Class to Functional components, use React Hooks (useState, useEffect).
29-
- If moving to TypeScript, add proper interfaces and types.
30-
- If moving between frameworks (e.g., React to Vue), map lifecycle methods and state management accurately.
31-
`
32-
: ''
33-
}
3427
3528
RULES:
3629
- Return ONLY raw code.

docker-compose.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ services:
99
- "11434:11434"
1010
volumes:
1111
- ollama_data:/root/.ollama
12-
entrypoint: /bin/sh
13-
command: -c "ollama serve & sleep 5 && ollama pull phi3:mini && wait"
12+
1413

1514
# 2. NestJS Backend
1615
backend:
@@ -34,7 +33,7 @@ services:
3433
ports:
3534
- "80:80"
3635
depends_on:
37-
- backend
36+
- backend
3837

3938
volumes:
4039
ollama_data:

frontend/src/App.tsx

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,19 @@ function App() {
2121
// --- State ---
2222
const [sourceCode, setSourceCode] = useState('// Your code here...');
2323
const [outputCode, setOutputCode] = useState('');
24-
const [sourceLang, setSourceLang] = useState('typescript');
25-
const [targetLang, setTargetLang] = useState(''); // Default to empty/placeholder
24+
const [sourceLang, setSourceLang] = useState('javascript');
25+
const [targetLang, setTargetLang] = useState('');
2626
const [mode, setMode] = useState<Mode>('translate');
2727
const [isProcessing, setIsProcessing] = useState(false);
2828
const [copied, setCopied] = useState(false);
2929
const [mobileNavOpen, setMobileNavOpen] = useState(false);
3030

3131
// --- Logic: Syncing & Placeholders ---
3232
useEffect(() => {
33-
const isMigration = sourceLang.includes('-');
34-
33+
// If switching modes or source language, clear output when appropriate
3534
if (mode === 'translate') {
36-
if (isMigration) {
37-
// Auto-assign target language based on migration selection
38-
if (sourceLang === 'react-ts') setTargetLang('typescript');
39-
else if (sourceLang === 'react-vue') setTargetLang('javascript');
40-
else setTargetLang('typescript'); // Default for most modern migrations
41-
} else {
42-
// If user switches back to a plain language, force them to pick a new target
43-
setOutputCode('');
44-
}
35+
// If user switches languages and there's no selected target, clear output
36+
setOutputCode((prev) => (targetLang ? prev : ''));
4537
}
4638

4739
// Set presentable text when switching modes
@@ -54,17 +46,12 @@ function App() {
5446
} else if (mode === 'translate' && !targetLang) {
5547
setOutputCode('');
5648
}
57-
}, [sourceLang, mode, targetLang]);
49+
}, [mode, targetLang]);
5850

5951
// --- Helpers ---
6052
const getEditorLanguage = (langId: string) => {
6153
if (!langId) return 'javascript';
62-
if (
63-
langId.includes('react') ||
64-
langId.includes('vue') ||
65-
langId.includes('javascript')
66-
)
67-
return 'typescript';
54+
if (langId === 'javascript' || langId === 'typescript') return 'typescript';
6855
return langId;
6956
};
7057

@@ -103,7 +90,7 @@ function App() {
10390
const handleAction = async () => {
10491
if (!sourceCode.trim() || sourceCode === '// Your code here...') return;
10592
if (mode === 'translate' && !targetLang) {
106-
alert('Please select a target language or framework migration.');
93+
alert('Please select a target language.');
10794
return;
10895
}
10996

@@ -116,16 +103,23 @@ function App() {
116103
} | null = null;
117104
if (mode === 'translate') {
118105
result = await translateCode(sourceCode, sourceLang, targetLang);
119-
setOutputCode(stripCodeBlockFormatting(result.translatedCode));
106+
setOutputCode(
107+
stripCodeBlockFormatting(
108+
result?.translatedCode || 'No translation received.',
109+
),
110+
);
120111
} else if (mode === 'review') {
121112
result = await reviewCode(sourceCode, sourceLang);
122-
setOutputCode(result.reviewContent);
113+
setOutputCode(result?.reviewContent || 'No review content received.');
123114
} else if (mode === 'fix') {
124115
result = await fixBugs(sourceCode, sourceLang);
125-
setOutputCode(stripCodeBlockFormatting(result.fixedCode));
116+
setOutputCode(
117+
stripCodeBlockFormatting(
118+
result?.fixedCode || 'No fixed code received.',
119+
),
120+
);
126121
}
127122
} catch (error) {
128-
console.error('API Error:', error);
129123
alert(
130124
'AI Engine is currently unavailable. Ensure Docker containers are running.',
131125
);
@@ -171,7 +165,7 @@ function App() {
171165
className={`flex items-center gap-2 px-4 py-1.5 rounded-lg text-sm font-medium transition-all ${
172166
mode === m.id
173167
? 'bg-slate-800 text-blue-400 shadow-sm'
174-
: 'text-slate-400 hover:text-slate-200'
168+
: 'text-slate-400 hover:text-slate-200 cursor-pointer'
175169
}`}
176170
>
177171
{m.icon} {m.label}
@@ -183,7 +177,7 @@ function App() {
183177
{/* Mobile menu toggle */}
184178
<button
185179
type="button"
186-
className="sm:hidden p-2 rounded-md text-slate-300 hover:bg-slate-800"
180+
className="sm:hidden p-2 rounded-md text-slate-300 hover:bg-slate-800 cursor-pointer"
187181
onClick={() => setMobileNavOpen((v) => !v)}
188182
aria-label="Toggle menu"
189183
>
@@ -222,7 +216,7 @@ function App() {
222216
setSourceCode('// Your code here...');
223217
setOutputCode('');
224218
}}
225-
className="p-2 hover:bg-slate-800 rounded-lg transition-colors text-slate-400 disabled:opacity-20"
219+
className="p-2 hover:bg-slate-800 rounded-lg transition-colors text-slate-400 disabled:opacity-20 cursor-pointer disabled:cursor-not-allowed"
226220
title="Reset All"
227221
>
228222
<RotateCcw size={20} />
@@ -233,7 +227,7 @@ function App() {
233227
className={`flex items-center gap-2 px-6 py-2 rounded-full font-bold transition-all ${
234228
isProcessing || (mode === 'translate' && !targetLang)
235229
? 'bg-slate-700 opacity-50 cursor-not-allowed'
236-
: 'bg-blue-600 hover:bg-blue-500 shadow-lg shadow-blue-900/20 active:scale-95'
230+
: 'bg-blue-600 hover:bg-blue-500 shadow-lg shadow-blue-900/20 active:scale-95 cursor-pointer'
237231
}`}
238232
onClick={handleAction}
239233
>
@@ -266,7 +260,7 @@ function App() {
266260
setMode(m.id as Mode);
267261
setMobileNavOpen(false);
268262
}}
269-
className={`flex-1 text-sm py-2 rounded-md ${mode === m.id ? 'bg-slate-800 text-blue-400' : 'text-slate-300'}`}
263+
className={`flex-1 text-sm py-2 rounded-md ${mode === m.id ? 'bg-slate-800 text-blue-400' : 'text-slate-300'} cursor-pointer`}
270264
>
271265
{m.label}
272266
</button>
@@ -281,7 +275,7 @@ function App() {
281275
setOutputCode('');
282276
setMobileNavOpen(false);
283277
}}
284-
className="flex-1 py-2 rounded-md text-sm text-slate-300 hover:bg-slate-800"
278+
className="flex-1 py-2 rounded-md text-sm text-slate-300 hover:bg-slate-800 cursor-pointer"
285279
>
286280
Reset
287281
</button>
@@ -292,7 +286,7 @@ function App() {
292286
setMobileNavOpen(false);
293287
}}
294288
disabled={isProcessing || (mode === 'translate' && !targetLang)}
295-
className={`flex-1 py-2 rounded-md text-sm font-bold ${isProcessing || (mode === 'translate' && !targetLang) ? 'bg-slate-700 opacity-50' : 'bg-blue-600'}`}
289+
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'}`}
296290
>
297291
Run AI
298292
</button>
@@ -309,7 +303,7 @@ function App() {
309303
className={isProcessing ? 'opacity-40 pointer-events-none' : ''}
310304
>
311305
<LanguageSelector
312-
label="Source Language / Framework"
306+
label="Source Language"
313307
value={sourceLang}
314308
onChange={setSourceLang}
315309
/>
@@ -325,7 +319,7 @@ function App() {
325319
type="button"
326320
onClick={handlePasteFromClipboard}
327321
disabled={isProcessing}
328-
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-800 text-sm"
322+
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"
329323
title="Paste from clipboard"
330324
>
331325
<Clipboard size={14} /> Paste
@@ -334,7 +328,7 @@ function App() {
334328
type="button"
335329
onClick={handleClearInput}
336330
disabled={isProcessing}
337-
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-slate-800 text-sm"
331+
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"
338332
title="Clear input"
339333
>
340334
Clear
@@ -369,7 +363,7 @@ function App() {
369363
label="Target Language"
370364
value={targetLang}
371365
onChange={setTargetLang}
372-
excludeId={sourceLang} // FILTERING: Can't pick the same lang as source
366+
excludeId={sourceLang}
373367
/>
374368
) : (
375369
<div className="flex flex-col gap-1.5 w-full">
@@ -393,7 +387,7 @@ function App() {
393387
setCopied(true);
394388
setTimeout(() => setCopied(false), 2000);
395389
}}
396-
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"
390+
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"
397391
>
398392
{copied ? (
399393
<Check size={16} className="text-green-500" />
@@ -443,6 +437,7 @@ function App() {
443437
? getEditorLanguage(targetLang)
444438
: 'markdown'
445439
}
440+
defaultLanguage="markdown"
446441
value={outputCode}
447442
options={{
448443
readOnly: true,

frontend/src/components/FileUploader.tsx

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, { useRef, useState } from 'react';
1+
import { UploadCloud } from 'lucide-react';
2+
import { useRef, useState } from 'react';
23

34
interface Props {
45
onFileRead: (content: string, filename?: string) => void;
@@ -56,27 +57,12 @@ const FileUploader = ({ onFileRead, disabled }: Props) => {
5657
}}
5758
disabled={disabled}
5859
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors text-sm font-medium ${
59-
disabled ? 'opacity-50 pointer-events-none' : 'hover:bg-slate-800'
60+
disabled
61+
? 'opacity-50 pointer-events-none'
62+
: 'hover:bg-slate-800 cursor-pointer'
6063
} ${dragOver ? 'bg-slate-800' : ''}`}
6164
>
62-
<svg
63-
xmlns="http://www.w3.org/2000/svg"
64-
width="16"
65-
height="16"
66-
viewBox="0 0 24 24"
67-
fill="none"
68-
stroke="currentColor"
69-
strokeWidth={2}
70-
strokeLinecap="round"
71-
strokeLinejoin="round"
72-
className="text-slate-300"
73-
role="img"
74-
>
75-
<title>Upload file</title>
76-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
77-
<polyline points="7 10 12 5 17 10" />
78-
<line x1="12" y1="5" x2="12" y2="19" />
79-
</svg>
65+
<UploadCloud size={16} className="text-slate-300" role="img" />
8066
<span>{disabled ? 'Upload (disabled)' : 'Upload / Drop'}</span>
8167
</button>
8268
</div>

0 commit comments

Comments
 (0)