Compare commits

...

6 Commits

7 changed files with 247 additions and 31 deletions

View File

@@ -1,3 +1,3 @@
const CHATGPT_API_WEB_VERSION = "v1.3.0";
const CHATGPT_API_WEB_VERSION = "v1.4.0";
export default CHATGPT_API_WEB_VERSION;

View File

@@ -27,6 +27,11 @@ export interface ChatStore {
model: string;
responseModelName: string;
cost: number;
temperature: number;
top_p: number;
presence_penalty: number;
frequency_penalty: number;
develop_mode: boolean;
}
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
@@ -35,7 +40,9 @@ const newChatStore = (
systemMessageContent = "Follow my instructions carefully",
apiEndpoint = _defaultAPIEndpoint,
streamMode = true,
model = "gpt-3.5-turbo-0613"
model = "gpt-3.5-turbo-0613",
temperature = 1.0,
dev = false
): ChatStore => {
return {
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
@@ -51,6 +58,11 @@ const newChatStore = (
model: getDefaultParams("model", model),
responseModelName: "",
cost: 0,
temperature: getDefaultParams("temp", temperature),
top_p: 1,
presence_penalty: 0,
frequency_penalty: 0,
develop_mode: getDefaultParams("dev", dev),
};
};

View File

@@ -32,6 +32,19 @@ export default function ChatBOX(props: {
const client = new ChatGPT(chatStore.apiKey);
const update_total_tokens = () => {
// manually estimate token
client.total_tokens = calculate_token_length(
chatStore.systemMessageContent
);
for (const msg of chatStore.history
.filter(({ hide }) => !hide)
.slice(chatStore.postBeginIndex)) {
client.total_tokens += msg.token;
}
chatStore.totalTokens = client.total_tokens;
};
const _completeWithStreamMode = async (response: Response) => {
chatStore.streamMode = true;
// call api, return reponse text
@@ -121,14 +134,7 @@ export default function ChatBOX(props: {
// manually copy status from client to chatStore
chatStore.maxTokens = client.max_tokens;
chatStore.tokenMargin = client.tokens_margin;
// manually estimate token
client.total_tokens = 0;
for (const msg of chatStore.history
.filter(({ hide }) => !hide)
.slice(chatStore.postBeginIndex)) {
client.total_tokens += msg.token;
}
chatStore.totalTokens = client.total_tokens;
update_total_tokens();
setChatStore({ ...chatStore });
setGeneratingMessage("");
setShowGenerating(false);
@@ -338,6 +344,33 @@ export default function ChatBOX(props: {
messageIndex={messageIndex}
/>
))}
{chatStore.develop_mode && (
<p className="text-center rounded">
<button
className="p-2 m-2 bg-teal-500 rounded"
onClick={async () => {
const messageIndex = chatStore.history.length - 1;
chatStore.history[messageIndex].hide = true;
//chatStore.totalTokens =
update_total_tokens();
setChatStore({ ...chatStore });
await complete();
}}
>
Re-Generate
</button>
<button
className="p-2 m-2 bg-yellow-500 rounded"
onClick={async () => {
await complete();
}}
>
Completion
</button>
</p>
)}
{showGenerating && (
<p className="p-2 my-2 animate-pulse dark:text-white message-content">
{generatingMessage || "生成中,最长可能需要一分钟,请保持网络稳定"}
@@ -366,6 +399,17 @@ export default function ChatBOX(props: {
</p>
)}
{chatStore.chatgpt_api_web_version < "v1.4.0" && (
<p className="p-2 my-2 text-center dark:text-white">
<br />
{chatStore.chatgpt_api_web_version} {"< v1.4.0"}
<br />
v1.4.0 使
<br />
</p>
)}
{showRetry && (
<p className="text-right p-2 my-2 dark:text-white">
<button
@@ -407,6 +451,42 @@ export default function ChatBOX(props: {
>
Send
</button>
{chatStore.develop_mode && (
<button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
disabled={showGenerating || !chatStore.apiKey}
onClick={() => {
chatStore.history.push({
role: "assistant",
content: inputMsg,
token: calculate_token_length(inputMsg),
hide: false,
});
update_total_tokens();
setChatStore({ ...chatStore });
}}
>
Assistant
</button>
)}
{chatStore.develop_mode && (
<button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
disabled={showGenerating || !chatStore.apiKey}
onClick={() => {
chatStore.history.push({
role: "user",
content: inputMsg,
token: calculate_token_length(inputMsg),
hide: false,
});
update_total_tokens();
setChatStore({ ...chatStore });
}}
>
User
</button>
)}
</div>
</div>
);

View File

@@ -1,5 +1,5 @@
export interface Message {
role: "system" | "user" | "assistant";
role: "system" | "user" | "assistant" | "function";
content: string;
}
@@ -45,6 +45,10 @@ class Chat {
tokens_margin: number;
apiEndpoint: string;
model: string;
temperature: number;
top_p: number;
presence_penalty: number;
frequency_penalty: number;
constructor(
OPENAI_API_KEY: string | undefined,
@@ -54,6 +58,10 @@ class Chat {
tokens_margin = 1024,
apiEndPoint = "https://api.openai.com/v1/chat/completions",
model = "gpt-3.5-turbo",
temperature = 1.0,
top_p = 1,
presence_penalty = 0,
frequency_penalty = 0,
} = {}
) {
if (OPENAI_API_KEY === undefined) {
@@ -67,6 +75,10 @@ class Chat {
this.sysMessageContent = systemMessage;
this.apiEndpoint = apiEndPoint;
this.model = model;
this.temperature = temperature;
this.top_p = top_p;
this.presence_penalty = presence_penalty;
this.frequency_penalty = frequency_penalty;
}
_fetch(stream = false) {
@@ -83,6 +95,10 @@ class Chat {
...this.messages,
],
stream,
temperature: this.temperature,
top_p: this.top_p,
presence_penalty: this.presence_penalty,
frequency_penalty: this.frequency_penalty,
}),
});
}

View File

@@ -7,10 +7,12 @@ function getDefaultParams(param: any, val: any) {
if (typeof val === "string") {
return get ?? val;
} else if (typeof val === "number") {
return parseInt(get ?? `${val}`);
return parseFloat(get ?? `${val}`);
} else if (typeof val === "boolean") {
if (get === "stream") return true;
if (get === "fetch") return false;
if (get === "true") return true;
if (get === "false") return false;
return val;
}
}

View File

@@ -1,3 +1,4 @@
import { useState } from "preact/hooks";
import { ChatStore } from "./app";
import { calculate_token_length } from "./chatgpt";
@@ -9,6 +10,7 @@ interface Props {
export default function Message(props: Props) {
const { chatStore, messageIndex, setChatStore } = props;
const chat = chatStore.history[messageIndex];
const [showEdit, setShowEdit] = useState(false);
const DeleteIcon = () => (
<button
className={`absolute bottom-0 ${
@@ -51,19 +53,73 @@ export default function Message(props: Props) {
chat.role === "assistant" ? "justify-start" : "justify-end"
}`}
>
<div
className={`relative w-fit p-2 rounded my-2 ${
chat.role === "assistant"
? "bg-white dark:bg-gray-700 dark:text-white"
: "bg-green-400"
} ${chat.hide ? "opacity-50" : ""}`}
>
<p className="message-content">
{chat.hide
? chat.content.split("\n")[0].slice(0, 16) + "... (deleted)"
: chat.content}
</p>
<DeleteIcon />
<div>
<div
className={`relative w-fit p-2 rounded my-2 ${
chat.role === "assistant"
? "bg-white dark:bg-gray-700 dark:text-white"
: "bg-green-400"
} ${chat.hide ? "opacity-50" : ""}`}
>
<p className="message-content">
{chat.hide
? chat.content.split("\n")[0].slice(0, 16) + "... (deleted)"
: chat.content}
</p>
<DeleteIcon />
</div>
{chatStore.develop_mode && (
<div>
token {chatStore.history[messageIndex].token}
<button
onClick={() => {
chatStore.history.splice(messageIndex, 1);
chatStore.postBeginIndex = Math.max(
chatStore.postBeginIndex - 1,
0
);
//chatStore.totalTokens =
chatStore.totalTokens = 0;
for (const i of chatStore.history
.filter(({ hide }) => !hide)
.slice(chatStore.postBeginIndex)
.map(({ token }) => token)) {
chatStore.totalTokens += i;
}
setChatStore({ ...chatStore });
}}
>
</button>
<button onClick={() => setShowEdit(true)}>🖋</button>
{showEdit && (
<div
className={
"absolute bg-black bg-opacity-50 w-full h-full top-0 left-0 pt-5 px-5 pb-20 rounded"
}
>
<textarea
className={"relative w-full h-full"}
value={chat.content}
onChange={(event: any) => {
chat.content = event.target.value;
setChatStore({ ...chatStore });
}}
></textarea>
<div className={"w-full flex justify-center"}>
<button
className={"m-2 p-1 rounded bg-green-500"}
onClick={() => {
setShowEdit(false);
}}
>
Close
</button>
</div>
</div>
)}
</div>
)}
</div>
</div>
</>

View File

@@ -45,10 +45,30 @@ const SelectModel = (props: {
);
};
const LongInput = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "systemMessageContent";
help: string;
}) => {
return (
<Help help={props.help}>
<textarea
className="m-2 p-2 border rounded focus w-full"
value={props.chatStore[props.field]}
onChange={(event: any) => {
props.chatStore[props.field] = event.target.value;
props.setChatStore({ ...props.chatStore });
}}
></textarea>
</Help>
);
};
const Input = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "apiKey" | "systemMessageContent" | "apiEndpoint";
field: "apiKey" | "apiEndpoint";
help: string;
}) => {
return (
@@ -68,7 +88,15 @@ const Input = (props: {
const Number = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "totalTokens" | "maxTokens" | "tokenMargin" | "postBeginIndex";
field:
| "totalTokens"
| "maxTokens"
| "tokenMargin"
| "postBeginIndex"
| "temperature"
| "top_p"
| "presence_penalty"
| "frequency_penalty";
readOnly: boolean;
help: string;
}) => {
@@ -82,7 +110,7 @@ const Number = (props: {
value={props.chatStore[props.field]}
onChange={(event: any) => {
console.log("type", typeof event.target.value);
let newNumber = parseInt(event.target.value);
let newNumber = parseFloat(event.target.value);
if (newNumber < 0) newNumber = 0;
props.chatStore[props.field] = newNumber;
props.setChatStore({ ...props.chatStore });
@@ -94,7 +122,7 @@ const Number = (props: {
const Choice = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "streamMode";
field: "streamMode" | "develop_mode";
help: string;
}) => {
return (
@@ -119,7 +147,7 @@ export default (props: {
setShow: StateUpdater<boolean>;
selectedChatStoreIndex: number;
}) => {
const link =
let link =
location.protocol +
"//" +
location.host +
@@ -131,6 +159,9 @@ export default (props: {
}&model=${props.chatStore.model}&sys=${encodeURIComponent(
props.chatStore.systemMessageContent
)}`;
if (props.chatStore.develop_mode) {
link = link + `&dev=true`;
}
const importFileRef = createRef();
const [totalCost, setTotalCost] = useState(getTotalCost());
@@ -157,7 +188,7 @@ export default (props: {
Total cost in this session ${props.chatStore.cost.toFixed(4)}
</p>
<div className="box">
<Input
<LongInput
field="systemMessageContent"
help="系统消息用于指示ChatGPT的角色和一些前置条件例如“你是一个有帮助的人工智能助理”或者“你是一个专业英语翻译把我的话全部翻译成英语”详情参考 OPEAN AI API 文档"
{...props}
@@ -177,6 +208,11 @@ export default (props: {
help="流模式,使用 stream mode 将可以动态看到生成内容,但无法准确计算 token 数量,在 token 数量过多时可能会裁切过多或过少历史消息"
{...props}
/>
<Choice
field="develop_mode"
help="开发者模式,拥有更多选项及功能"
{...props}
/>
<SelectModel
help="模型,默认 3.5。不同模型性能和定价也不同,请参考 API 文档。GPT-4 模型处于内测阶段,需要向 OPENAI 申请, 请确保您有访问权限)"
{...props}
@@ -205,6 +241,20 @@ export default (props: {
readOnly={true}
{...props}
/>
<Number field="temperature" help="温度" readOnly={false} {...props} />
<Number field="top_p" help="top_p" readOnly={false} {...props} />
<Number
field="presence_penalty"
help="presence_penalty"
readOnly={false}
{...props}
/>
<Number
field="frequency_penalty"
help="frequency_penalty"
readOnly={false}
{...props}
/>
<p className="flex justify-evenly">
<button
className="p-2 m-2 rounded bg-amber-500"