Merge branch 'main' of https://github.com/mckaywrigley/chatbot-ui-pro into mac
This commit is contained in:
@@ -34,3 +34,4 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
.idea
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Conversation, KeyValuePair, Message, OpenAIModel } from "@/types";
|
import { Conversation, KeyValuePair, Message, OpenAIModel } from "@/types";
|
||||||
import { FC, useEffect, useRef, useState } from "react";
|
import { FC, MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
import { ChatInput } from "./ChatInput";
|
import { ChatInput } from "./ChatInput";
|
||||||
import { ChatLoader } from "./ChatLoader";
|
import { ChatLoader } from "./ChatLoader";
|
||||||
import { ChatMessage } from "./ChatMessage";
|
import { ChatMessage } from "./ChatMessage";
|
||||||
@@ -17,9 +17,10 @@ interface Props {
|
|||||||
lightMode: "light" | "dark";
|
lightMode: "light" | "dark";
|
||||||
onSend: (message: Message, isResend: boolean) => void;
|
onSend: (message: Message, isResend: boolean) => void;
|
||||||
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||||
|
stopConversationRef: MutableRefObject<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Chat: FC<Props> = ({ conversation, models, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation }) => {
|
export const Chat: FC<Props> = ({ conversation, models, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, stopConversationRef }) => {
|
||||||
const [currentMessage, setCurrentMessage] = useState<Message>();
|
const [currentMessage, setCurrentMessage] = useState<Message>();
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -33,7 +34,7 @@ export const Chat: FC<Props> = ({ conversation, models, messageIsStreaming, mode
|
|||||||
}, [conversation.messages]);
|
}, [conversation.messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1 overflow-none dark:bg-[#343541]">
|
<div className="relative flex-1 overflow-none dark:bg-[#343541] bg-white">
|
||||||
{modelError ? (
|
{modelError ? (
|
||||||
<div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6">
|
<div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6">
|
||||||
<div className="text-center text-red-500">Error fetching models.</div>
|
<div className="text-center text-red-500">Error fetching models.</div>
|
||||||
@@ -96,6 +97,7 @@ export const Chat: FC<Props> = ({ conversation, models, messageIsStreaming, mode
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ChatInput
|
<ChatInput
|
||||||
|
stopConversationRef={stopConversationRef}
|
||||||
messageIsStreaming={messageIsStreaming}
|
messageIsStreaming={messageIsStreaming}
|
||||||
onSend={(message) => {
|
onSend={(message) => {
|
||||||
setCurrentMessage(message);
|
setCurrentMessage(message);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Message, OpenAIModel, OpenAIModelID } from "@/types";
|
import { Message, OpenAIModel, OpenAIModelID } from "@/types";
|
||||||
import { IconSend } from "@tabler/icons-react";
|
import { IconHandStop, IconSend } from "@tabler/icons-react";
|
||||||
import { FC, KeyboardEvent, useEffect, useRef, useState } from "react";
|
import { FC, KeyboardEvent, MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messageIsStreaming: boolean;
|
messageIsStreaming: boolean;
|
||||||
onSend: (message: Message) => void;
|
onSend: (message: Message) => void;
|
||||||
model: OpenAIModel;
|
model: OpenAIModel;
|
||||||
|
stopConversationRef: MutableRefObject<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming, model }) => {
|
export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming, model, stopConversationRef }) => {
|
||||||
const [content, setContent] = useState<string>();
|
const [content, setContent] = useState<string>();
|
||||||
const [isTyping, setIsTyping] = useState<boolean>(false);
|
const [isTyping, setIsTyping] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -67,13 +68,20 @@ export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming, model }) => {
|
|||||||
}
|
}
|
||||||
}, [content]);
|
}, [content]);
|
||||||
|
|
||||||
|
function handleStopConversation() {
|
||||||
|
stopConversationRef.current = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
stopConversationRef.current = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-0 left-0 w-full border-t md:border-t-0 dark:border-white/20 md:border-transparent md:dark:border-transparent dark:bg-[#444654] md:dark:bg-gradient-to-t from-[#343541] via-[#343541] to-[#343541]/0 bg-white md:dark:!bg-transparent dark:md:bg-vert-dark-gradient pt-2">
|
<div className="absolute bottom-0 left-0 w-full border-t md:border-t-0 dark:border-white/20 md:border-transparent md:dark:border-transparent dark:bg-[#444654] md:dark:bg-gradient-to-t from-[#343541] via-[#343541] to-[#343541]/0 bg-white md:dark:!bg-transparent dark:md:bg-vert-dark-gradient pt-2">
|
||||||
<div className="stretch mx-2 md:mt-[52px] mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-3xl">
|
<div className="stretch mx-2 md:mt-[52px] mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-3xl">
|
||||||
<div className="flex flex-col w-full py-2 flex-grow md:py-3 md:pl-4 relative border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-[#40414F] rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]">
|
<div className="flex flex-col w-full py-2 flex-grow md:py-3 md:pl-4 relative border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-[#40414F] rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className="text-base m-0 w-full resize-none outline-none border-0 bg-transparent p-0 pr-7 focus:ring-0 focus-visible:ring-0 dark:bg-transparent pl-2 md:pl-0"
|
className="text-black dark:text-white m-0 w-full resize-none outline-none border-0 bg-transparent p-0 pr-7 focus:ring-0 focus-visible:ring-0 dark:bg-transparent pl-2 md:pl-0"
|
||||||
style={{
|
style={{
|
||||||
resize: "none",
|
resize: "none",
|
||||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||||
@@ -93,11 +101,35 @@ export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming, model }) => {
|
|||||||
className="absolute right-5 focus:outline-none text-neutral-800 hover:text-neutral-900 dark:text-neutral-100 dark:hover:text-neutral-200 dark:bg-opacity-50 hover:bg-neutral-200 p-1 rounded-sm"
|
className="absolute right-5 focus:outline-none text-neutral-800 hover:text-neutral-900 dark:text-neutral-100 dark:hover:text-neutral-200 dark:bg-opacity-50 hover:bg-neutral-200 p-1 rounded-sm"
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
>
|
>
|
||||||
<IconSend size={16} className="opacity-60"/>
|
<IconSend
|
||||||
|
size={16}
|
||||||
|
className="opacity-60"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
{messageIsStreaming ? (
|
||||||
|
<button
|
||||||
|
className="absolute right-12 focus:outline-none text-neutral-800 hover:text-neutral-900 dark:text-neutral-100 dark:hover:text-neutral-200 dark:bg-opacity-50 hover:bg-neutral-200 p-1 rounded-sm"
|
||||||
|
onClick={handleStopConversation}
|
||||||
|
>
|
||||||
|
<IconHandStop
|
||||||
|
size={16}
|
||||||
|
className="opacity-60"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 pt-2 pb-3 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6"><a href="https://github.com/mckaywrigley/chatbot-ui" target="_blank" rel="noreferrer" className="underline">ChatBot UI</a>. Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.</div>
|
<div className="px-3 pt-2 pb-3 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6">
|
||||||
|
<a
|
||||||
|
href="https://github.com/mckaywrigley/chatbot-ui"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
ChatBot UI
|
||||||
|
</a>
|
||||||
|
. Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface Props {
|
|||||||
export const ChatMessage: FC<Props> = ({ message, lightMode }) => {
|
export const ChatMessage: FC<Props> = ({ message, lightMode }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group ${message.role === "assistant" ? "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-gray-50 dark:bg-[#444654]" : "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 "}`}
|
className={`group ${message.role === "assistant" ? "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-gray-50 dark:bg-[#444654]" : "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-white dark:bg-[#343541]"}`}
|
||||||
style={{ overflowWrap: "anywhere" }}
|
style={{ overflowWrap: "anywhere" }}
|
||||||
>
|
>
|
||||||
<div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0 m-auto">
|
<div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0 m-auto">
|
||||||
|
|||||||
+10
-1
@@ -8,7 +8,7 @@ import { saveConversation, saveConversations, updateConversation } from "@/utils
|
|||||||
import { exportConversations, importConversations } from "@/utils/app/data";
|
import { exportConversations, importConversations } from "@/utils/app/data";
|
||||||
import { IconArrowBarLeft, IconArrowBarRight } from "@tabler/icons-react";
|
import { IconArrowBarLeft, IconArrowBarRight } from "@tabler/icons-react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
@@ -21,6 +21,7 @@ export default function Home() {
|
|||||||
const [apiKey, setApiKey] = useState<string>("");
|
const [apiKey, setApiKey] = useState<string>("");
|
||||||
const [messageError, setMessageError] = useState<boolean>(false);
|
const [messageError, setMessageError] = useState<boolean>(false);
|
||||||
const [modelError, setModelError] = useState<boolean>(false);
|
const [modelError, setModelError] = useState<boolean>(false);
|
||||||
|
const stopConversationRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const handleSend = async (message: Message, isResend: boolean) => {
|
const handleSend = async (message: Message, isResend: boolean) => {
|
||||||
if (selectedConversation) {
|
if (selectedConversation) {
|
||||||
@@ -53,11 +54,13 @@ export default function Home() {
|
|||||||
prompt: updatedConversation.prompt
|
prompt: updatedConversation.prompt
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
const response = await fetch("/api/chat", {
|
const response = await fetch("/api/chat", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
|
signal: controller.signal,
|
||||||
body: JSON.stringify(chatBody)
|
body: JSON.stringify(chatBody)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,6 +90,11 @@ export default function Home() {
|
|||||||
let text = "";
|
let text = "";
|
||||||
|
|
||||||
while (!done) {
|
while (!done) {
|
||||||
|
if (stopConversationRef.current === true) {
|
||||||
|
controller.abort();
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
const { value, done: doneReading } = await reader.read();
|
const { value, done: doneReading } = await reader.read();
|
||||||
done = doneReading;
|
done = doneReading;
|
||||||
const chunkValue = decoder.decode(value);
|
const chunkValue = decoder.decode(value);
|
||||||
@@ -385,6 +393,7 @@ export default function Home() {
|
|||||||
lightMode={lightMode}
|
lightMode={lightMode}
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
onUpdateConversation={handleUpdateConversation}
|
onUpdateConversation={handleUpdateConversation}
|
||||||
|
stopConversationRef={stopConversationRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,3 +27,7 @@
|
|||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: #202123;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user