856 lines
28 KiB
TypeScript
856 lines
28 KiB
TypeScript
import { IDBPDatabase } from "idb";
|
|
import { useRef } from "react";
|
|
import { useEffect, useState, Dispatch } from "react";
|
|
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
|
import {
|
|
STORAGE_NAME_TEMPLATE,
|
|
STORAGE_NAME_TEMPLATE_API,
|
|
STORAGE_NAME_TEMPLATE_API_IMAGE_GEN,
|
|
STORAGE_NAME_TEMPLATE_API_TTS,
|
|
STORAGE_NAME_TEMPLATE_API_WHISPER,
|
|
STORAGE_NAME_TEMPLATE_TOOLS,
|
|
} from "@/const";
|
|
import { addTotalCost, getTotalCost } from "@/utils/totalCost";
|
|
import ChatGPT, {
|
|
calculate_token_length,
|
|
FetchResponse,
|
|
Message as MessageType,
|
|
MessageDetail,
|
|
ToolCall,
|
|
Logprobs,
|
|
StreamingUsage,
|
|
} from "@/chatgpt";
|
|
import {
|
|
ChatStore,
|
|
ChatStoreMessage,
|
|
TemplateChatStore,
|
|
TemplateAPI,
|
|
TemplateTools,
|
|
} from "../types/chatstore";
|
|
import Message from "@/message";
|
|
import { models } from "@/types/models";
|
|
import Settings from "@/components/Settings";
|
|
import { AddImage } from "@/addImage";
|
|
import { ListAPIs } from "@/listAPIs";
|
|
import { ListToolsTempaltes } from "@/listToolsTemplates";
|
|
import { autoHeight } from "@/textarea";
|
|
import Search from "@/search";
|
|
import Templates from "@/components/Templates";
|
|
import VersionHint from "@/components/VersionHint";
|
|
import StatusBar from "@/components/StatusBar";
|
|
import WhisperButton from "@/components/WhisperButton";
|
|
import AddToolMsg from "./AddToolMsg";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { ChatInput } from "@/components/ui/chat/chat-input";
|
|
import {
|
|
ChatBubble,
|
|
ChatBubbleAvatar,
|
|
ChatBubbleMessage,
|
|
ChatBubbleAction,
|
|
ChatBubbleActionWrapper,
|
|
} from "@/components/ui/chat/chat-bubble";
|
|
|
|
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
|
|
import {
|
|
AlertTriangleIcon,
|
|
ArrowUpIcon,
|
|
CornerDownLeftIcon,
|
|
CornerLeftUpIcon,
|
|
CornerUpLeftIcon,
|
|
GlobeIcon,
|
|
ImageIcon,
|
|
InfoIcon,
|
|
KeyIcon,
|
|
SearchIcon,
|
|
Settings2,
|
|
Settings2Icon,
|
|
} from "lucide-react";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import {
|
|
NavigationMenu,
|
|
NavigationMenuContent,
|
|
NavigationMenuItem,
|
|
NavigationMenuLink,
|
|
NavigationMenuList,
|
|
NavigationMenuTrigger,
|
|
navigationMenuTriggerStyle,
|
|
} from "@/components/ui/navigation-menu";
|
|
|
|
export default function ChatBOX(props: {
|
|
db: Promise<IDBPDatabase<ChatStore>>;
|
|
chatStore: ChatStore;
|
|
setChatStore: (cs: ChatStore) => void;
|
|
selectedChatIndex: number;
|
|
setSelectedChatIndex: Dispatch<number>;
|
|
}) {
|
|
const { chatStore, setChatStore } = props;
|
|
// prevent error
|
|
if (chatStore === undefined) return <div></div>;
|
|
const [inputMsg, setInputMsg] = useState("");
|
|
const [images, setImages] = useState<MessageDetail[]>([]);
|
|
const [showAddImage, setShowAddImage] = useState(false);
|
|
const [showGenerating, setShowGenerating] = useState(false);
|
|
const [generatingMessage, setGeneratingMessage] = useState("");
|
|
const [showRetry, setShowRetry] = useState(false);
|
|
const [showAddToolMsg, setShowAddToolMsg] = useState(false);
|
|
const [showSearch, setShowSearch] = useState(false);
|
|
let default_follow = localStorage.getItem("follow");
|
|
if (default_follow === null) {
|
|
default_follow = "true";
|
|
}
|
|
const [follow, _setFollow] = useState(default_follow === "true");
|
|
|
|
const setFollow = (follow: boolean) => {
|
|
console.log("set follow", follow);
|
|
localStorage.setItem("follow", follow.toString());
|
|
_setFollow(follow);
|
|
};
|
|
|
|
const messagesEndRef = useRef<HTMLElement>(null);
|
|
useEffect(() => {
|
|
if (follow) {
|
|
if (messagesEndRef.current === null) return;
|
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
|
}
|
|
}, [showRetry, showGenerating, generatingMessage]);
|
|
|
|
const client = new ChatGPT(chatStore.apiKey);
|
|
|
|
const _completeWithStreamMode = async (response: Response) => {
|
|
let responseTokenCount = 0;
|
|
const allChunkMessage: string[] = [];
|
|
const allChunkTool: ToolCall[] = [];
|
|
setShowGenerating(true);
|
|
const logprobs: Logprobs = {
|
|
content: [],
|
|
};
|
|
let response_model_name: string | null = null;
|
|
let usage: StreamingUsage | null = null;
|
|
for await (const i of client.processStreamResponse(response)) {
|
|
response_model_name = i.model;
|
|
responseTokenCount += 1;
|
|
if (i.usage) {
|
|
usage = i.usage;
|
|
}
|
|
|
|
const c = i.choices[0];
|
|
|
|
// skip if choice is empty (e.g. azure)
|
|
if (!c) continue;
|
|
|
|
const logprob = c?.logprobs?.content[0]?.logprob;
|
|
if (logprob !== undefined) {
|
|
logprobs.content.push({
|
|
token: c?.delta?.content ?? "",
|
|
logprob,
|
|
});
|
|
console.log(c?.delta?.content, logprob);
|
|
}
|
|
|
|
allChunkMessage.push(c?.delta?.content ?? "");
|
|
const tool_calls = c?.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);
|
|
const content = allChunkMessage.join("");
|
|
|
|
// estimate cost
|
|
let cost = 0;
|
|
if (response_model_name) {
|
|
cost +=
|
|
responseTokenCount *
|
|
(models[response_model_name]?.price?.completion ?? 0);
|
|
let sum = 0;
|
|
for (const msg of chatStore.history
|
|
.filter(({ hide }) => !hide)
|
|
.slice(chatStore.postBeginIndex)) {
|
|
sum += msg.token;
|
|
}
|
|
cost += sum * (models[response_model_name]?.price?.prompt ?? 0);
|
|
if (usage) {
|
|
// use the response usage if exists
|
|
cost = 0;
|
|
cost +=
|
|
(usage.prompt_tokens ?? 0) *
|
|
(models[response_model_name]?.price?.prompt ?? 0);
|
|
cost +=
|
|
(usage.completion_tokens ?? 0) *
|
|
models[response_model_name]?.price?.completion;
|
|
console.log("usage", usage, "cost", cost);
|
|
}
|
|
}
|
|
|
|
console.log("cost", cost);
|
|
chatStore.cost += cost;
|
|
addTotalCost(cost);
|
|
|
|
console.log("save logprobs", logprobs);
|
|
const newMsg: ChatStoreMessage = {
|
|
role: "assistant",
|
|
content,
|
|
hide: false,
|
|
token: responseTokenCount,
|
|
example: false,
|
|
audio: null,
|
|
logprobs,
|
|
response_model_name,
|
|
};
|
|
if (allChunkTool.length > 0) newMsg.tool_calls = allChunkTool;
|
|
|
|
chatStore.history.push(newMsg);
|
|
// manually copy status from client to chatStore
|
|
chatStore.maxTokens = client.max_tokens;
|
|
chatStore.tokenMargin = client.tokens_margin;
|
|
setChatStore({ ...chatStore });
|
|
setGeneratingMessage("");
|
|
setShowGenerating(false);
|
|
};
|
|
|
|
const _completeWithFetchMode = async (response: Response) => {
|
|
const data = (await response.json()) as FetchResponse;
|
|
if (data.model) {
|
|
let cost = 0;
|
|
cost +=
|
|
(data.usage.prompt_tokens ?? 0) *
|
|
(models[data.model]?.price?.prompt ?? 0);
|
|
cost +=
|
|
(data.usage.completion_tokens ?? 0) *
|
|
(models[data.model]?.price?.completion ?? 0);
|
|
chatStore.cost += cost;
|
|
addTotalCost(cost);
|
|
}
|
|
const msg = client.processFetchResponse(data);
|
|
|
|
// estimate user's input message token
|
|
let aboveToken = 0;
|
|
for (const msg of chatStore.history
|
|
.filter(({ hide }) => !hide)
|
|
.slice(chatStore.postBeginIndex, -1)) {
|
|
aboveToken += msg.token;
|
|
}
|
|
if (data.usage.prompt_tokens) {
|
|
const userMessageToken = data.usage.prompt_tokens - aboveToken;
|
|
console.log("set user message token");
|
|
if (chatStore.history.filter((msg) => !msg.hide).length > 0) {
|
|
chatStore.history.filter((msg) => !msg.hide).slice(-1)[0].token =
|
|
userMessageToken;
|
|
}
|
|
}
|
|
|
|
chatStore.history.push({
|
|
role: "assistant",
|
|
content: msg.content,
|
|
tool_calls: msg.tool_calls,
|
|
hide: false,
|
|
token:
|
|
data.usage.completion_tokens ?? calculate_token_length(msg.content),
|
|
example: false,
|
|
audio: null,
|
|
logprobs: data.choices[0]?.logprobs,
|
|
response_model_name: data.model,
|
|
});
|
|
setShowGenerating(false);
|
|
};
|
|
|
|
// wrap the actuall complete api
|
|
const complete = async () => {
|
|
// manually copy status from chatStore to client
|
|
client.apiEndpoint = chatStore.apiEndpoint;
|
|
client.sysMessageContent = chatStore.systemMessageContent;
|
|
client.toolsString = chatStore.toolsString;
|
|
client.tokens_margin = chatStore.tokenMargin;
|
|
client.temperature = chatStore.temperature;
|
|
client.enable_temperature = chatStore.temperature_enabled;
|
|
client.top_p = chatStore.top_p;
|
|
client.enable_top_p = chatStore.top_p_enabled;
|
|
client.frequency_penalty = chatStore.frequency_penalty;
|
|
client.presence_penalty = chatStore.presence_penalty;
|
|
client.json_mode = chatStore.json_mode;
|
|
client.messages = chatStore.history
|
|
// only copy non hidden message
|
|
.filter(({ hide }) => !hide)
|
|
.slice(chatStore.postBeginIndex)
|
|
// only copy content and role attribute to client for posting
|
|
.map(({ content, role, example, tool_call_id, tool_calls }) => {
|
|
const ret: MessageType = {
|
|
content,
|
|
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.max_tokens = chatStore.maxTokens;
|
|
client.max_gen_tokens = chatStore.maxGenTokens;
|
|
client.enable_max_gen_tokens = chatStore.maxGenTokens_enabled;
|
|
|
|
try {
|
|
setShowGenerating(true);
|
|
const response = await client._fetch(
|
|
chatStore.streamMode,
|
|
chatStore.logprobs
|
|
);
|
|
const contentType = response.headers.get("content-type");
|
|
if (contentType?.startsWith("text/event-stream")) {
|
|
await _completeWithStreamMode(response);
|
|
} else if (contentType?.startsWith("application/json")) {
|
|
await _completeWithFetchMode(response);
|
|
} else {
|
|
throw `unknown response content type ${contentType}`;
|
|
}
|
|
// manually copy status from client to chatStore
|
|
chatStore.maxTokens = client.max_tokens;
|
|
chatStore.tokenMargin = client.tokens_margin;
|
|
chatStore.totalTokens = client.total_tokens;
|
|
|
|
console.log("postBeginIndex", chatStore.postBeginIndex);
|
|
setShowRetry(false);
|
|
setChatStore({ ...chatStore });
|
|
} catch (error) {
|
|
setShowRetry(true);
|
|
alert(error);
|
|
} finally {
|
|
setShowGenerating(false);
|
|
props.setSelectedChatIndex(props.selectedChatIndex);
|
|
}
|
|
};
|
|
|
|
// when user click the "send" button or ctrl+Enter in the textarea
|
|
const send = async (msg = "", call_complete = true) => {
|
|
const inputMsg = msg.trim();
|
|
if (!inputMsg && images.length === 0) {
|
|
console.log("empty message");
|
|
return;
|
|
}
|
|
|
|
let content: string | MessageDetail[] = inputMsg;
|
|
if (images.length > 0) {
|
|
content = images;
|
|
}
|
|
if (images.length > 0 && inputMsg.trim()) {
|
|
content = [{ type: "text", text: inputMsg }, ...images];
|
|
}
|
|
chatStore.history.push({
|
|
role: "user",
|
|
content,
|
|
hide: false,
|
|
token: calculate_token_length(inputMsg.trim()),
|
|
example: false,
|
|
audio: null,
|
|
logprobs: null,
|
|
response_model_name: null,
|
|
});
|
|
|
|
// manually calculate token length
|
|
chatStore.totalTokens +=
|
|
calculate_token_length(inputMsg.trim()) + calculate_token_length(images);
|
|
client.total_tokens = chatStore.totalTokens;
|
|
|
|
setChatStore({ ...chatStore });
|
|
setInputMsg("");
|
|
setImages([]);
|
|
if (call_complete) {
|
|
await complete();
|
|
}
|
|
};
|
|
|
|
const [showSettings, setShowSettings] = useState(false);
|
|
|
|
const [templates, _setTemplates] = useState(
|
|
JSON.parse(
|
|
localStorage.getItem(STORAGE_NAME_TEMPLATE) || "[]"
|
|
) as TemplateChatStore[]
|
|
);
|
|
const [templateAPIs, _setTemplateAPIs] = useState(
|
|
JSON.parse(
|
|
localStorage.getItem(STORAGE_NAME_TEMPLATE_API) || "[]"
|
|
) as TemplateAPI[]
|
|
);
|
|
const [templateAPIsWhisper, _setTemplateAPIsWhisper] = useState(
|
|
JSON.parse(
|
|
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_WHISPER) || "[]"
|
|
) as TemplateAPI[]
|
|
);
|
|
const [templateAPIsTTS, _setTemplateAPIsTTS] = useState(
|
|
JSON.parse(
|
|
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_TTS) || "[]"
|
|
) as TemplateAPI[]
|
|
);
|
|
const [templateAPIsImageGen, _setTemplateAPIsImageGen] = useState(
|
|
JSON.parse(
|
|
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_IMAGE_GEN) || "[]"
|
|
) as TemplateAPI[]
|
|
);
|
|
const [toolsTemplates, _setToolsTemplates] = useState(
|
|
JSON.parse(
|
|
localStorage.getItem(STORAGE_NAME_TEMPLATE_TOOLS) || "[]"
|
|
) as TemplateTools[]
|
|
);
|
|
const setTemplates = (templates: TemplateChatStore[]) => {
|
|
localStorage.setItem(STORAGE_NAME_TEMPLATE, JSON.stringify(templates));
|
|
_setTemplates(templates);
|
|
};
|
|
const setTemplateAPIs = (templateAPIs: TemplateAPI[]) => {
|
|
localStorage.setItem(
|
|
STORAGE_NAME_TEMPLATE_API,
|
|
JSON.stringify(templateAPIs)
|
|
);
|
|
_setTemplateAPIs(templateAPIs);
|
|
};
|
|
const setTemplateAPIsWhisper = (templateAPIWhisper: TemplateAPI[]) => {
|
|
localStorage.setItem(
|
|
STORAGE_NAME_TEMPLATE_API_WHISPER,
|
|
JSON.stringify(templateAPIWhisper)
|
|
);
|
|
_setTemplateAPIsWhisper(templateAPIWhisper);
|
|
};
|
|
const setTemplateAPIsTTS = (templateAPITTS: TemplateAPI[]) => {
|
|
localStorage.setItem(
|
|
STORAGE_NAME_TEMPLATE_API_TTS,
|
|
JSON.stringify(templateAPITTS)
|
|
);
|
|
_setTemplateAPIsTTS(templateAPITTS);
|
|
};
|
|
const setTemplateAPIsImageGen = (templateAPIImageGen: TemplateAPI[]) => {
|
|
localStorage.setItem(
|
|
STORAGE_NAME_TEMPLATE_API_IMAGE_GEN,
|
|
JSON.stringify(templateAPIImageGen)
|
|
);
|
|
_setTemplateAPIsImageGen(templateAPIImageGen);
|
|
};
|
|
const setTemplateTools = (templateTools: TemplateTools[]) => {
|
|
localStorage.setItem(
|
|
STORAGE_NAME_TEMPLATE_TOOLS,
|
|
JSON.stringify(templateTools)
|
|
);
|
|
_setToolsTemplates(templateTools);
|
|
};
|
|
const userInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-col p-2 gap-2 w-full">
|
|
<div className="flex items-center gap-2 justify-between">
|
|
{true && (
|
|
<Settings
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
setShow={setShowSettings}
|
|
selectedChatStoreIndex={props.selectedChatIndex}
|
|
templates={templates}
|
|
setTemplates={setTemplates}
|
|
templateAPIs={templateAPIs}
|
|
setTemplateAPIs={setTemplateAPIs}
|
|
templateAPIsWhisper={templateAPIsWhisper}
|
|
setTemplateAPIsWhisper={setTemplateAPIsWhisper}
|
|
templateAPIsTTS={templateAPIsTTS}
|
|
setTemplateAPIsTTS={setTemplateAPIsTTS}
|
|
templateAPIsImageGen={templateAPIsImageGen}
|
|
setTemplateAPIsImageGen={setTemplateAPIsImageGen}
|
|
templateTools={toolsTemplates}
|
|
setTemplateTools={setTemplateTools}
|
|
/>
|
|
)}
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => setShowSearch(true)}
|
|
>
|
|
<SearchIcon />
|
|
</Button>
|
|
</div>
|
|
{showSearch && (
|
|
<Search
|
|
setSelectedChatIndex={props.setSelectedChatIndex}
|
|
db={props.db}
|
|
chatStore={chatStore}
|
|
show={showSearch}
|
|
setShow={setShowSearch}
|
|
/>
|
|
)}
|
|
|
|
{!chatStore.apiKey && (
|
|
<Alert>
|
|
<KeyIcon className="h-4 w-4" />
|
|
<AlertTitle>Heads up!</AlertTitle>
|
|
<AlertDescription>
|
|
{Tr("Please click above to set")} (OpenAI) API KEY
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
{!chatStore.apiEndpoint && (
|
|
<Alert>
|
|
<GlobeIcon className="h-4 w-4" />
|
|
<AlertTitle>Heads up!</AlertTitle>
|
|
<AlertDescription>
|
|
{Tr("Please click above to set")} API Endpoint
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
<NavigationMenu>
|
|
<NavigationMenuList>
|
|
{templateAPIs.length > 0 && (
|
|
<ListAPIs
|
|
label="API"
|
|
tmps={templateAPIs}
|
|
setTmps={setTemplateAPIs}
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
apiField="apiEndpoint"
|
|
keyField="apiKey"
|
|
/>
|
|
)}
|
|
{templateAPIsWhisper.length > 0 && (
|
|
<ListAPIs
|
|
label="Whisper API"
|
|
tmps={templateAPIsWhisper}
|
|
setTmps={setTemplateAPIsWhisper}
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
apiField="whisper_api"
|
|
keyField="whisper_key"
|
|
/>
|
|
)}
|
|
{templateAPIsTTS.length > 0 && (
|
|
<ListAPIs
|
|
label="TTS API"
|
|
tmps={templateAPIsTTS}
|
|
setTmps={setTemplateAPIsTTS}
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
apiField="tts_api"
|
|
keyField="tts_key"
|
|
/>
|
|
)}
|
|
{templateAPIsImageGen.length > 0 && (
|
|
<ListAPIs
|
|
label="Image Gen API"
|
|
tmps={templateAPIsImageGen}
|
|
setTmps={setTemplateAPIsImageGen}
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
apiField="image_gen_api"
|
|
keyField="image_gen_key"
|
|
/>
|
|
)}
|
|
{toolsTemplates.length > 0 && (
|
|
<ListToolsTempaltes
|
|
templateTools={toolsTemplates}
|
|
setTemplateTools={setTemplateTools}
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
/>
|
|
)}
|
|
</NavigationMenuList>
|
|
</NavigationMenu>
|
|
</div>
|
|
<div className="grow flex flex-col p-2 w-full">
|
|
<ChatMessageList>
|
|
{chatStore.history.filter((msg) => !msg.example).length == 0 && (
|
|
<div className="bg-base-200 break-all p-3 my-3 text-left">
|
|
<h2>
|
|
<span>{Tr("Saved prompt templates")}</span>
|
|
<Button
|
|
variant="link"
|
|
className="mx-2"
|
|
onClick={() => {
|
|
chatStore.systemMessageContent = "";
|
|
chatStore.toolsString = "";
|
|
chatStore.history = [];
|
|
setChatStore({ ...chatStore });
|
|
}}
|
|
>
|
|
{Tr("Reset Current")}
|
|
</Button>
|
|
</h2>
|
|
<div className="divider"></div>
|
|
<div className="flex flex-wrap">
|
|
<Templates
|
|
templates={templates}
|
|
setTemplates={setTemplates}
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{chatStore.history.length === 0 && (
|
|
<Alert variant="default" className="my-3">
|
|
<InfoIcon className="h-4 w-4" />
|
|
<AlertTitle>{Tr("No chat history here")}</AlertTitle>
|
|
<AlertDescription className="flex flex-col gap-1 mt-5">
|
|
<div className="flex items-center gap-2">
|
|
<Settings2Icon className="h-4 w-4" />
|
|
<span>
|
|
{Tr("Model")}: {chatStore.model}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<ArrowUpIcon className="h-4 w-4" />
|
|
<span>
|
|
{Tr("Click above to change the settings of this chat")}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<CornerLeftUpIcon className="h-4 w-4" />
|
|
<span>{Tr("Click the corner to create a new chat")}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<AlertTriangleIcon className="h-4 w-4" />
|
|
<span>
|
|
{Tr(
|
|
"All chat history and settings are stored in the local browser"
|
|
)}
|
|
</span>
|
|
</div>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
{chatStore.systemMessageContent.trim() && (
|
|
<ChatBubble variant="received">
|
|
<ChatBubbleMessage>
|
|
<div className="flex flex-col gap-1">
|
|
<div className="text-sm font-bold">System Prompt</div>
|
|
<div
|
|
className="cursor-pointer"
|
|
onClick={() => setShowSettings(true)}
|
|
>
|
|
{chatStore.systemMessageContent}
|
|
</div>
|
|
</div>
|
|
</ChatBubbleMessage>
|
|
<ChatBubbleActionWrapper>
|
|
<ChatBubbleAction
|
|
className="size-7"
|
|
icon={<Settings2Icon className="size-4" />}
|
|
onClick={() => setShowSettings(true)}
|
|
/>
|
|
</ChatBubbleActionWrapper>
|
|
</ChatBubble>
|
|
)}
|
|
{chatStore.history.map((_, messageIndex) => (
|
|
<Message
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
messageIndex={messageIndex}
|
|
/>
|
|
))}
|
|
{showGenerating && (
|
|
<ChatBubble variant="received">
|
|
<ChatBubbleMessage isLoading>
|
|
{generatingMessage}
|
|
</ChatBubbleMessage>
|
|
</ChatBubble>
|
|
)}
|
|
<p className="text-center">
|
|
{chatStore.history.length > 0 && (
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
className="m-2"
|
|
disabled={showGenerating}
|
|
onClick={async () => {
|
|
const messageIndex = chatStore.history.length - 1;
|
|
if (chatStore.history[messageIndex].role === "assistant") {
|
|
chatStore.history[messageIndex].hide = true;
|
|
}
|
|
|
|
setChatStore({ ...chatStore });
|
|
await complete();
|
|
}}
|
|
>
|
|
{Tr("Re-Generate")}
|
|
</Button>
|
|
)}
|
|
{chatStore.develop_mode && chatStore.history.length > 0 && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={showGenerating}
|
|
onClick={async () => {
|
|
await complete();
|
|
}}
|
|
>
|
|
{Tr("Completion")}
|
|
</Button>
|
|
)}
|
|
</p>
|
|
<p className="p-2 my-2 text-center opacity-50 dark:text-white">
|
|
{chatStore.postBeginIndex !== 0 && (
|
|
<Alert variant="default">
|
|
<InfoIcon className="h-4 w-4" />
|
|
<AlertTitle>{Tr("Chat History Notice")}</AlertTitle>
|
|
<AlertDescription>
|
|
{Tr("Info: chat history is too long, forget messages")}:{" "}
|
|
{chatStore.postBeginIndex}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</p>
|
|
<VersionHint chatStore={chatStore} />
|
|
{showRetry && (
|
|
<p className="text-right p-2 my-2 dark:text-white">
|
|
<Button
|
|
variant="destructive"
|
|
onClick={async () => {
|
|
setShowRetry(false);
|
|
await complete();
|
|
}}
|
|
>
|
|
{Tr("Retry")}
|
|
</Button>
|
|
</p>
|
|
)}
|
|
<div ref={messagesEndRef as any}></div>
|
|
</ChatMessageList>
|
|
{images.length > 0 && (
|
|
<div className="flex flex-wrap">
|
|
{images.map((image, index) => (
|
|
<div className="flex flex-col">
|
|
{image.type === "image_url" && (
|
|
<img
|
|
className="rounded m-1 p-1 border-2 border-gray-400 max-h-32 max-w-xs"
|
|
src={image.image_url?.url}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{generatingMessage && (
|
|
<div className="flex items-center justify-end gap-2 p-2 m-2 rounded bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
Follow
|
|
</label>
|
|
<Switch
|
|
checked={follow}
|
|
onCheckedChange={setFollow}
|
|
aria-label="Toggle auto-scroll"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="sticky top-0 z-10 bg-background">
|
|
<form className="relative rounded-lg border bg-background focus-within:ring-1 focus-within:ring-ring p-1">
|
|
<ChatInput
|
|
value={inputMsg}
|
|
ref={userInputRef as any}
|
|
placeholder="Type your message here..."
|
|
onChange={(event: any) => {
|
|
setInputMsg(event.target.value);
|
|
autoHeight(event.target);
|
|
}}
|
|
onKeyPress={(event: any) => {
|
|
if (event.ctrlKey && event.code === "Enter") {
|
|
send(event.target.value, true);
|
|
setInputMsg("");
|
|
event.target.value = "";
|
|
autoHeight(event.target);
|
|
return;
|
|
}
|
|
autoHeight(event.target);
|
|
setInputMsg(event.target.value);
|
|
}}
|
|
className="min-h-12 resize-none rounded-lg bg-background border-0 p-3 shadow-none focus-visible:ring-0"
|
|
/>
|
|
<div className="flex items-center p-3 pt-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
type="button"
|
|
onClick={() => setShowAddImage(true)}
|
|
disabled={showGenerating}
|
|
>
|
|
<ImageIcon className="size-4" />
|
|
<span className="sr-only">Add Image</span>
|
|
</Button>
|
|
|
|
{chatStore.whisper_api && chatStore.whisper_key && (
|
|
<>
|
|
<WhisperButton
|
|
chatStore={chatStore}
|
|
inputMsg={inputMsg}
|
|
setInputMsg={setInputMsg}
|
|
/>
|
|
<span className="sr-only">Use Microphone</span>
|
|
</>
|
|
)}
|
|
|
|
<Button
|
|
size="sm"
|
|
className="ml-auto gap-1.5"
|
|
disabled={showGenerating}
|
|
onClick={() => {
|
|
send(inputMsg, true);
|
|
if (userInputRef.current === null) return;
|
|
userInputRef.current.value = "";
|
|
autoHeight(userInputRef.current);
|
|
}}
|
|
>
|
|
Send Message
|
|
<CornerDownLeftIcon className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
|
|
<AddImage
|
|
chatStore={chatStore}
|
|
setChatStore={setChatStore}
|
|
setShowAddImage={setShowAddImage}
|
|
images={images}
|
|
showAddImage={showAddImage}
|
|
setImages={setImages}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|