init
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# ChatGPT API WEB
|
||||
|
||||
一个简单的网页,调用 OPENAI ChatGPT 进行对话。
|
||||
|
||||
与官方 ChatGPT 相比:
|
||||
|
||||
- 对话记录使用浏览器的 localStorage 保存在本地
|
||||
- 可删除对话消息
|
||||
- 可以设置 system message (如:"你是一个喵娘",参见官方 [API 文档](https://platform.openai.com/docs/guides/chat))
|
||||
- 可以为不同对话设置不同 APIKEY
|
||||
- 小(整个网页 30k 左右)
|
||||
- 可以设置不同的 API Endpoint(方便墙内人士使用反向代理转发 API 请求)
|
||||
|
||||
## 屏幕截图
|
||||
|
||||

|
||||
|
||||
~~发病.webp~~
|
||||
|
||||
## 使用
|
||||
|
||||
以下任意方式都可:
|
||||
|
||||
- 访问 github pages 部署
|
||||
- 从 release 下载网页文件然后双击打开
|
||||
- 自行编译构建网页
|
||||
|
||||
### 更改默认参数
|
||||
|
||||
- `key`: OPENAI API KEY 默认为空
|
||||
- `sys`: system message 默认为 "你是一个猫娘,你要模仿猫娘的语气说话"
|
||||
- `api`: API Endpoint 默认为 `https://api.openai.com/v1/chat/completions`
|
||||
|
||||
例如 `http://localhost:1234/?key=xxxx` 那么新创建的会话将会使用该默认 API
|
||||
|
||||
以上三个参数应用于单个对话,随时可在顶部更改
|
||||
|
||||
## 自行编译构建网页
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ChatGPT API Web</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "chatgpt-api-web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.21",
|
||||
"preact": "^10.11.3",
|
||||
"sakura.css": "^1.4.1",
|
||||
"tailwindcss": "^3.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.5.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.1.0"
|
||||
}
|
||||
}
|
||||
6
postcss.config.cjs
Normal file
6
postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
screenshot.webp
Normal file
BIN
screenshot.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
311
src/app.tsx
Normal file
311
src/app.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "./global.css";
|
||||
|
||||
import ChatGPT, { Message } from "./chatgpt";
|
||||
import { createRef } from "preact";
|
||||
|
||||
export interface ChatStore {
|
||||
systemMessageContent: string;
|
||||
history: Message[];
|
||||
postBeginIndex: number;
|
||||
tokenMargin: number;
|
||||
totalTokens: number;
|
||||
maxTokens: number;
|
||||
apiKey: string;
|
||||
apiEndpoint: string;
|
||||
}
|
||||
|
||||
const defaultAPIKEY = () => {
|
||||
const queryParameters = new URLSearchParams(window.location.search);
|
||||
const key = queryParameters.get("key");
|
||||
return key;
|
||||
};
|
||||
|
||||
const defaultSysMessage = () => {
|
||||
const queryParameters = new URLSearchParams(window.location.search);
|
||||
const sys = queryParameters.get("sys");
|
||||
return sys;
|
||||
};
|
||||
|
||||
const defaultAPIEndpoint = () => {
|
||||
const queryParameters = new URLSearchParams(window.location.search);
|
||||
const sys = queryParameters.get("api");
|
||||
return sys;
|
||||
};
|
||||
|
||||
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||
export const newChatStore = (
|
||||
apiKey = "",
|
||||
systemMessageContent = "你是一个猫娘,你要模仿猫娘的语气说话",
|
||||
apiEndpoint = _defaultAPIEndpoint
|
||||
): ChatStore => {
|
||||
return {
|
||||
systemMessageContent: defaultSysMessage() || systemMessageContent,
|
||||
history: [],
|
||||
postBeginIndex: 0,
|
||||
tokenMargin: 1024,
|
||||
totalTokens: 0,
|
||||
maxTokens: 4096,
|
||||
apiKey: defaultAPIKEY() || apiKey,
|
||||
apiEndpoint: defaultAPIEndpoint() || apiEndpoint,
|
||||
};
|
||||
};
|
||||
|
||||
const STORAGE_NAME = "chatgpt-api-web";
|
||||
|
||||
export function App() {
|
||||
const initAllChatStore: ChatStore[] = JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME) || "[]"
|
||||
);
|
||||
if (initAllChatStore.length === 0) {
|
||||
initAllChatStore.push(newChatStore());
|
||||
localStorage.setItem(STORAGE_NAME, JSON.stringify(initAllChatStore));
|
||||
}
|
||||
const [allChatStore, setAllChatStore] = useState(initAllChatStore);
|
||||
const [selectedChatIndex, setSelectedChatIndex] = useState(0);
|
||||
const chatStore = allChatStore[selectedChatIndex];
|
||||
const setChatStore = (cs: ChatStore) => {
|
||||
allChatStore[selectedChatIndex] = cs;
|
||||
setAllChatStore([...allChatStore]);
|
||||
};
|
||||
useEffect(() => {
|
||||
console.log("saved", allChatStore);
|
||||
localStorage.setItem(STORAGE_NAME, JSON.stringify(allChatStore));
|
||||
}, [allChatStore]);
|
||||
|
||||
const [inputMsg, setInputMsg] = useState("");
|
||||
const [showGenerating, setShowGenerating] = useState(false);
|
||||
|
||||
const client = new ChatGPT(chatStore.apiKey);
|
||||
|
||||
const _complete = async () => {
|
||||
client.apiEndpoint = chatStore.apiEndpoint;
|
||||
client.sysMessageContent = chatStore.systemMessageContent;
|
||||
client.messages = chatStore.history.slice(chatStore.postBeginIndex);
|
||||
const response = await client.complete();
|
||||
chatStore.history.push({ role: "assistant", content: response });
|
||||
chatStore.maxTokens = client.max_tokens;
|
||||
chatStore.tokenMargin = client.tokens_margin;
|
||||
chatStore.totalTokens = client.total_tokens;
|
||||
chatStore.postBeginIndex =
|
||||
chatStore.history.length - client.messages.length;
|
||||
console.log("postBeginIndex", chatStore.postBeginIndex);
|
||||
setChatStore({ ...chatStore });
|
||||
};
|
||||
|
||||
const complete = async () => {
|
||||
try {
|
||||
setShowGenerating(true);
|
||||
await _complete();
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
} finally {
|
||||
setShowGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const send = async () => {
|
||||
if (!inputMsg) {
|
||||
console.log("empty message");
|
||||
return;
|
||||
}
|
||||
chatStore.history.push({ role: "user", content: inputMsg.trim() });
|
||||
setChatStore({ ...chatStore });
|
||||
setInputMsg("");
|
||||
await complete();
|
||||
setChatStore({ ...chatStore });
|
||||
};
|
||||
|
||||
const changAPIKEY = () => {
|
||||
const newAPIKEY = prompt(`Current API KEY: ${chatStore.apiKey}`);
|
||||
if (!newAPIKEY) return;
|
||||
chatStore.apiKey = newAPIKEY;
|
||||
setChatStore({ ...chatStore });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex text-sm h-screen bg-slate-200">
|
||||
<div className="flex flex-col h-full p-4 border-r-indigo-500 border-2">
|
||||
<div className="grow overflow-scroll">
|
||||
<button
|
||||
className="bg-violet-300 p-1 rounded hover:bg-violet-400"
|
||||
onClick={() => {
|
||||
allChatStore.push(
|
||||
newChatStore(
|
||||
allChatStore[selectedChatIndex].apiKey,
|
||||
allChatStore[selectedChatIndex].systemMessageContent,
|
||||
allChatStore[selectedChatIndex].apiEndpoint
|
||||
)
|
||||
);
|
||||
setAllChatStore([...allChatStore]);
|
||||
setSelectedChatIndex(allChatStore.length - 1);
|
||||
}}
|
||||
>
|
||||
NEW
|
||||
</button>
|
||||
<ul>
|
||||
{allChatStore.map((cs, i) => (
|
||||
<li>
|
||||
<button
|
||||
className={`w-full my-1 p-1 rounded hover:bg-blue-300 ${
|
||||
i === selectedChatIndex ? "bg-blue-500" : "bg-blue-200"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedChatIndex(i);
|
||||
}}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
className="rounded bg-rose-400 p-1 my-1 w-full"
|
||||
onClick={() => {
|
||||
if (!confirm("Are you sure you want to delete this chat history?"))
|
||||
return;
|
||||
const oldAPIkey = allChatStore[selectedChatIndex].apiKey;
|
||||
allChatStore.splice(selectedChatIndex, 1);
|
||||
if (allChatStore.length === 0) {
|
||||
allChatStore.push(
|
||||
newChatStore(
|
||||
oldAPIkey,
|
||||
allChatStore[selectedChatIndex].systemMessageContent,
|
||||
allChatStore[selectedChatIndex].apiEndpoint
|
||||
)
|
||||
);
|
||||
setSelectedChatIndex(0);
|
||||
} else {
|
||||
setSelectedChatIndex(selectedChatIndex - 1);
|
||||
}
|
||||
setAllChatStore([...allChatStore]);
|
||||
}}
|
||||
>
|
||||
DEL
|
||||
</button>
|
||||
</div>
|
||||
<div className="grow flex flex-col p-4">
|
||||
<p>
|
||||
<div>
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() => {
|
||||
const newSysMsgContent = prompt(
|
||||
"Change system message content"
|
||||
);
|
||||
if (newSysMsgContent === null) return;
|
||||
chatStore.systemMessageContent = newSysMsgContent;
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
{chatStore.systemMessageContent}
|
||||
</button>{" "}
|
||||
<button className="underline" onClick={changAPIKEY}>
|
||||
KEY
|
||||
</button>{" "}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() => {
|
||||
const newEndpoint = prompt(
|
||||
`Enter new API endpoint\n(current: ${chatStore.apiEndpoint})\n(default: ${_defaultAPIEndpoint})`
|
||||
);
|
||||
if (!newEndpoint) return;
|
||||
chatStore.apiEndpoint = newEndpoint;
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
ENDPOINT
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span>Total: {chatStore.totalTokens}</span>{" "}
|
||||
<span>Max: {chatStore.maxTokens}</span>{" "}
|
||||
<span>Margin: {chatStore.tokenMargin}</span>{" "}
|
||||
<span>
|
||||
Message: {chatStore.history.length - chatStore.postBeginIndex}
|
||||
</span>{" "}
|
||||
<span>Cut: {chatStore.postBeginIndex}</span>
|
||||
</div>
|
||||
</p>
|
||||
<div className="grow overflow-scroll">
|
||||
{chatStore.history.length === 0 && (
|
||||
<p className="opacity-60 p-6 rounded bg-white my-3 text-left">
|
||||
喵喵,请先在上方设置 (OPENAI) API KEY
|
||||
</p>
|
||||
)}
|
||||
{chatStore.history.map((chat, i) => {
|
||||
const pClassName =
|
||||
chat.role === "assistant"
|
||||
? "p-2 rounded relative bg-white my-2 text-left"
|
||||
: "p-2 rounded relative bg-green-400 my-2 text-right";
|
||||
const iconClassName =
|
||||
chat.role === "user"
|
||||
? "absolute bottom-0 left-0"
|
||||
: "absolute bottom-0 right-0";
|
||||
const DeleteIcon = () => (
|
||||
<button
|
||||
className={iconClassName}
|
||||
onClick={() => {
|
||||
if (
|
||||
confirm(
|
||||
`Are you sure to delete this message?\n${chat.content.slice(
|
||||
0,
|
||||
39
|
||||
)}...`
|
||||
)
|
||||
) {
|
||||
chatStore.history.splice(i, 1);
|
||||
setChatStore({ ...chatStore });
|
||||
}
|
||||
}}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<p className={pClassName}>
|
||||
{chat.content
|
||||
.split("\n")
|
||||
.filter((line) => line)
|
||||
.map((line) => (
|
||||
<p className="my-1">{line}</p>
|
||||
))}
|
||||
<DeleteIcon />
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
{showGenerating && (
|
||||
<p className="animate-pulse">Generating... please wait...</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<textarea
|
||||
value={inputMsg}
|
||||
onChange={(event: any) => setInputMsg(event.target.value)}
|
||||
onKeyPress={(event: any) => {
|
||||
console.log(event);
|
||||
if (event.ctrlKey && event.code === "Enter") {
|
||||
send();
|
||||
setInputMsg("");
|
||||
return;
|
||||
}
|
||||
setInputMsg(event.target.value);
|
||||
}}
|
||||
className="rounded grow m-1 p-1 border-2 border-gray-400 w-0"
|
||||
placeholder="Type here..."
|
||||
></textarea>
|
||||
<button
|
||||
className="rounded m-1 p-1 border-2 border-rose-400 bg-cyan-400 hover:bg-cyan-600"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
send();
|
||||
}}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
src/chatgpt.ts
Normal file
148
src/chatgpt.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
export interface Message {
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
class Chat {
|
||||
OPENAI_API_KEY: string;
|
||||
messages: Message[];
|
||||
sysMessageContent: string;
|
||||
total_tokens: number;
|
||||
max_tokens: number;
|
||||
tokens_margin: number;
|
||||
apiEndpoint: string;
|
||||
|
||||
constructor(
|
||||
OPENAI_API_KEY: string | undefined,
|
||||
{
|
||||
systemMessage = "你是一个有用的人工智能助理",
|
||||
max_tokens = 4096,
|
||||
tokens_margin = 1024,
|
||||
apiEndPoint = "https://api.openai.com/v1/chat/completions",
|
||||
} = {}
|
||||
) {
|
||||
if (OPENAI_API_KEY === undefined) {
|
||||
throw "OPENAI_API_KEY is undefined";
|
||||
}
|
||||
this.OPENAI_API_KEY = OPENAI_API_KEY;
|
||||
this.messages = [];
|
||||
this.total_tokens = 0;
|
||||
this.max_tokens = max_tokens;
|
||||
this.tokens_margin = tokens_margin;
|
||||
this.sysMessageContent = systemMessage;
|
||||
this.apiEndpoint = apiEndPoint;
|
||||
}
|
||||
|
||||
async fetch(): Promise<{
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
usage: {
|
||||
prompt_tokens: number | undefined;
|
||||
completion_tokens: number | undefined;
|
||||
total_tokens: number | undefined;
|
||||
};
|
||||
choices: {
|
||||
message: Message | undefined;
|
||||
finish_reason: "stop" | "length";
|
||||
index: number | undefined;
|
||||
}[];
|
||||
}> {
|
||||
const resp = await fetch(this.apiEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.OPENAI_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "gpt-3.5-turbo",
|
||||
messages: [
|
||||
{ role: "system", content: this.sysMessageContent },
|
||||
...this.messages,
|
||||
],
|
||||
}),
|
||||
}).then((resp) => resp.json());
|
||||
return resp;
|
||||
}
|
||||
|
||||
async say(content: string): Promise<string> {
|
||||
this.messages.push({ role: "user", content });
|
||||
await this.complete();
|
||||
return this.messages.slice(-1)[0].content;
|
||||
}
|
||||
|
||||
async complete(): Promise<string> {
|
||||
const resp = await this.fetch();
|
||||
this.total_tokens = resp?.usage?.total_tokens ?? 0;
|
||||
if (resp?.choices[0]?.message) {
|
||||
this.messages.push(resp?.choices[0]?.message);
|
||||
}
|
||||
|
||||
if (resp.choices[0]?.finish_reason === "length") {
|
||||
this.forceForgetSomeMessages();
|
||||
} else {
|
||||
this.forgetSomeMessages();
|
||||
}
|
||||
|
||||
return (
|
||||
resp?.choices[0]?.message?.content ?? `Error: ${JSON.stringify(resp)}`
|
||||
);
|
||||
}
|
||||
|
||||
// https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
|
||||
calculate_token_length(content: string): number {
|
||||
const totalCount = content.length;
|
||||
const chineseCount = content.match(/[\u00ff-\uffff]|\S+/g)?.length ?? 0;
|
||||
const englishCount = totalCount - chineseCount;
|
||||
const tokenLength = englishCount / 4 + (chineseCount * 4) / 3;
|
||||
return ~~tokenLength;
|
||||
}
|
||||
|
||||
user(...messages: string[]) {
|
||||
for (const msg of messages) {
|
||||
this.messages.push({ role: "user", content: msg });
|
||||
this.total_tokens += this.calculate_token_length(msg);
|
||||
this.forgetSomeMessages();
|
||||
}
|
||||
}
|
||||
|
||||
assistant(...messages: string[]) {
|
||||
for (const msg of messages) {
|
||||
this.messages.push({ role: "assistant", content: msg });
|
||||
this.total_tokens += this.calculate_token_length(msg);
|
||||
this.forgetSomeMessages();
|
||||
}
|
||||
}
|
||||
|
||||
forgetSomeMessages() {
|
||||
// forget occur condition
|
||||
if (this.total_tokens + this.tokens_margin >= this.max_tokens) {
|
||||
this.forceForgetSomeMessages();
|
||||
}
|
||||
}
|
||||
|
||||
forceForgetSomeMessages() {
|
||||
this.messages = [
|
||||
...this.messages.slice(Math.max(~~(this.messages.length / 4), 2)),
|
||||
];
|
||||
}
|
||||
|
||||
forgetAllMessage() {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
stats(): string {
|
||||
return (
|
||||
`total_tokens: ${this.total_tokens}` +
|
||||
"\n" +
|
||||
`max_tokens: ${this.max_tokens}` +
|
||||
"\n" +
|
||||
`tokens_margin: ${this.tokens_margin}` +
|
||||
"\n" +
|
||||
`messages.length: ${this.messages.length}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Chat;
|
||||
3
src/global.css
Normal file
3
src/global.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
4
src/main.tsx
Normal file
4
src/main.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import { render } from 'preact'
|
||||
import { App } from './app'
|
||||
|
||||
render(<App />, document.getElementById('app') as HTMLElement)
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
8
tailwind.config.cjs
Normal file
8
tailwind.config.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
8
vite.config.ts
Normal file
8
vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import preact from '@preact/preset-vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [preact()],
|
||||
base: './',
|
||||
})
|
||||
Reference in New Issue
Block a user