Spaces:
Runtime error
Runtime error
Upload 24 files
Browse files- Dockerfile +20 -0
- README.md +29 -11
- components/APIKeyInput.tsx +16 -0
- components/CodeBlock.tsx +50 -0
- components/LanguageSelect.tsx +77 -0
- components/ModelSelect.tsx +24 -0
- components/TextBlock.tsx +21 -0
- docker-compose.yml +17 -0
- next.config.js +6 -0
- package-lock.json +0 -0
- package.json +33 -0
- pages/_app.tsx +15 -0
- pages/_document.tsx +13 -0
- pages/api/translate.ts +28 -0
- pages/index.tsx +225 -0
- postcss.config.js +6 -0
- prettier.config.js +5 -0
- public/favicon.ico +0 -0
- public/screenshot.png +0 -0
- styles/globals.css +3 -0
- tailwind.config.js +8 -0
- tsconfig.json +23 -0
- types/types.ts +13 -0
- utils/index.ts +143 -0
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use the official Node.js image as the base image
|
2 |
+
FROM node:14
|
3 |
+
|
4 |
+
# Set the working directory inside the container
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy package.json and package-lock.json to the working directory
|
8 |
+
COPY package*.json ./
|
9 |
+
|
10 |
+
# Install the dependencies
|
11 |
+
RUN npm ci
|
12 |
+
|
13 |
+
# Copy the rest of the application code to the working directory
|
14 |
+
COPY . .
|
15 |
+
|
16 |
+
# Expose the port the app will run on
|
17 |
+
EXPOSE 3001
|
18 |
+
|
19 |
+
# Start the application
|
20 |
+
CMD ["npm", "run", "dev"]
|
README.md
CHANGED
@@ -1,11 +1,29 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AI Code Translator
|
2 |
+
|
3 |
+
Use AI to translate code from one language to another.
|
4 |
+
|
5 |
+

|
6 |
+
|
7 |
+
## Running Locally
|
8 |
+
|
9 |
+
**1. Clone Repo**
|
10 |
+
|
11 |
+
```bash
|
12 |
+
git clone https://github.com/mckaywrigley/ai-code-translator.git
|
13 |
+
```
|
14 |
+
|
15 |
+
**2. Install Dependencies**
|
16 |
+
|
17 |
+
```bash
|
18 |
+
npm i
|
19 |
+
```
|
20 |
+
|
21 |
+
**3. Run App**
|
22 |
+
|
23 |
+
```bash
|
24 |
+
npm run dev
|
25 |
+
```
|
26 |
+
|
27 |
+
## Contact
|
28 |
+
|
29 |
+
If you have any questions, feel free to reach out to me on [Twitter](https://twitter.com/mckaywrigley).
|
components/APIKeyInput.tsx
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
interface Props {
|
2 |
+
apiKey: string;
|
3 |
+
onChange: (apiKey: string) => void;
|
4 |
+
}
|
5 |
+
|
6 |
+
export const APIKeyInput: React.FC<Props> = ({ apiKey, onChange }) => {
|
7 |
+
return (
|
8 |
+
<input
|
9 |
+
className="mt-1 h-[24px] w-[280px] rounded-md border border-gray-300 px-3 py-2 text-black shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
|
10 |
+
type="password"
|
11 |
+
placeholder="OpenAI API Key"
|
12 |
+
value={apiKey}
|
13 |
+
onChange={(e) => onChange(e.target.value)}
|
14 |
+
/>
|
15 |
+
);
|
16 |
+
};
|
components/CodeBlock.tsx
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { StreamLanguage } from '@codemirror/language';
|
2 |
+
import { go } from '@codemirror/legacy-modes/mode/go';
|
3 |
+
import { tokyoNight } from '@uiw/codemirror-theme-tokyo-night';
|
4 |
+
import CodeMirror from '@uiw/react-codemirror';
|
5 |
+
import { FC, useEffect, useState } from 'react';
|
6 |
+
|
7 |
+
interface Props {
|
8 |
+
code: string;
|
9 |
+
editable?: boolean;
|
10 |
+
onChange?: (value: string) => void;
|
11 |
+
}
|
12 |
+
|
13 |
+
export const CodeBlock: FC<Props> = ({
|
14 |
+
code,
|
15 |
+
editable = false,
|
16 |
+
onChange = () => {},
|
17 |
+
}) => {
|
18 |
+
const [copyText, setCopyText] = useState<string>('Copy');
|
19 |
+
|
20 |
+
useEffect(() => {
|
21 |
+
const timeout = setTimeout(() => {
|
22 |
+
setCopyText('Copy');
|
23 |
+
}, 2000);
|
24 |
+
|
25 |
+
return () => clearTimeout(timeout);
|
26 |
+
}, [copyText]);
|
27 |
+
|
28 |
+
return (
|
29 |
+
<div className="relative">
|
30 |
+
<button
|
31 |
+
className="absolute right-0 top-0 z-10 rounded bg-[#1A1B26] p-1 text-xs text-white hover:bg-[#2D2E3A] active:bg-[#2D2E3A]"
|
32 |
+
onClick={() => {
|
33 |
+
navigator.clipboard.writeText(code);
|
34 |
+
setCopyText('Copied!');
|
35 |
+
}}
|
36 |
+
>
|
37 |
+
{copyText}
|
38 |
+
</button>
|
39 |
+
|
40 |
+
<CodeMirror
|
41 |
+
editable={editable}
|
42 |
+
value={code}
|
43 |
+
minHeight="500px"
|
44 |
+
extensions={[StreamLanguage.define(go)]}
|
45 |
+
theme={tokyoNight}
|
46 |
+
onChange={(value) => onChange(value)}
|
47 |
+
/>
|
48 |
+
</div>
|
49 |
+
);
|
50 |
+
};
|
components/LanguageSelect.tsx
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { FC } from 'react';
|
2 |
+
|
3 |
+
interface Props {
|
4 |
+
language: string;
|
5 |
+
onChange: (language: string) => void;
|
6 |
+
}
|
7 |
+
|
8 |
+
export const LanguageSelect: FC<Props> = ({ language, onChange }) => {
|
9 |
+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
10 |
+
onChange(e.target.value);
|
11 |
+
};
|
12 |
+
|
13 |
+
return (
|
14 |
+
<select
|
15 |
+
className="w-full rounded-md bg-[#1F2937] px-4 py-2 text-neutral-200"
|
16 |
+
value={language}
|
17 |
+
onChange={handleChange}
|
18 |
+
>
|
19 |
+
{languages
|
20 |
+
.sort((a, b) => a.label.localeCompare(b.label))
|
21 |
+
.map((language) => (
|
22 |
+
<option key={language.value} value={language.value}>
|
23 |
+
{language.label}
|
24 |
+
</option>
|
25 |
+
))}
|
26 |
+
</select>
|
27 |
+
);
|
28 |
+
};
|
29 |
+
|
30 |
+
const languages = [
|
31 |
+
{ value: 'Pascal', label: 'Pascal' },
|
32 |
+
{ value: 'JavaScript', label: 'JavaScript' },
|
33 |
+
{ value: 'TypeScript', label: 'TypeScript' },
|
34 |
+
{ value: 'Python', label: 'Python' },
|
35 |
+
{ value: 'TSX', label: 'TSX' },
|
36 |
+
{ value: 'JSX', label: 'JSX' },
|
37 |
+
{ value: 'Vue', label: 'Vue' },
|
38 |
+
{ value: 'Go', label: 'Go' },
|
39 |
+
{ value: 'C', label: 'C' },
|
40 |
+
{ value: 'C++', label: 'C++' },
|
41 |
+
{ value: 'Java', label: 'Java' },
|
42 |
+
{ value: 'C#', label: 'C#' },
|
43 |
+
{ value: 'Visual Basic .NET', label: 'Visual Basic .NET' },
|
44 |
+
{ value: 'SQL', label: 'SQL' },
|
45 |
+
{ value: 'Assembly Language', label: 'Assembly Language' },
|
46 |
+
{ value: 'PHP', label: 'PHP' },
|
47 |
+
{ value: 'Ruby', label: 'Ruby' },
|
48 |
+
{ value: 'Swift', label: 'Swift' },
|
49 |
+
{ value: 'SwiftUI', label: 'SwiftUI' },
|
50 |
+
{ value: 'Kotlin', label: 'Kotlin' },
|
51 |
+
{ value: 'R', label: 'R' },
|
52 |
+
{ value: 'Objective-C', label: 'Objective-C' },
|
53 |
+
{ value: 'Perl', label: 'Perl' },
|
54 |
+
{ value: 'SAS', label: 'SAS' },
|
55 |
+
{ value: 'Scala', label: 'Scala' },
|
56 |
+
{ value: 'Dart', label: 'Dart' },
|
57 |
+
{ value: 'Rust', label: 'Rust' },
|
58 |
+
{ value: 'Haskell', label: 'Haskell' },
|
59 |
+
{ value: 'Lua', label: 'Lua' },
|
60 |
+
{ value: 'Groovy', label: 'Groovy' },
|
61 |
+
{ value: 'Elixir', label: 'Elixir' },
|
62 |
+
{ value: 'Clojure', label: 'Clojure' },
|
63 |
+
{ value: 'Lisp', label: 'Lisp' },
|
64 |
+
{ value: 'Julia', label: 'Julia' },
|
65 |
+
{ value: 'Matlab', label: 'Matlab' },
|
66 |
+
{ value: 'Fortran', label: 'Fortran' },
|
67 |
+
{ value: 'COBOL', label: 'COBOL' },
|
68 |
+
{ value: 'Bash', label: 'Bash' },
|
69 |
+
{ value: 'Powershell', label: 'Powershell' },
|
70 |
+
{ value: 'PL/SQL', label: 'PL/SQL' },
|
71 |
+
{ value: 'CSS', label: 'CSS' },
|
72 |
+
{ value: 'Racket', label: 'Racket' },
|
73 |
+
{ value: 'HTML', label: 'HTML' },
|
74 |
+
{ value: 'NoSQL', label: 'NoSQL' },
|
75 |
+
{ value: 'Natural Language', label: 'Natural Language' },
|
76 |
+
{ value: 'CoffeeScript', label: 'CoffeeScript' },
|
77 |
+
];
|
components/ModelSelect.tsx
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { OpenAIModel } from '@/types/types';
|
2 |
+
import { FC } from 'react';
|
3 |
+
|
4 |
+
interface Props {
|
5 |
+
model: OpenAIModel;
|
6 |
+
onChange: (model: OpenAIModel) => void;
|
7 |
+
}
|
8 |
+
|
9 |
+
export const ModelSelect: FC<Props> = ({ model, onChange }) => {
|
10 |
+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
11 |
+
onChange(e.target.value as OpenAIModel);
|
12 |
+
};
|
13 |
+
|
14 |
+
return (
|
15 |
+
<select
|
16 |
+
className="h-[40px] w-[140px] rounded-md bg-[#1F2937] px-4 py-2 text-neutral-200"
|
17 |
+
value={model}
|
18 |
+
onChange={handleChange}
|
19 |
+
>
|
20 |
+
<option value="gpt-3.5-turbo">GPT-3.5</option>
|
21 |
+
<option value="gpt-4">GPT-4</option>
|
22 |
+
</select>
|
23 |
+
);
|
24 |
+
};
|
components/TextBlock.tsx
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
interface Props {
|
2 |
+
text: string;
|
3 |
+
editable?: boolean;
|
4 |
+
onChange?: (value: string) => void;
|
5 |
+
}
|
6 |
+
|
7 |
+
export const TextBlock: React.FC<Props> = ({
|
8 |
+
text,
|
9 |
+
editable = false,
|
10 |
+
onChange = () => {},
|
11 |
+
}) => {
|
12 |
+
return (
|
13 |
+
<textarea
|
14 |
+
className="min-h-[500px] w-full bg-[#1A1B26] p-4 text-[15px] text-neutral-200 focus:outline-none"
|
15 |
+
style={{ resize: 'none' }}
|
16 |
+
value={text}
|
17 |
+
onChange={(e) => onChange(e.target.value)}
|
18 |
+
disabled={!editable}
|
19 |
+
/>
|
20 |
+
);
|
21 |
+
};
|
docker-compose.yml
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: '3.8'
|
2 |
+
|
3 |
+
services:
|
4 |
+
ai-code-translator:
|
5 |
+
build: .
|
6 |
+
container_name: ai_code_translator
|
7 |
+
ports:
|
8 |
+
- '3001:3000'
|
9 |
+
volumes:
|
10 |
+
- .:/app
|
11 |
+
- /app/node_modules
|
12 |
+
environment:
|
13 |
+
- NODE_ENV=development
|
14 |
+
command: npm run dev
|
15 |
+
restart: always
|
16 |
+
security_opt:
|
17 |
+
- no-new-privileges:true
|
next.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('next').NextConfig} */
|
2 |
+
const nextConfig = {
|
3 |
+
reactStrictMode: true,
|
4 |
+
}
|
5 |
+
|
6 |
+
module.exports = nextConfig
|
package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
package.json
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "code-translate",
|
3 |
+
"version": "0.1.0",
|
4 |
+
"private": true,
|
5 |
+
"scripts": {
|
6 |
+
"dev": "next dev",
|
7 |
+
"build": "next build",
|
8 |
+
"start": "next start",
|
9 |
+
"lint": "next lint"
|
10 |
+
},
|
11 |
+
"dependencies": {
|
12 |
+
"@codemirror/legacy-modes": "^6.3.2",
|
13 |
+
"@uiw/codemirror-theme-tokyo-night": "^4.19.11",
|
14 |
+
"@uiw/react-codemirror": "^4.19.11",
|
15 |
+
"endent": "^2.1.0",
|
16 |
+
"eventsource-parser": "^1.0.0",
|
17 |
+
"next": "13.2.4",
|
18 |
+
"react": "18.2.0",
|
19 |
+
"react-dom": "18.2.0"
|
20 |
+
},
|
21 |
+
"devDependencies": {
|
22 |
+
"@types/node": "18.15.11",
|
23 |
+
"@types/react": "18.0.31",
|
24 |
+
"@types/react-dom": "18.0.11",
|
25 |
+
"autoprefixer": "^10.4.14",
|
26 |
+
"eslint": "8.37.0",
|
27 |
+
"eslint-config-next": "13.2.4",
|
28 |
+
"postcss": "^8.4.21",
|
29 |
+
"prettier-plugin-tailwindcss": "^0.2.6",
|
30 |
+
"tailwindcss": "^3.3.1",
|
31 |
+
"typescript": "5.0.3"
|
32 |
+
}
|
33 |
+
}
|
pages/_app.tsx
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import "@/styles/globals.css";
|
2 |
+
import type { AppProps } from "next/app";
|
3 |
+
import { Inter } from "next/font/google";
|
4 |
+
|
5 |
+
const inter = Inter({ subsets: ["latin"] });
|
6 |
+
|
7 |
+
function App({ Component, pageProps }: AppProps<{}>) {
|
8 |
+
return (
|
9 |
+
<main className={inter.className}>
|
10 |
+
<Component {...pageProps} />
|
11 |
+
</main>
|
12 |
+
);
|
13 |
+
}
|
14 |
+
|
15 |
+
export default App;
|
pages/_document.tsx
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Html, Head, Main, NextScript } from 'next/document'
|
2 |
+
|
3 |
+
export default function Document() {
|
4 |
+
return (
|
5 |
+
<Html lang="en">
|
6 |
+
<Head />
|
7 |
+
<body>
|
8 |
+
<Main />
|
9 |
+
<NextScript />
|
10 |
+
</body>
|
11 |
+
</Html>
|
12 |
+
)
|
13 |
+
}
|
pages/api/translate.ts
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TranslateBody } from '@/types/types';
|
2 |
+
import { OpenAIStream } from '@/utils';
|
3 |
+
|
4 |
+
export const config = {
|
5 |
+
runtime: 'edge',
|
6 |
+
};
|
7 |
+
|
8 |
+
const handler = async (req: Request): Promise<Response> => {
|
9 |
+
try {
|
10 |
+
const { inputLanguage, outputLanguage, inputCode, model, apiKey } =
|
11 |
+
(await req.json()) as TranslateBody;
|
12 |
+
|
13 |
+
const stream = await OpenAIStream(
|
14 |
+
inputLanguage,
|
15 |
+
outputLanguage,
|
16 |
+
inputCode,
|
17 |
+
model,
|
18 |
+
apiKey,
|
19 |
+
);
|
20 |
+
|
21 |
+
return new Response(stream);
|
22 |
+
} catch (error) {
|
23 |
+
console.error(error);
|
24 |
+
return new Response('Error', { status: 500 });
|
25 |
+
}
|
26 |
+
};
|
27 |
+
|
28 |
+
export default handler;
|
pages/index.tsx
ADDED
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { APIKeyInput } from '@/components/APIKeyInput';
|
2 |
+
import { CodeBlock } from '@/components/CodeBlock';
|
3 |
+
import { LanguageSelect } from '@/components/LanguageSelect';
|
4 |
+
import { ModelSelect } from '@/components/ModelSelect';
|
5 |
+
import { TextBlock } from '@/components/TextBlock';
|
6 |
+
import { OpenAIModel, TranslateBody } from '@/types/types';
|
7 |
+
import Head from 'next/head';
|
8 |
+
import { useEffect, useState } from 'react';
|
9 |
+
|
10 |
+
export default function Home() {
|
11 |
+
const [inputLanguage, setInputLanguage] = useState<string>('JavaScript');
|
12 |
+
const [outputLanguage, setOutputLanguage] = useState<string>('Python');
|
13 |
+
const [inputCode, setInputCode] = useState<string>('');
|
14 |
+
const [outputCode, setOutputCode] = useState<string>('');
|
15 |
+
const [model, setModel] = useState<OpenAIModel>('gpt-3.5-turbo');
|
16 |
+
const [loading, setLoading] = useState<boolean>(false);
|
17 |
+
const [hasTranslated, setHasTranslated] = useState<boolean>(false);
|
18 |
+
const [apiKey, setApiKey] = useState<string>('');
|
19 |
+
|
20 |
+
const handleTranslate = async () => {
|
21 |
+
const maxCodeLength = model === 'gpt-3.5-turbo' ? 6000 : 12000;
|
22 |
+
|
23 |
+
if (!apiKey) {
|
24 |
+
alert('Please enter an API key.');
|
25 |
+
return;
|
26 |
+
}
|
27 |
+
|
28 |
+
if (inputLanguage === outputLanguage) {
|
29 |
+
alert('Please select different languages.');
|
30 |
+
return;
|
31 |
+
}
|
32 |
+
|
33 |
+
if (!inputCode) {
|
34 |
+
alert('Please enter some code.');
|
35 |
+
return;
|
36 |
+
}
|
37 |
+
|
38 |
+
if (inputCode.length > maxCodeLength) {
|
39 |
+
alert(
|
40 |
+
`Please enter code less than ${maxCodeLength} characters. You are currently at ${inputCode.length} characters.`,
|
41 |
+
);
|
42 |
+
return;
|
43 |
+
}
|
44 |
+
|
45 |
+
setLoading(true);
|
46 |
+
setOutputCode('');
|
47 |
+
|
48 |
+
const controller = new AbortController();
|
49 |
+
|
50 |
+
const body: TranslateBody = {
|
51 |
+
inputLanguage,
|
52 |
+
outputLanguage,
|
53 |
+
inputCode,
|
54 |
+
model,
|
55 |
+
apiKey,
|
56 |
+
};
|
57 |
+
|
58 |
+
const response = await fetch('/api/translate', {
|
59 |
+
method: 'POST',
|
60 |
+
headers: {
|
61 |
+
'Content-Type': 'application/json',
|
62 |
+
},
|
63 |
+
signal: controller.signal,
|
64 |
+
body: JSON.stringify(body),
|
65 |
+
});
|
66 |
+
|
67 |
+
if (!response.ok) {
|
68 |
+
setLoading(false);
|
69 |
+
alert('Something went wrong.');
|
70 |
+
return;
|
71 |
+
}
|
72 |
+
|
73 |
+
const data = response.body;
|
74 |
+
|
75 |
+
if (!data) {
|
76 |
+
setLoading(false);
|
77 |
+
alert('Something went wrong.');
|
78 |
+
return;
|
79 |
+
}
|
80 |
+
|
81 |
+
const reader = data.getReader();
|
82 |
+
const decoder = new TextDecoder();
|
83 |
+
let done = false;
|
84 |
+
let code = '';
|
85 |
+
|
86 |
+
while (!done) {
|
87 |
+
const { value, done: doneReading } = await reader.read();
|
88 |
+
done = doneReading;
|
89 |
+
const chunkValue = decoder.decode(value);
|
90 |
+
|
91 |
+
code += chunkValue;
|
92 |
+
|
93 |
+
setOutputCode((prevCode) => prevCode + chunkValue);
|
94 |
+
}
|
95 |
+
|
96 |
+
setLoading(false);
|
97 |
+
setHasTranslated(true);
|
98 |
+
copyToClipboard(code);
|
99 |
+
};
|
100 |
+
|
101 |
+
const copyToClipboard = (text: string) => {
|
102 |
+
const el = document.createElement('textarea');
|
103 |
+
el.value = text;
|
104 |
+
document.body.appendChild(el);
|
105 |
+
el.select();
|
106 |
+
document.execCommand('copy');
|
107 |
+
document.body.removeChild(el);
|
108 |
+
};
|
109 |
+
|
110 |
+
const handleApiKeyChange = (value: string) => {
|
111 |
+
setApiKey(value);
|
112 |
+
|
113 |
+
localStorage.setItem('apiKey', value);
|
114 |
+
};
|
115 |
+
|
116 |
+
useEffect(() => {
|
117 |
+
if (hasTranslated) {
|
118 |
+
handleTranslate();
|
119 |
+
}
|
120 |
+
}, [outputLanguage]);
|
121 |
+
|
122 |
+
useEffect(() => {
|
123 |
+
const apiKey = localStorage.getItem('apiKey');
|
124 |
+
|
125 |
+
if (apiKey) {
|
126 |
+
setApiKey(apiKey);
|
127 |
+
}
|
128 |
+
}, []);
|
129 |
+
|
130 |
+
return (
|
131 |
+
<>
|
132 |
+
<Head>
|
133 |
+
<title>Code Translator</title>
|
134 |
+
<meta
|
135 |
+
name="description"
|
136 |
+
content="Use AI to translate code from one language to another."
|
137 |
+
/>
|
138 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
139 |
+
<link rel="icon" href="/favicon.ico" />
|
140 |
+
</Head>
|
141 |
+
<div className="flex h-full min-h-screen flex-col items-center bg-[#0E1117] px-4 pb-20 text-neutral-200 sm:px-10">
|
142 |
+
<div className="mt-10 flex flex-col items-center justify-center sm:mt-20">
|
143 |
+
<div className="text-4xl font-bold">AI Code Translator</div>
|
144 |
+
</div>
|
145 |
+
|
146 |
+
<div className="mt-6 text-center text-sm">
|
147 |
+
<APIKeyInput apiKey={apiKey} onChange={handleApiKeyChange} />
|
148 |
+
</div>
|
149 |
+
|
150 |
+
<div className="mt-2 flex items-center space-x-2">
|
151 |
+
<ModelSelect model={model} onChange={(value) => setModel(value)} />
|
152 |
+
|
153 |
+
<button
|
154 |
+
className="w-[140px] cursor-pointer rounded-md bg-violet-500 px-4 py-2 font-bold hover:bg-violet-600 active:bg-violet-700"
|
155 |
+
onClick={() => handleTranslate()}
|
156 |
+
disabled={loading}
|
157 |
+
>
|
158 |
+
{loading ? 'Translating...' : 'Translate'}
|
159 |
+
</button>
|
160 |
+
</div>
|
161 |
+
|
162 |
+
<div className="mt-2 text-center text-xs">
|
163 |
+
{loading
|
164 |
+
? 'Translating...'
|
165 |
+
: hasTranslated
|
166 |
+
? 'Output copied to clipboard!'
|
167 |
+
: 'Enter some code and click "Translate"'}
|
168 |
+
</div>
|
169 |
+
|
170 |
+
<div className="mt-6 flex w-full max-w-[1200px] flex-col justify-between sm:flex-row sm:space-x-4">
|
171 |
+
<div className="h-100 flex flex-col justify-center space-y-2 sm:w-2/4">
|
172 |
+
<div className="text-center text-xl font-bold">Input</div>
|
173 |
+
|
174 |
+
<LanguageSelect
|
175 |
+
language={inputLanguage}
|
176 |
+
onChange={(value) => {
|
177 |
+
setInputLanguage(value);
|
178 |
+
setHasTranslated(false);
|
179 |
+
setInputCode('');
|
180 |
+
setOutputCode('');
|
181 |
+
}}
|
182 |
+
/>
|
183 |
+
|
184 |
+
{inputLanguage === 'Natural Language' ? (
|
185 |
+
<TextBlock
|
186 |
+
text={inputCode}
|
187 |
+
editable={!loading}
|
188 |
+
onChange={(value) => {
|
189 |
+
setInputCode(value);
|
190 |
+
setHasTranslated(false);
|
191 |
+
}}
|
192 |
+
/>
|
193 |
+
) : (
|
194 |
+
<CodeBlock
|
195 |
+
code={inputCode}
|
196 |
+
editable={!loading}
|
197 |
+
onChange={(value) => {
|
198 |
+
setInputCode(value);
|
199 |
+
setHasTranslated(false);
|
200 |
+
}}
|
201 |
+
/>
|
202 |
+
)}
|
203 |
+
</div>
|
204 |
+
<div className="mt-8 flex h-full flex-col justify-center space-y-2 sm:mt-0 sm:w-2/4">
|
205 |
+
<div className="text-center text-xl font-bold">Output</div>
|
206 |
+
|
207 |
+
<LanguageSelect
|
208 |
+
language={outputLanguage}
|
209 |
+
onChange={(value) => {
|
210 |
+
setOutputLanguage(value);
|
211 |
+
setOutputCode('');
|
212 |
+
}}
|
213 |
+
/>
|
214 |
+
|
215 |
+
{outputLanguage === 'Natural Language' ? (
|
216 |
+
<TextBlock text={outputCode} />
|
217 |
+
) : (
|
218 |
+
<CodeBlock code={outputCode} />
|
219 |
+
)}
|
220 |
+
</div>
|
221 |
+
</div>
|
222 |
+
</div>
|
223 |
+
</>
|
224 |
+
);
|
225 |
+
}
|
postcss.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
plugins: {
|
3 |
+
tailwindcss: {},
|
4 |
+
autoprefixer: {},
|
5 |
+
},
|
6 |
+
}
|
prettier.config.js
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
trailingComma: "all",
|
3 |
+
singleQuote: true,
|
4 |
+
plugins: [require("prettier-plugin-tailwindcss")]
|
5 |
+
};
|
public/favicon.ico
ADDED
|
public/screenshot.png
ADDED
![]() |
styles/globals.css
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
tailwind.config.js
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/** @type {import('tailwindcss').Config} */
|
2 |
+
module.exports = {
|
3 |
+
content: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
|
4 |
+
theme: {
|
5 |
+
extend: {}
|
6 |
+
},
|
7 |
+
plugins: []
|
8 |
+
};
|
tsconfig.json
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"target": "ES2020",
|
4 |
+
"module": "nodenext",
|
5 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
6 |
+
"allowJs": true,
|
7 |
+
"skipLibCheck": true,
|
8 |
+
"strict": true,
|
9 |
+
"forceConsistentCasingInFileNames": true,
|
10 |
+
"noEmit": true,
|
11 |
+
"esModuleInterop": true,
|
12 |
+
"moduleResolution": "node",
|
13 |
+
"resolveJsonModule": true,
|
14 |
+
"isolatedModules": true,
|
15 |
+
"jsx": "preserve",
|
16 |
+
"incremental": true,
|
17 |
+
"paths": {
|
18 |
+
"@/*": ["./*"]
|
19 |
+
}
|
20 |
+
},
|
21 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
22 |
+
"exclude": ["node_modules"]
|
23 |
+
}
|
types/types.ts
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export type OpenAIModel = 'gpt-3.5-turbo' | 'gpt-4';
|
2 |
+
|
3 |
+
export interface TranslateBody {
|
4 |
+
inputLanguage: string;
|
5 |
+
outputLanguage: string;
|
6 |
+
inputCode: string;
|
7 |
+
model: OpenAIModel;
|
8 |
+
apiKey: string;
|
9 |
+
}
|
10 |
+
|
11 |
+
export interface TranslateResponse {
|
12 |
+
code: string;
|
13 |
+
}
|
utils/index.ts
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import endent from 'endent';
|
2 |
+
import {
|
3 |
+
createParser,
|
4 |
+
ParsedEvent,
|
5 |
+
ReconnectInterval,
|
6 |
+
} from 'eventsource-parser';
|
7 |
+
|
8 |
+
const createPrompt = (
|
9 |
+
inputLanguage: string,
|
10 |
+
outputLanguage: string,
|
11 |
+
inputCode: string,
|
12 |
+
) => {
|
13 |
+
if (inputLanguage === 'Natural Language') {
|
14 |
+
return endent`
|
15 |
+
You are an expert programmer in all programming languages. Translate the natural language to "${outputLanguage}" code. Do not include \`\`\`.
|
16 |
+
|
17 |
+
Example translating from natural language to JavaScript:
|
18 |
+
|
19 |
+
Natural language:
|
20 |
+
Print the numbers 0 to 9.
|
21 |
+
|
22 |
+
JavaScript code:
|
23 |
+
for (let i = 0; i < 10; i++) {
|
24 |
+
console.log(i);
|
25 |
+
}
|
26 |
+
|
27 |
+
Natural language:
|
28 |
+
${inputCode}
|
29 |
+
|
30 |
+
${outputLanguage} code (no \`\`\`):
|
31 |
+
`;
|
32 |
+
} else if (outputLanguage === 'Natural Language') {
|
33 |
+
return endent`
|
34 |
+
You are an expert programmer in all programming languages. Translate the "${inputLanguage}" code to natural language in plain English that the average adult could understand. Respond as bullet points starting with -.
|
35 |
+
|
36 |
+
Example translating from JavaScript to natural language:
|
37 |
+
|
38 |
+
JavaScript code:
|
39 |
+
for (let i = 0; i < 10; i++) {
|
40 |
+
console.log(i);
|
41 |
+
}
|
42 |
+
|
43 |
+
Natural language:
|
44 |
+
Print the numbers 0 to 9.
|
45 |
+
|
46 |
+
${inputLanguage} code:
|
47 |
+
${inputCode}
|
48 |
+
|
49 |
+
Natural language:
|
50 |
+
`;
|
51 |
+
} else {
|
52 |
+
return endent`
|
53 |
+
You are an expert programmer in all programming languages. Translate the "${inputLanguage}" code to "${outputLanguage}" code. Do not include \`\`\`.
|
54 |
+
|
55 |
+
Example translating from JavaScript to Python:
|
56 |
+
|
57 |
+
JavaScript code:
|
58 |
+
for (let i = 0; i < 10; i++) {
|
59 |
+
console.log(i);
|
60 |
+
}
|
61 |
+
|
62 |
+
Python code:
|
63 |
+
for i in range(10):
|
64 |
+
print(i)
|
65 |
+
|
66 |
+
${inputLanguage} code:
|
67 |
+
${inputCode}
|
68 |
+
|
69 |
+
${outputLanguage} code (no \`\`\`):
|
70 |
+
`;
|
71 |
+
}
|
72 |
+
};
|
73 |
+
|
74 |
+
export const OpenAIStream = async (
|
75 |
+
inputLanguage: string,
|
76 |
+
outputLanguage: string,
|
77 |
+
inputCode: string,
|
78 |
+
model: string,
|
79 |
+
key: string,
|
80 |
+
) => {
|
81 |
+
const prompt = createPrompt(inputLanguage, outputLanguage, inputCode);
|
82 |
+
|
83 |
+
const system = { role: 'system', content: prompt };
|
84 |
+
|
85 |
+
const res = await fetch(`https://api.openai.com/v1/chat/completions`, {
|
86 |
+
headers: {
|
87 |
+
'Content-Type': 'application/json',
|
88 |
+
Authorization: `Bearer ${key || process.env.OPENAI_API_KEY}`,
|
89 |
+
},
|
90 |
+
method: 'POST',
|
91 |
+
body: JSON.stringify({
|
92 |
+
model,
|
93 |
+
messages: [system],
|
94 |
+
temperature: 0,
|
95 |
+
stream: true,
|
96 |
+
}),
|
97 |
+
});
|
98 |
+
|
99 |
+
const encoder = new TextEncoder();
|
100 |
+
const decoder = new TextDecoder();
|
101 |
+
|
102 |
+
if (res.status !== 200) {
|
103 |
+
const statusText = res.statusText;
|
104 |
+
const result = await res.body?.getReader().read();
|
105 |
+
throw new Error(
|
106 |
+
`OpenAI API returned an error: ${
|
107 |
+
decoder.decode(result?.value) || statusText
|
108 |
+
}`,
|
109 |
+
);
|
110 |
+
}
|
111 |
+
|
112 |
+
const stream = new ReadableStream({
|
113 |
+
async start(controller) {
|
114 |
+
const onParse = (event: ParsedEvent | ReconnectInterval) => {
|
115 |
+
if (event.type === 'event') {
|
116 |
+
const data = event.data;
|
117 |
+
|
118 |
+
if (data === '[DONE]') {
|
119 |
+
controller.close();
|
120 |
+
return;
|
121 |
+
}
|
122 |
+
|
123 |
+
try {
|
124 |
+
const json = JSON.parse(data);
|
125 |
+
const text = json.choices[0].delta.content;
|
126 |
+
const queue = encoder.encode(text);
|
127 |
+
controller.enqueue(queue);
|
128 |
+
} catch (e) {
|
129 |
+
controller.error(e);
|
130 |
+
}
|
131 |
+
}
|
132 |
+
};
|
133 |
+
|
134 |
+
const parser = createParser(onParse);
|
135 |
+
|
136 |
+
for await (const chunk of res.body as any) {
|
137 |
+
parser.feed(decoder.decode(chunk));
|
138 |
+
}
|
139 |
+
},
|
140 |
+
});
|
141 |
+
|
142 |
+
return stream;
|
143 |
+
};
|