Merge branch 'tool-call'
This commit is contained in:
10
src/app.tsx
10
src/app.tsx
@@ -21,9 +21,11 @@ export interface TemplateAPI {
|
|||||||
key: string;
|
key: string;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatStore {
|
export interface ChatStore {
|
||||||
chatgpt_api_web_version: string;
|
chatgpt_api_web_version: string;
|
||||||
systemMessageContent: string;
|
systemMessageContent: string;
|
||||||
|
toolsString: string;
|
||||||
history: ChatStoreMessage[];
|
history: ChatStoreMessage[];
|
||||||
postBeginIndex: number;
|
postBeginIndex: number;
|
||||||
tokenMargin: number;
|
tokenMargin: number;
|
||||||
@@ -67,11 +69,13 @@ export const newChatStore = (
|
|||||||
tts_api = "",
|
tts_api = "",
|
||||||
tts_key = "",
|
tts_key = "",
|
||||||
tts_speed = 1.0,
|
tts_speed = 1.0,
|
||||||
tts_speed_enabled = false
|
tts_speed_enabled = false,
|
||||||
|
toolsString = ""
|
||||||
): ChatStore => {
|
): ChatStore => {
|
||||||
return {
|
return {
|
||||||
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
||||||
systemMessageContent: getDefaultParams("sys", systemMessageContent),
|
systemMessageContent: getDefaultParams("sys", systemMessageContent),
|
||||||
|
toolsString,
|
||||||
history: [],
|
history: [],
|
||||||
postBeginIndex: 0,
|
postBeginIndex: 0,
|
||||||
tokenMargin: 1024,
|
tokenMargin: 1024,
|
||||||
@@ -173,6 +177,7 @@ export function App() {
|
|||||||
if (ret.maxGenTokens_enabled === undefined) ret.maxGenTokens_enabled = true;
|
if (ret.maxGenTokens_enabled === undefined) ret.maxGenTokens_enabled = true;
|
||||||
if (ret.model === undefined) ret.model = "gpt-3.5-turbo";
|
if (ret.model === undefined) ret.model = "gpt-3.5-turbo";
|
||||||
if (ret.responseModelName === undefined) ret.responseModelName = "";
|
if (ret.responseModelName === undefined) ret.responseModelName = "";
|
||||||
|
if (ret.toolsString === undefined) ret.toolsString = "";
|
||||||
if (ret.chatgpt_api_web_version === undefined)
|
if (ret.chatgpt_api_web_version === undefined)
|
||||||
// this is from old version becasue it is undefined,
|
// this is from old version becasue it is undefined,
|
||||||
// so no higher than v1.3.0
|
// so no higher than v1.3.0
|
||||||
@@ -250,7 +255,8 @@ export function App() {
|
|||||||
chatStore.tts_api,
|
chatStore.tts_api,
|
||||||
chatStore.tts_key,
|
chatStore.tts_key,
|
||||||
chatStore.tts_speed,
|
chatStore.tts_speed,
|
||||||
chatStore.tts_speed_enabled
|
chatStore.tts_speed_enabled,
|
||||||
|
chatStore.toolsString
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
setSelectedChatIndex(newKey as number);
|
setSelectedChatIndex(newKey as number);
|
||||||
|
|||||||
163
src/chatbox.tsx
163
src/chatbox.tsx
@@ -13,7 +13,9 @@ import ChatGPT, {
|
|||||||
calculate_token_length,
|
calculate_token_length,
|
||||||
ChunkMessage,
|
ChunkMessage,
|
||||||
FetchResponse,
|
FetchResponse,
|
||||||
|
Message as MessageType,
|
||||||
MessageDetail,
|
MessageDetail,
|
||||||
|
ToolCall,
|
||||||
} from "./chatgpt";
|
} from "./chatgpt";
|
||||||
import Message from "./message";
|
import Message from "./message";
|
||||||
import models from "./models";
|
import models from "./models";
|
||||||
@@ -41,6 +43,9 @@ export default function ChatBOX(props: {
|
|||||||
const [generatingMessage, setGeneratingMessage] = useState("");
|
const [generatingMessage, setGeneratingMessage] = useState("");
|
||||||
const [showRetry, setShowRetry] = useState(false);
|
const [showRetry, setShowRetry] = useState(false);
|
||||||
const [isRecording, setIsRecording] = useState("Mic");
|
const [isRecording, setIsRecording] = useState("Mic");
|
||||||
|
const [showAddToolMsg, setShowAddToolMsg] = useState(false);
|
||||||
|
const [newToolCallID, setNewToolCallID] = useState("");
|
||||||
|
const [newToolContent, setNewToolContent] = useState("");
|
||||||
const mediaRef = createRef();
|
const mediaRef = createRef();
|
||||||
|
|
||||||
const messagesEndRef = createRef();
|
const messagesEndRef = createRef();
|
||||||
@@ -67,12 +72,48 @@ export default function ChatBOX(props: {
|
|||||||
let responseTokenCount = 0;
|
let responseTokenCount = 0;
|
||||||
chatStore.streamMode = true;
|
chatStore.streamMode = true;
|
||||||
const allChunkMessage: string[] = [];
|
const allChunkMessage: string[] = [];
|
||||||
|
const allChunkTool: ToolCall[] = [];
|
||||||
setShowGenerating(true);
|
setShowGenerating(true);
|
||||||
for await (const i of client.processStreamResponse(response)) {
|
for await (const i of client.processStreamResponse(response)) {
|
||||||
chatStore.responseModelName = i.model;
|
chatStore.responseModelName = i.model;
|
||||||
responseTokenCount += 1;
|
responseTokenCount += 1;
|
||||||
allChunkMessage.push(i.choices[0].delta.content ?? "");
|
allChunkMessage.push(i.choices[0].delta.content ?? "");
|
||||||
setGeneratingMessage(allChunkMessage.join(""));
|
const tool_calls = i.choices[0].delta.tool_calls;
|
||||||
|
if (tool_calls) {
|
||||||
|
for (const tool_call of tool_calls) {
|
||||||
|
// init
|
||||||
|
if (tool_call.id) {
|
||||||
|
allChunkTool.push({
|
||||||
|
id: tool_call.id,
|
||||||
|
type: tool_call.type,
|
||||||
|
index: tool_call.index,
|
||||||
|
function: {
|
||||||
|
name: tool_call.function.name,
|
||||||
|
arguments: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update tool call arguments
|
||||||
|
const tool = allChunkTool.find(
|
||||||
|
(tool) => tool.index === tool_call.index
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
console.log("tool (by index) not found", tool_call.index);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tool.function.arguments += tool_call.function.arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setGeneratingMessage(
|
||||||
|
allChunkMessage.join("") +
|
||||||
|
allChunkTool.map((tool) => {
|
||||||
|
return `Tool Call ID: ${tool.id}\nType: ${tool.type}\nFunction: ${tool.function.name}\nArguments: ${tool.function.arguments}`;
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setShowGenerating(false);
|
setShowGenerating(false);
|
||||||
const content = allChunkMessage.join("");
|
const content = allChunkMessage.join("");
|
||||||
@@ -99,6 +140,7 @@ export default function ChatBOX(props: {
|
|||||||
chatStore.history.push({
|
chatStore.history.push({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content,
|
content,
|
||||||
|
tool_calls: allChunkTool,
|
||||||
hide: false,
|
hide: false,
|
||||||
token: responseTokenCount,
|
token: responseTokenCount,
|
||||||
example: false,
|
example: false,
|
||||||
@@ -127,7 +169,7 @@ export default function ChatBOX(props: {
|
|||||||
chatStore.cost += cost;
|
chatStore.cost += cost;
|
||||||
addTotalCost(cost);
|
addTotalCost(cost);
|
||||||
}
|
}
|
||||||
const content = client.processFetchResponse(data);
|
const msg = client.processFetchResponse(data);
|
||||||
|
|
||||||
// estimate user's input message token
|
// estimate user's input message token
|
||||||
let aboveToken = 0;
|
let aboveToken = 0;
|
||||||
@@ -147,9 +189,11 @@ export default function ChatBOX(props: {
|
|||||||
|
|
||||||
chatStore.history.push({
|
chatStore.history.push({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content,
|
content: msg.content,
|
||||||
|
tool_calls: msg.tool_calls,
|
||||||
hide: false,
|
hide: false,
|
||||||
token: data.usage.completion_tokens ?? calculate_token_length(content),
|
token:
|
||||||
|
data.usage.completion_tokens ?? calculate_token_length(msg.content),
|
||||||
example: false,
|
example: false,
|
||||||
});
|
});
|
||||||
setShowGenerating(false);
|
setShowGenerating(false);
|
||||||
@@ -160,6 +204,7 @@ export default function ChatBOX(props: {
|
|||||||
// manually copy status from chatStore to client
|
// manually copy status from chatStore to client
|
||||||
client.apiEndpoint = chatStore.apiEndpoint;
|
client.apiEndpoint = chatStore.apiEndpoint;
|
||||||
client.sysMessageContent = chatStore.systemMessageContent;
|
client.sysMessageContent = chatStore.systemMessageContent;
|
||||||
|
client.toolsString = chatStore.toolsString;
|
||||||
client.tokens_margin = chatStore.tokenMargin;
|
client.tokens_margin = chatStore.tokenMargin;
|
||||||
client.temperature = chatStore.temperature;
|
client.temperature = chatStore.temperature;
|
||||||
client.enable_temperature = chatStore.temperature_enabled;
|
client.enable_temperature = chatStore.temperature_enabled;
|
||||||
@@ -172,18 +217,22 @@ export default function ChatBOX(props: {
|
|||||||
.filter(({ hide }) => !hide)
|
.filter(({ hide }) => !hide)
|
||||||
.slice(chatStore.postBeginIndex)
|
.slice(chatStore.postBeginIndex)
|
||||||
// only copy content and role attribute to client for posting
|
// only copy content and role attribute to client for posting
|
||||||
.map(({ content, role, example }) => {
|
.map(({ content, role, example, tool_call_id, tool_calls }) => {
|
||||||
if (example) {
|
const ret: MessageType = {
|
||||||
return {
|
|
||||||
content,
|
|
||||||
role: "system",
|
|
||||||
name: role === "assistant" ? "example_assistant" : "example_user",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
content,
|
content,
|
||||||
role,
|
role,
|
||||||
|
tool_calls,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (example) {
|
||||||
|
ret.name =
|
||||||
|
ret.role === "assistant" ? "example_assistant" : "example_user";
|
||||||
|
ret.role = "system";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool_call_id) ret.tool_call_id = tool_call_id;
|
||||||
|
|
||||||
|
return ret;
|
||||||
});
|
});
|
||||||
client.model = chatStore.model;
|
client.model = chatStore.model;
|
||||||
client.max_tokens = chatStore.maxTokens;
|
client.max_tokens = chatStore.maxTokens;
|
||||||
@@ -406,6 +455,7 @@ export default function ChatBOX(props: {
|
|||||||
className="mx-2 underline cursor-pointer"
|
className="mx-2 underline cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
chatStore.systemMessageContent = "";
|
chatStore.systemMessageContent = "";
|
||||||
|
chatStore.toolsString = "";
|
||||||
chatStore.history = [];
|
chatStore.history = [];
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
@@ -943,6 +993,93 @@ export default function ChatBOX(props: {
|
|||||||
{Tr("User")}
|
{Tr("User")}
|
||||||
</button>
|
</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={() => {
|
||||||
|
setShowAddToolMsg(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("Tool")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{showAddToolMsg && (
|
||||||
|
<div
|
||||||
|
className="absolute z-10 bg-black bg-opacity-50 w-full h-full flex justify-center items-center left-0 top-0 overflow-scroll"
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddToolMsg(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white rounded p-2 z-20 flex flex-col"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>Add Tool Message</h2>
|
||||||
|
<hr className="my-2" />
|
||||||
|
<span>
|
||||||
|
<label>tool_call_id</label>
|
||||||
|
<input
|
||||||
|
className="rounded m-1 p-1 border-2 border-gray-400"
|
||||||
|
type="text"
|
||||||
|
value={newToolCallID}
|
||||||
|
onChange={(event: any) =>
|
||||||
|
setNewToolCallID(event.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<label>Content</label>
|
||||||
|
<textarea
|
||||||
|
className="rounded m-1 p-1 border-2 border-gray-400"
|
||||||
|
rows={5}
|
||||||
|
value={newToolContent}
|
||||||
|
onChange={(event: any) =>
|
||||||
|
setNewToolContent(event.target.value)
|
||||||
|
}
|
||||||
|
></textarea>
|
||||||
|
</span>
|
||||||
|
<span className={`flex justify-between p-2`}>
|
||||||
|
<button
|
||||||
|
className="rounded m-1 p-1 border-2 bg-red-400 hover:bg-red-600"
|
||||||
|
onClick={() => setShowAddToolMsg(false)}
|
||||||
|
>
|
||||||
|
{Tr("Cancle")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||||
|
onClick={() => {
|
||||||
|
if (!newToolCallID.trim()) {
|
||||||
|
alert("tool_call_id is empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newToolContent.trim()) {
|
||||||
|
alert("content is empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chatStore.history.push({
|
||||||
|
role: "tool",
|
||||||
|
tool_call_id: newToolCallID.trim(),
|
||||||
|
content: newToolContent.trim(),
|
||||||
|
token: calculate_token_length(newToolContent),
|
||||||
|
hide: false,
|
||||||
|
example: false,
|
||||||
|
});
|
||||||
|
update_total_tokens();
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
setNewToolCallID("");
|
||||||
|
setNewToolContent("");
|
||||||
|
setShowAddToolMsg(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("Add")}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,13 +8,53 @@ export interface MessageDetail {
|
|||||||
text?: string;
|
text?: string;
|
||||||
image_url?: ImageURL;
|
image_url?: ImageURL;
|
||||||
}
|
}
|
||||||
|
export interface ToolCall {
|
||||||
|
index: number;
|
||||||
|
id?: string;
|
||||||
|
type: string;
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
export interface Message {
|
export interface Message {
|
||||||
role: "system" | "user" | "assistant" | "function";
|
role: "system" | "user" | "assistant" | "tool";
|
||||||
content: string | MessageDetail[];
|
content: string | MessageDetail[];
|
||||||
name?: "example_user" | "example_assistant";
|
name?: "example_user" | "example_assistant";
|
||||||
|
tool_calls?: ToolCall[];
|
||||||
|
tool_call_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Delta {
|
||||||
|
role?: string;
|
||||||
|
content?: string;
|
||||||
|
tool_calls?: ToolCall[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Choices {
|
||||||
|
index: number;
|
||||||
|
delta: Delta;
|
||||||
|
finish_reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamingResponseChunk {
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
created: number;
|
||||||
|
model: string;
|
||||||
|
system_fingerprint: string;
|
||||||
|
choices: Choices[];
|
||||||
}
|
}
|
||||||
export const getMessageText = (message: Message): string => {
|
export const getMessageText = (message: Message): string => {
|
||||||
if (typeof message.content === "string") {
|
if (typeof message.content === "string") {
|
||||||
|
// function call message
|
||||||
|
if (message.tool_calls) {
|
||||||
|
return message.tool_calls
|
||||||
|
.map((tc) => {
|
||||||
|
return `Tool Call ID: ${tc.id}\nType: ${tc.type}\nFunction: ${tc.function.name}\nArguments: ${tc.function.arguments}}`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
return message.content;
|
return message.content;
|
||||||
}
|
}
|
||||||
return message.content
|
return message.content
|
||||||
@@ -78,6 +118,7 @@ class Chat {
|
|||||||
OPENAI_API_KEY: string;
|
OPENAI_API_KEY: string;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
sysMessageContent: string;
|
sysMessageContent: string;
|
||||||
|
toolsString: string;
|
||||||
total_tokens: number;
|
total_tokens: number;
|
||||||
max_tokens: number;
|
max_tokens: number;
|
||||||
max_gen_tokens: number;
|
max_gen_tokens: number;
|
||||||
@@ -96,6 +137,7 @@ class Chat {
|
|||||||
OPENAI_API_KEY: string | undefined,
|
OPENAI_API_KEY: string | undefined,
|
||||||
{
|
{
|
||||||
systemMessage = "",
|
systemMessage = "",
|
||||||
|
toolsString = "",
|
||||||
max_tokens = 4096,
|
max_tokens = 4096,
|
||||||
max_gen_tokens = 2048,
|
max_gen_tokens = 2048,
|
||||||
enable_max_gen_tokens = true,
|
enable_max_gen_tokens = true,
|
||||||
@@ -121,6 +163,7 @@ class Chat {
|
|||||||
this.enable_max_gen_tokens = enable_max_gen_tokens;
|
this.enable_max_gen_tokens = enable_max_gen_tokens;
|
||||||
this.tokens_margin = tokens_margin;
|
this.tokens_margin = tokens_margin;
|
||||||
this.sysMessageContent = systemMessage;
|
this.sysMessageContent = systemMessage;
|
||||||
|
this.toolsString = toolsString;
|
||||||
this.apiEndpoint = apiEndPoint;
|
this.apiEndpoint = apiEndPoint;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.temperature = temperature;
|
this.temperature = temperature;
|
||||||
@@ -178,6 +221,25 @@ class Chat {
|
|||||||
body["max_tokens"] = this.max_gen_tokens;
|
body["max_tokens"] = this.max_gen_tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parse toolsString to function call format
|
||||||
|
const ts = this.toolsString.trim();
|
||||||
|
if (ts) {
|
||||||
|
try {
|
||||||
|
const fcList: any[] = JSON.parse(ts);
|
||||||
|
body["tools"] = fcList.map((fc) => {
|
||||||
|
return {
|
||||||
|
type: "function",
|
||||||
|
function: fc,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("toolsString parse error");
|
||||||
|
throw (
|
||||||
|
"Function call toolsString parse error, not a valied json list: " + e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return fetch(this.apiEndpoint, {
|
return fetch(this.apiEndpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -224,7 +286,7 @@ class Chat {
|
|||||||
console.log("line", line);
|
console.log("line", line);
|
||||||
try {
|
try {
|
||||||
const jsonStr = line.slice("data:".length).trim();
|
const jsonStr = line.slice("data:".length).trim();
|
||||||
const json = JSON.parse(jsonStr);
|
const json = JSON.parse(jsonStr) as StreamingResponseChunk;
|
||||||
yield json;
|
yield json;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Chunk parse error at: ${line}`);
|
console.log(`Chunk parse error at: ${line}`);
|
||||||
@@ -234,7 +296,7 @@ class Chat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processFetchResponse(resp: FetchResponse): string {
|
processFetchResponse(resp: FetchResponse): Message {
|
||||||
if (resp.error !== undefined) {
|
if (resp.error !== undefined) {
|
||||||
throw JSON.stringify(resp.error);
|
throw JSON.stringify(resp.error);
|
||||||
}
|
}
|
||||||
@@ -249,15 +311,19 @@ class Chat {
|
|||||||
this.forgetSomeMessages();
|
this.forgetSomeMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
let content = resp.choices[0].message?.content ?? "";
|
||||||
(resp?.choices[0]?.message?.content as string) ??
|
if (
|
||||||
`Error: ${JSON.stringify(resp)}`
|
!resp.choices[0]?.message?.content &&
|
||||||
);
|
!resp.choices[0]?.message?.tool_calls
|
||||||
}
|
) {
|
||||||
|
content = `Unparsed response: ${JSON.stringify(resp)}`;
|
||||||
|
}
|
||||||
|
|
||||||
async complete(): Promise<string> {
|
return {
|
||||||
const resp = await this.fetch();
|
role: "assistant",
|
||||||
return this.processFetchResponse(resp);
|
content,
|
||||||
|
tool_calls: resp?.choices[0]?.message?.tool_calls,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
completeWithSteam() {
|
completeWithSteam() {
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ body::-webkit-scrollbar {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.message-content {
|
.message-content {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
217
src/message.tsx
217
src/message.tsx
@@ -12,35 +12,97 @@ interface EditMessageProps {
|
|||||||
setChatStore: (cs: ChatStore) => void;
|
setChatStore: (cs: ChatStore) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isVailedJSON = (str: string): boolean => {
|
||||||
|
try {
|
||||||
|
JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
function EditMessage(props: EditMessageProps) {
|
function EditMessage(props: EditMessageProps) {
|
||||||
const { setShowEdit, chat, setChatStore, chatStore } = props;
|
const { setShowEdit, chat, setChatStore, chatStore } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
"absolute bg-black bg-opacity-50 w-full h-full top-0 left-0 pt-5 px-5 pb-20 rounded z-10"
|
"absolute bg-black bg-opacity-50 w-full h-full top-0 left-0 rounded z-10 overflow-scroll"
|
||||||
}
|
}
|
||||||
onClick={() => setShowEdit(false)}
|
onClick={() => setShowEdit(false)}
|
||||||
>
|
>
|
||||||
<div className="w-full h-full z-20">
|
<div
|
||||||
|
className="m-10 p-2 bg-white rounded"
|
||||||
|
onClick={(event: any) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
{typeof chat.content === "string" ? (
|
{typeof chat.content === "string" ? (
|
||||||
<textarea
|
<div className="flex flex-col">
|
||||||
className={"w-full h-full"}
|
{chat.tool_call_id && (
|
||||||
value={chat.content}
|
<span className="my-2">
|
||||||
onClick={(event: any) => {
|
<label>tool_call_id: </label>
|
||||||
event.stopPropagation();
|
<input
|
||||||
}}
|
className="rounded border border-gray-400"
|
||||||
onChange={(event: any) => {
|
value={chat.tool_call_id}
|
||||||
chat.content = event.target.value;
|
onChange={(event: any) => {
|
||||||
chat.token = calculate_token_length(chat.content);
|
chat.tool_call_id = event.target.value;
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
onKeyPress={(event: any) => {
|
/>
|
||||||
if (event.keyCode == 27) {
|
</span>
|
||||||
setShowEdit(false);
|
)}
|
||||||
}
|
{chat.tool_calls &&
|
||||||
}}
|
chat.tool_calls.map((tool_call) => (
|
||||||
></textarea>
|
<div className="flex flex-col w-full">
|
||||||
|
<span className="my-2 w-full">
|
||||||
|
<label>Tool Call ID: </label>
|
||||||
|
<input
|
||||||
|
value={tool_call.id}
|
||||||
|
className="rounded border border-gray-400"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="my-2 w-full">
|
||||||
|
<label>Function: </label>
|
||||||
|
<input
|
||||||
|
value={tool_call.function.name}
|
||||||
|
className="rounded border border-gray-400"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="my-2">
|
||||||
|
<label>Arguments: </label>
|
||||||
|
<span className="underline">
|
||||||
|
Vailed JSON:{" "}
|
||||||
|
{isVailedJSON(tool_call.function.arguments) ? "🆗" : "❌"}
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
className="rounded border border-gray-400 w-full h-32 my-2"
|
||||||
|
value={tool_call.function.arguments}
|
||||||
|
onChange={(event: any) => {
|
||||||
|
tool_call.function.arguments =
|
||||||
|
event.target.value.trim();
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
}}
|
||||||
|
></textarea>
|
||||||
|
</span>
|
||||||
|
<hr className="my-2" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<textarea
|
||||||
|
className="rounded border border-gray-400 w-full h-32 my-2"
|
||||||
|
value={chat.content}
|
||||||
|
onChange={(event: any) => {
|
||||||
|
chat.content = event.target.value;
|
||||||
|
chat.token = calculate_token_length(chat.content);
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
}}
|
||||||
|
onKeyPress={(event: any) => {
|
||||||
|
if (event.keyCode == 27) {
|
||||||
|
setShowEdit(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={"w-full h-full flex flex-col overflow-scroll"}
|
className={"w-full h-full flex flex-col overflow-scroll"}
|
||||||
@@ -245,14 +307,18 @@ export default function Message(props: Props) {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const CopyIcon = () => {
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setShowCopiedHint(true);
|
||||||
|
setTimeout(() => setShowCopiedHint(false), 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CopyIcon = ({ textToCopy }: { textToCopy: string }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(getMessageText(chat));
|
copyToClipboard(textToCopy);
|
||||||
setShowCopiedHint(true);
|
|
||||||
setTimeout(() => setShowCopiedHint(false), 1000);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
📋
|
📋
|
||||||
@@ -288,40 +354,81 @@ export default function Message(props: Props) {
|
|||||||
: "bg-green-400"
|
: "bg-green-400"
|
||||||
} ${chat.hide ? "opacity-50" : ""}`}
|
} ${chat.hide ? "opacity-50" : ""}`}
|
||||||
>
|
>
|
||||||
<p className={renderMarkdown ? "" : "message-content"}>
|
{chat.hide ? (
|
||||||
{typeof chat.content !== "string" ? (
|
getMessageText(chat).split("\n")[0].slice(0, 18) + "... (deleted)"
|
||||||
// render for multiple messages
|
) : typeof chat.content !== "string" ? (
|
||||||
chat.content.map((mdt) =>
|
chat.content.map((mdt) =>
|
||||||
mdt.type === "text" ? (
|
mdt.type === "text" ? (
|
||||||
chat.hide ? (
|
chat.hide ? (
|
||||||
mdt.text?.split("\n")[0].slice(0, 16) + "... (deleted)"
|
mdt.text?.split("\n")[0].slice(0, 16) + "... (deleted)"
|
||||||
) : renderMarkdown ? (
|
) : renderMarkdown ? (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<Markdown markdown={mdt.text} />
|
<Markdown markdown={mdt.text} />
|
||||||
) : (
|
|
||||||
mdt.text
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<img
|
mdt.text
|
||||||
className="cursor-pointer max-w-xs max-h-32 p-1"
|
|
||||||
src={mdt.image_url?.url}
|
|
||||||
onClick={() => {
|
|
||||||
window.open(mdt.image_url?.url, "_blank");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className="cursor-pointer max-w-xs max-h-32 p-1"
|
||||||
|
src={mdt.image_url?.url}
|
||||||
|
onClick={() => {
|
||||||
|
window.open(mdt.image_url?.url, "_blank");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
) : // render for single message
|
)
|
||||||
chat.hide ? (
|
) : chat.tool_calls ? (
|
||||||
getMessageText(chat).split("\n")[0].slice(0, 16) +
|
<div className="message-content">
|
||||||
"... (deleted)"
|
<div>
|
||||||
) : renderMarkdown ? (
|
{chat.tool_calls?.map((tool_call) => (
|
||||||
// @ts-ignore
|
<div className="bg-blue-300 dark:bg-blue-800 p-1 rounded my-1">
|
||||||
<Markdown markdown={getMessageText(chat)} />
|
<strong>
|
||||||
) : (
|
Tool Call ID:{" "}
|
||||||
getMessageText(chat)
|
<span
|
||||||
)}
|
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
||||||
</p>
|
onClick={() => copyToClipboard(String(tool_call.id))}
|
||||||
|
>
|
||||||
|
{tool_call?.id}
|
||||||
|
</span>
|
||||||
|
</strong>
|
||||||
|
<p>Type: {tool_call?.type}</p>
|
||||||
|
<p>
|
||||||
|
Function:
|
||||||
|
<span
|
||||||
|
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
||||||
|
onClick={() =>
|
||||||
|
copyToClipboard(tool_call.function.name)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tool_call.function.name}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Arguments:
|
||||||
|
<span
|
||||||
|
className="p-1 m-1 rounded cursor-pointer hover:opacity-50 hover:underline"
|
||||||
|
onClick={() =>
|
||||||
|
copyToClipboard(tool_call.function.arguments)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tool_call.function.arguments}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : renderMarkdown ? (
|
||||||
|
// @ts-ignore
|
||||||
|
<Markdown markdown={getMessageText(chat)} />
|
||||||
|
) : (
|
||||||
|
<div className="message-content">
|
||||||
|
{
|
||||||
|
// only show when content is string or list of message
|
||||||
|
chat.content && getMessageText(chat)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<hr className="mt-2" />
|
<hr className="mt-2" />
|
||||||
<div className="w-full flex justify-between">
|
<div className="w-full flex justify-between">
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
@@ -333,7 +440,7 @@ export default function Message(props: Props) {
|
|||||||
setChatStore={setChatStore}
|
setChatStore={setChatStore}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CopyIcon />
|
<CopyIcon textToCopy={getMessageText(chat)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showEdit && (
|
{showEdit && (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import models from "./models";
|
|||||||
import { TemplateChatStore } from "./chatbox";
|
import { TemplateChatStore } from "./chatbox";
|
||||||
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||||
import p from "preact-markdown";
|
import p from "preact-markdown";
|
||||||
|
import { isVailedJSON } from "./message";
|
||||||
|
|
||||||
const TTS_VOICES: string[] = [
|
const TTS_VOICES: string[] = [
|
||||||
"alloy",
|
"alloy",
|
||||||
@@ -60,7 +61,7 @@ const SelectModel = (props: {
|
|||||||
const LongInput = (props: {
|
const LongInput = (props: {
|
||||||
chatStore: ChatStore;
|
chatStore: ChatStore;
|
||||||
setChatStore: (cs: ChatStore) => void;
|
setChatStore: (cs: ChatStore) => void;
|
||||||
field: "systemMessageContent";
|
field: "systemMessageContent" | "toolsString";
|
||||||
help: string;
|
help: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
@@ -373,6 +374,15 @@ export default (props: {
|
|||||||
help="系统消息,用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档"
|
help="系统消息,用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
<span>
|
||||||
|
Valied JSON:{" "}
|
||||||
|
{isVailedJSON(props.chatStore.toolsString) ? "🆗" : "❌"}
|
||||||
|
</span>
|
||||||
|
<LongInput
|
||||||
|
field="toolsString"
|
||||||
|
help="function call tools, should be valied json format in list"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
field="apiKey"
|
field="apiKey"
|
||||||
help="OPEN AI API 密钥,请勿泄漏此密钥"
|
help="OPEN AI API 密钥,请勿泄漏此密钥"
|
||||||
|
|||||||
Reference in New Issue
Block a user