Skip to content

Commit

Permalink
Merge pull request #578 from openchatai/ui/sessions-can-review-the-re…
Browse files Browse the repository at this point in the history
…sponse

UI/Let users up&down vote the copilot response
  • Loading branch information
faltawy authored Jan 24, 2024
2 parents 9fcfbc9 + 3df25e4 commit b3dc7ae
Show file tree
Hide file tree
Showing 15 changed files with 520 additions and 141 deletions.
18 changes: 11 additions & 7 deletions copilot-widget/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,20 @@ <h2>
</style>
<script type="module" src="/src/main.tsx"></script>
<div id="opencopilot-root"></div>

<script>
const token = "KIxFQavTJYV8aCXa";
const apiUrl = "http://localhost:8888/backend";
const socketUrl = "http://localhost:8888";
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
initAiCoPilot({
initialMessage: "Hi Sir", // optional
token: "FsQJM4nPZAAEKgqt", // required
token: token, // required
triggerSelector: "#triggerSelector", // optional
rootId: "opencopilot-root", // optional otherwise it will create a div with id opencopilot-root
apiUrl: "https://cloud.opencopilot.so/backend", // required
socketUrl: "https://cloud.opencopilot.so",
apiUrl: apiUrl, // required
socketUrl: socketUrl, // required
defaultOpen: true, // optional
containerProps: {}, // optional
headers: {
Expand All @@ -91,11 +95,11 @@ <h2>
document.addEventListener("DOMContentLoaded", () => {
initAiCoPilot({
initialMessage: "Hi Sir", // optional
token: "TGtuQAhEtmQkGKqJ", // required
token: token, // required
triggerSelector: "#triggerSelector", // optional
rootId: "opencopilot-root-2", // optional otherwise it will create a div with id opencopilot-root
apiUrl: "https://cloud.opencopilot.so/backend", // required
socketUrl: "https://cloud.opencopilot.so",
apiUrl: apiUrl, // required
socketUrl: socketUrl,
defaultOpen: true, // optional
containerProps: {
// optional
Expand Down
27 changes: 18 additions & 9 deletions copilot-widget/lib/components/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FailedMessage, useChat } from "@lib/contexts/Controller";
import { getLast, isEmpty } from "@lib/utils/utils";
import { useConfigData } from "@lib/contexts/ConfigData";
import useTypeWriter from "@lib/hooks/useTypeWriter";
import { Vote } from "./Vote";

function BotIcon({ error }: { error?: boolean }) {
return (
Expand Down Expand Up @@ -41,7 +42,16 @@ function UserIcon() {
</Tooltip>
);
}
function useVote() {
const {
setLastMessageId,
lastMessageToVote
} = useChat();

return {

}
}
export function BotTextMessage({
message,
timestamp,
Expand All @@ -51,7 +61,7 @@ export function BotTextMessage({
timestamp?: number | Date;
id?: string | number;
}) {
const { messages } = useChat();
const { messages, lastMessageToVote } = useChat();
const isLast = getLast(messages)?.id === id;
if (isEmpty(message)) return null;
return (
Expand All @@ -75,14 +85,13 @@ export function BotTextMessage({
</div>
</div>
{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>
<div className="opencopilot-w-full opencopilot-ps-10 opencopilot-flex-nowrap opencopilot-flex opencopilot-items-center opencopilot-justify-between">
<span className="opencopilot-text-xs opencopilot-m-0">
Bot
</span>
{
lastMessageToVote && isLast && <Vote messageId={lastMessageToVote} />
}
</div>
)}
</div>
Expand Down
29 changes: 29 additions & 0 deletions copilot-widget/lib/components/Vote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useDownvote, useUpvote } from '@lib/hooks/useVote';
import cn from '@lib/utils/cn';
import {
ThumbsUp,
ThumbsDown,
} from 'lucide-react';

const SIZE = 26;

export function Vote({ messageId }: { messageId: number }) {
const [asyncUpvoteState, asyncUpvote] = useUpvote(String(messageId));
const [asyncDownvoteState, asyncDownvote] = useDownvote(String(messageId));
const isUpvoted = !!asyncUpvoteState.value?.data.message;
const isDownvoted = !!asyncDownvoteState.value?.data.message;
const userVoted = isUpvoted || isDownvoted;
return (
<div className='opencopilot-flex opencopilot-items-center opencopilot-justify-end opencopilot-w-full opencopilot-gap-px [&>button]:opencopilot-p-1'>
{
userVoted ? <span className='opencopilot-text-xs text-blur-out opencopilot-text-emerald-500'>thank you</span> : <><button onClick={asyncUpvote} className={cn('opencopilot-transition-all opencopilot-rounded-lg', isUpvoted ? '*:opencopilot-fill-emerald-500' : 'active:*:opencopilot-scale-105')}>
<ThumbsUp size={SIZE} className='opencopilot-transition-all opencopilot-stroke-emerald-500' />
</button>
<button onClick={asyncDownvote} className={cn('opencopilot-transition-all opencopilot-rounded-lg', isDownvoted ? '*:opencopilot-fill-rose-500' : 'active:*:opencopilot-scale-105')}>
<ThumbsDown size={SIZE} className='opencopilot-transition-all opencopilot-stroke-rose-500' />
</button></>
}

</div>
)
}
22 changes: 20 additions & 2 deletions copilot-widget/lib/contexts/Controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface ChatContextData {
loading: boolean;
failedMessage: FailedMessage | null;
reset: () => void;
setLastMessageId: (id: number | null) => void;
lastMessageToVote: number | null;
}
const [
useChat,
Expand All @@ -37,6 +39,10 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const config = useConfigData();
const { sessionId } = useSessionId(config.token);
const [conversationInfo, setConversationInfo] = useState<string | null>(null);
const [lastMessageToVote, setLastMessageToVote] = useState<number | null>(null);
const setLastMessageId = useCallback((id: number | null) => {
setLastMessageToVote(id)
}, [])
useEffect(() => {
getInitialData(axiosInstance).then((data) => {
setMessages(historyToMessages(data.history))
Expand Down Expand Up @@ -94,7 +100,6 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {

const updateBotMessage = useCallback((id: string, text: string) => {
const botMessage = messages.find(m => m.id === id) as BotResponse
console.log({ botMessage })
if (botMessage) {
// append the text to the bot message
const textt = botMessage.response.text + text
Expand Down Expand Up @@ -133,7 +138,18 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
}

}, [currentMessagePair, sessionId, socket, updateBotMessage]);

useEffect(() => {
socket.on(`${sessionId}_vote`, (content) => {
console.log(`${sessionId}_vote ==>`, content)
if (content) {
setLastMessageToVote(content)
}
});
return () => {
socket.off(`${sessionId}_vote`);
setLastMessageToVote(null)
};
}, [sessionId, socket])
function reset() {
setMessages([]);
}
Expand All @@ -145,6 +161,8 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
loading,
failedMessage,
reset,
setLastMessageId,
lastMessageToVote
};

return (
Expand Down
1 change: 0 additions & 1 deletion copilot-widget/lib/contexts/InitialDataContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ function InitialDataProvider({ children }: { children: ReactNode }) {
const { axiosInstance } = useAxiosInstance();
const [data, setData] = useState<InitialDataType | undefined>();
const [loading, setLoading] = useState<boolean>(true);
console.log(data)
function loadData() {
setLoading(true);
getInitialData(axiosInstance)
Expand Down
71 changes: 71 additions & 0 deletions copilot-widget/lib/hooks/useAsyncFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// https://github.com/streamich/react-use/blob/master/src/useAsyncFn.ts
import { DependencyList, useCallback, useRef, useState } from "react";
import useMountedState from "./useMountedState";
import { FunctionReturningPromise, PromiseType } from "@lib/types/helpers";

export type AsyncState<T> =
| {
loading: boolean;
error?: undefined;
value?: undefined;
}
| {
loading: true;
error?: Error | undefined;
value?: T;
}
| {
loading: false;
error: Error;
value?: undefined;
}
| {
loading: false;
error?: undefined;
value: T;
};

type StateFromFunctionReturningPromise<T extends FunctionReturningPromise> =
AsyncState<PromiseType<ReturnType<T>>>;

export type AsyncFnReturn<
T extends FunctionReturningPromise = FunctionReturningPromise
> = [StateFromFunctionReturningPromise<T>, T];

export default function useAsyncFn<T extends FunctionReturningPromise>(
fn: T,
deps: DependencyList = [],
initialState: StateFromFunctionReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
const lastCallId = useRef(0);
const isMounted = useMountedState();
const [state, set] =
useState<StateFromFunctionReturningPromise<T>>(initialState);

const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
const callId = ++lastCallId.current;

if (!state.loading) {
set((prevState) => ({ ...prevState, loading: true }));
}

return fn(...args).then(
(value) => {
isMounted() &&
callId === lastCallId.current &&
set({ value, loading: false });

return value;
},
(error) => {
isMounted() &&
callId === lastCallId.current &&
set({ error, loading: false });

return error;
}
) as ReturnType<T>;
}, deps);

return [state, callback as unknown as T];
}
16 changes: 16 additions & 0 deletions copilot-widget/lib/hooks/useMountedState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useCallback, useEffect, useRef } from "react";

export default function useMountedState(): () => boolean {
const mountedRef = useRef<boolean>(false);
const get = useCallback(() => mountedRef.current, []);

useEffect(() => {
mountedRef.current = true;

return () => {
mountedRef.current = false;
};
}, []);

return get;
}
26 changes: 26 additions & 0 deletions copilot-widget/lib/hooks/useVote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useAxiosInstance } from "@lib/contexts/axiosInstance";
import useAsyncFn from "./useAsyncFn";

function useUpvote(id: string, onSuccess?: () => void) {
const axios = useAxiosInstance();
return useAsyncFn(
async () =>
axios.axiosInstance.post<{
message: string;
}>(`/chat/vote/${id}`),
[axios, id, onSuccess]
);
}

function useDownvote(id: string, onSuccess?: () => void) {
const axios = useAxiosInstance();
return useAsyncFn(
async () =>
axios.axiosInstance.delete<{
message: string;
}>(`/chat/vote/${id}`),
[axios, id, onSuccess]
);
}

export { useUpvote, useDownvote };
5 changes: 5 additions & 0 deletions copilot-widget/lib/types/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type PromiseType<P extends Promise<any>> = P extends Promise<infer T>
? T
: never;

export type FunctionReturningPromise = (...args: any[]) => Promise<any>;
8 changes: 4 additions & 4 deletions copilot-widget/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@openchatai/copilot-widget",
"private": false,
"version": "2.2.2",
"version": "2.3.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -28,12 +28,12 @@
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"autoprefixer": "^10.4.17",
"axios": "^1.6.0",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"postcss": "^8.4.31",
"postcss": "^8.4.33",
"prettier": "^2.8.8",
"react": "^18.x",
"react-dom": "^18.x",
Expand All @@ -46,7 +46,7 @@
"socket.io-client": "^4.7.2",
"tailwind-merge": "^1.13.2",
"tailwind-scrollbar": "^3.0.4",
"tailwindcss": "^3.3.3",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.6",
"timeago.js": "^4.0.2",
"typescript": "^5.0.2",
Expand Down
Loading

0 comments on commit b3dc7ae

Please sign in to comment.