Skip to content
This repository has been archived by the owner on Jan 5, 2025. It is now read-only.

Commit

Permalink
Merge pull request #221 from openchatai/falta/widget-enhancments-2
Browse files Browse the repository at this point in the history
Falta/widget enhancments 2
  • Loading branch information
faltawy authored Nov 5, 2023
2 parents e55ec1f + 29e0172 commit d339c14
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 59 deletions.
6 changes: 3 additions & 3 deletions copilot-widget/lib/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export default function ChatHeader() {
<RxCross2 size={19} />
</DialogTrigger>
<DialogContent>
<DialogHeader>Are you sure?</DialogHeader>
<div className="opencopilot-space-y-1">
<DialogHeader className="opencopilot-mx-auto">Are you sure?</DialogHeader>
<div className="opencopilot-space-y-1.5">
<DialogClose
onClick={() => {
// Close the widget after 500ms, IDK why but it solves the focus trap issue
Expand All @@ -38,7 +38,7 @@ export default function ChatHeader() {
>
<span>Exit</span>
</DialogClose>
<DialogClose className="opencopilot-block opencopilot-w-full opencopilot-px-2 opencopilot-shadow opencopilot-py-1 opencopilot-border opencopilot-border-accent2 opencopilot-rounded-md opencopilot-text-black">
<DialogClose className="opencopilot-block opencopilot-w-full opencopilot-px-2 opencopilot-py-1 opencopilot-rounded-md opencopilot-text-black">
<span>Cancel</span>
</DialogClose>
</div>
Expand Down
15 changes: 9 additions & 6 deletions copilot-widget/lib/components/ChatInputFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import TextareaAutosize from 'react-textarea-autosize';
import TextareaAutosize from "react-textarea-autosize";
import { VscSend } from "react-icons/vsc";
import { CgRedo } from "react-icons/cg";
import { useChat } from "../contexts/Controller";
import { useRef, useState } from "react";
import { useInitialData } from "../contexts/InitialDataContext";
import { TbBulb } from "react-icons/tb";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ToolTip";
import { getId, isEmpty } from "@lib/utils/utils";
function MessageSuggestions() {
const idata = useInitialData();
const { data } = useInitialData();
const { messages, sendMessage } = useChat();

return (
<>
{messages.length === 0 && idata?.data?.inital_questions && (
{isEmpty(messages) && !isEmpty(data?.inital_questions) && (
<div className="opencopilot-flex opencopilot-items-center opencopilot-gap-4 opencopilot-justify-between opencopilot-w-full opencopilot-px-4">
<div className="opencopilot-flex opencopilot-items-center opencopilot-flex-wrap opencopilot-gap-2 opencopilot-flex-1">
{idata.data?.inital_questions.map((q, index) => (
{data?.inital_questions.map((q, index) => (
<button
className="opencopilot-text-xs opencopilot-w-fit opencopilot-font-semibold opencopilot-whitespace-nowrap opencopilot-px-2.5 opencopilot-py-1 opencopilot-rounded-full opencopilot-bg-accent opencopilot-text-primary"
key={index}
onClick={() => {
sendMessage({
from: "user",
content: q,
id: getId(),
});
}}
>
Expand Down Expand Up @@ -68,6 +70,7 @@ function ChatInputFooter() {
sendMessage({
from: "user",
content: input,
id: getId(),
});
}
}
Expand All @@ -76,7 +79,7 @@ function ChatInputFooter() {
<div className="opencopilot-overflow-y-auto opencopilot-w-full ">
<MessageSuggestions />
</div>
<div className="opencopilot-w-full opencopilot-flex opencopilot-items-center opencopilot-transition-all opencopilot-justify-between focus-within:opencopilot-ring-1 focus-within:opencopilot-ring-primary opencopilot-gap-2 opencopilot-bg-accent opencopilot-p-2 opencopilot-rounded-2xl">
<div className="opencopilot-w-full opencopilot-flex opencopilot-items-center opencopilot-ring-[#334155]/60 opencopilot-transition-colors opencopilot-justify-between opencopilot-ring-1 opencopilot-overflow-hidden focus-within:opencopilot-ring-primary opencopilot-gap-2 opencopilot-bg-accent opencopilot-p-2 opencopilot-rounded-2xl">
<div className="opencopilot-flex-1">
<TextareaAutosize
dir="auto"
Expand All @@ -93,7 +96,7 @@ function ChatInputFooter() {
rows={1}
value={input}
onChange={handleTextareaChange}
className="opencopilot-w-full opencopilot-resize-none opencopilot-bg-transparent focus-visible:opencopilot-outline-none opencopilot-border-none focus:opencopilot-outline-none focus:opencopilot-border-none opencopilot-max-h-[200px] opencopilot-scrollbar-thin opencopilot-leading-tight opencopilot-whitespace-pre-wrap opencopilot-py-1.5 opencopilot-px-4 placeholder:opencopilot-align-middle opencopilot-overflow-auto opencopilot-outline-none opencopilot-text-accent2 opencopilot-text-[14px] placeholder:opencopilot-text-xs opencopilot-font-normal"
className="opencopilot-w-full opencopilot-resize-none opencopilot-bg-transparent focus-visible:opencopilot-outline-none opencopilot-border-none focus:opencopilot-outline-none focus:opencopilot-border-none opencopilot-scrollbar-thin opencopilot-leading-tight opencopilot-whitespace-pre-wrap opencopilot-py-1.5 opencopilot-px-4 placeholder:opencopilot-align-middle opencopilot-overflow-auto opencopilot-outline-none opencopilot-text-accent2 opencopilot-text-[14px] placeholder:opencopilot-text-xs opencopilot-font-normal"
/>
</div>
<div className="opencopilot-flex opencopilot-items-center opencopilot-justify-center opencopilot-gap-2 opencopilot-h-fit opencopilot-px-2 opencopilot-text-lg">
Expand Down
48 changes: 34 additions & 14 deletions copilot-widget/lib/components/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { FaRegUserCircle } from "react-icons/fa";
import { format } from "timeago.js";
import cn from "../utils/cn";
import formatTimeFromTimestamp from "../utils/formatTime";
import { useCopyToClipboard } from "@lib/hooks/useCopy";
import { HiOutlineClipboard, HiOutlineClipboardCheck } from "react-icons/hi";
import { FailedMessage, useChat } from "@lib/contexts/Controller";
import { getLast } from "@lib/utils/utils";
function BotIcon({ error }: { error?: boolean }) {
return (
<img
Expand All @@ -33,20 +37,24 @@ function UserIcon() {
export function BotTextMessage({
message,
timestamp,
id,
}: {
message: string;
timestamp?: number;
id?: string;
}) {
const { displayText } = useTypeWriter({
text: message,
every: 0.0001,
shouldStart: true,
});

const [copied, copy] = useCopyToClipboard();
const { messages } = useChat();
const isLast = getLast(messages)?.id === id;
return (
<div className="opencopilot-p-2 opencopilot-w-full">
<div className="opencopilot-p-2 group opencopilot-w-full">
<div
className="opencopilot-flex opencopilot-items-start opencopilot-gap-3 opencopilot-w-full"
className="opencopilot-flex opencopilot-select-none opencopilot-items-start opencopilot-gap-3 opencopilot-w-full"
dir="auto"
>
<BotIcon />
Expand All @@ -63,15 +71,27 @@ export function BotTextMessage({
</div>
</div>
</div>
<div className="opencopilot-w-full opencopilot-ps-[52px]">
<div>
{timestamp && (
<span className="opencopilot-text-xs opencopilot-m-0 group-last-of-type:opencopilot-inline opencopilot-hidden">
Bot · {format(timestamp)}
</span>
)}
{isLast && (
<div className="opencopilot-w-full opencopilot-ps-10 opencopilot-flex opencopilot-items-center opencopilot-justify-between">
<div>
{timestamp && (
<span className="opencopilot-text-xs opencopilot-m-0">
Bot · {format(timestamp)}
</span>
)}
</div>
<button
className="opencopilot-text-lg opencopilot-justify-self-end"
onClick={() => copy(displayText)}
>
{copied ? (
<HiOutlineClipboardCheck className="opencopilot-text-emerald-500" />
) : (
<HiOutlineClipboard />
)}
</button>
</div>
</div>
)}
</div>
);
}
Expand Down Expand Up @@ -105,11 +125,12 @@ export function UserMessage({
}: {
content: string;
timestamp?: number;
id?: string;
}) {
return (
<div
dir="auto"
className="opencopilot-w-full last-of-type:opencopilot-mb-10 opencopilot-bg-accent opencopilot-p-2 opencopilot-flex opencopilot-gap-3 opencopilot-items-center"
className="opencopilot-w-full opencopilot-overflow-x-auto opencopilot-max-w-full last-of-type:opencopilot-mb-10 opencopilot-bg-accent opencopilot-p-2 opencopilot-flex opencopilot-gap-3 opencopilot-items-center"
>
<UserIcon />
<div>
Expand All @@ -128,12 +149,11 @@ export function UserMessage({
);
}

export function BotMessageError() {
export function BotMessageError({ message }: { message?: FailedMessage }) {
const { displayText } = useTypeWriter({
text: "Error sending the message.",
every: 0.001,
});

return (
<div className="opencopilot-clear-both opencopilot-w-full opencopilot-p-2">
<div className="opencopilot-flex opencopilot-items-center opencopilot-gap-3 opencopilot-w-full">
Expand Down
50 changes: 33 additions & 17 deletions copilot-widget/lib/contexts/Controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import { useAxiosInstance } from "./axiosInstance";
import { useConfigData } from "./ConfigData";
import { useSoundEffectes } from "../hooks/useSoundEffects";
import { Message } from "@lib/types";

import { getId } from "@lib/utils/utils";
export type FailedMessage = {
message: Message;
reason?: string;
};
interface ChatContextProps {
messages: Message[];
sendMessage: (message: Message) => void;
loading: boolean;
error: boolean;
failedMessage: FailedMessage | null;
reset: () => void;
}

Expand All @@ -19,7 +23,7 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [messages, setMessages] = useState<Message[]>([]);
const sfx = useSoundEffectes();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [failedMessage, setError] = useState<FailedMessage | null>(null);
const { axiosInstance } = useAxiosInstance();
const config = useConfigData();
const addMessage = (message: Message) => {
Expand All @@ -40,20 +44,32 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {

const sendMessage = async (message: Message) => {
// send user message
setError(false);
addMessage(message);

if (message.from === "user") {
addMessage(message);
}
setError(null);
setLoading(true);
try {
// Make the API call to send the message
// we want to set loading for the bot message to be sent.
setLoading(true);
const response = await axiosInstance.post<
Extract<Message, { from: "bot" }>
>("/chat/send", { ...message, headers: config?.headers });
addMessage({ ...response.data, from: "bot" });
} catch (error) {
console.error("Error sending the message:");
setError(true);
const { data, status, statusText } = await axiosInstance.post(
"/chat/send",
{
...message,
headers: config?.headers,
}
);
if (status === 200) {
addMessage({ ...data, id: getId(), from: "bot" });
} else {
setError({
message,
reason: statusText,
});
}
} catch (error: any) {
setError({
message,
reason: error?.message,
});
} finally {
setLoading(false);
}
Expand All @@ -66,7 +82,7 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
messages,
sendMessage,
loading,
error,
failedMessage,
reset,
};

Expand Down
8 changes: 4 additions & 4 deletions copilot-widget/lib/contexts/InitialDataContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ type InitialDataContextType = {
refetch: () => void;
};

const InitialDataContext = createContext<InitialDataContextType | undefined>(
undefined
const InitialDataContext = createContext<InitialDataContextType>(
{} as InitialDataContextType
);

export function InitialDataProvider({ children }: { children: ReactNode }) {
Expand All @@ -33,7 +33,7 @@ export function InitialDataProvider({ children }: { children: ReactNode }) {

useEffect(() => {
loadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
Expand All @@ -49,7 +49,7 @@ export function InitialDataProvider({ children }: { children: ReactNode }) {
);
}

export const useInitialData = (): InitialDataContextType | undefined => {
export const useInitialData = (): InitialDataContextType => {
const context = useContext(InitialDataContext);
if (!context) {
console.warn("Error loading initial data....");
Expand Down
36 changes: 36 additions & 0 deletions copilot-widget/lib/hooks/useCopy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState, useEffect } from "react";

type CopyFn = (text: any) => Promise<boolean>;

export function useCopyToClipboard(): [boolean, CopyFn] {
const [copied, setCopied] = useState(false);
const copy: CopyFn = async (text) => {
if (!navigator?.clipboard) {
console.warn("Clipboard not supported");
return false;
}

// Try to save to clipboard then update the state if it worked
try {
await navigator.clipboard.writeText(text);
setCopied(true);
return true;
} catch (error) {
console.warn("Copy failed", error);
setCopied(false);
return false;
}
};

useEffect(() => {
if (copied) {
const timeout = setTimeout(() => {
setCopied(false);
}, 5000);

return () => clearTimeout(timeout);
}
}, [copied]);

return [copied, copy];
}
8 changes: 5 additions & 3 deletions copilot-widget/lib/screens/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Map } from "../utils/Map";
function ChatScreen() {
const scrollElementRef = useRef(null);
const [setPos] = useScrollToPercentage(scrollElementRef);
const { messages, loading, error } = useChat();
const { messages, loading, failedMessage } = useChat();
const config = useConfigData();
const initialMessage = config?.initialMessage;
useEffect(() => {
Expand All @@ -32,7 +32,7 @@ function ChatScreen() {
<ChatHeader />
<main
ref={scrollElementRef}
className="opencopilot-flex-1 opencopilot-w-full opencopilot-shrink-0 opencopilot-overflow-auto opencopilot-scrollbar-thin opencopilot-scroll-smooth"
className="opencopilot-flex-1 opencopilot-w-full opencopilot-overflow-x-hidden opencopilot-shrink-0 opencopilot-overflow-auto opencopilot-scrollbar-thin opencopilot-scroll-smooth"
>
<div className="opencopilot-flex opencopilot-h-fit opencopilot-mt-auto opencopilot-flex-col opencopilot-py-2 opencopilot-max-h-full opencopilot-items-center opencopilot-gap-1 last:fade-in-right">
{
Expand All @@ -48,6 +48,7 @@ function ChatScreen() {
return (
<BotTextMessage
timestamp={message.timestamp}
id={message.id}
key={index}
message={message.response.text}
/>
Expand All @@ -56,6 +57,7 @@ function ChatScreen() {
return (
<UserMessage
key={index}
id={message.id}
timestamp={message.timestamp}
content={message.content}
/>
Expand All @@ -64,7 +66,7 @@ function ChatScreen() {
}}
/>
{loading && <BotMessageLoading />}
{error && <BotMessageError />}
{failedMessage && <BotMessageError message={failedMessage} />}
</div>
</main>
<ChatInputFooter />
Expand Down
Loading

0 comments on commit d339c14

Please sign in to comment.