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 |
+
};
|