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

Commit b3dc7ae

Browse files
authored
Merge pull request #578 from openchatai/ui/sessions-can-review-the-response
UI/Let users up&down vote the copilot response
2 parents 9fcfbc9 + 3df25e4 commit b3dc7ae

15 files changed

+520
-141
lines changed

copilot-widget/index.html

+11-7
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,20 @@ <h2>
6262
</style>
6363
<script type="module" src="/src/main.tsx"></script>
6464
<div id="opencopilot-root"></div>
65-
65+
<script>
66+
const token = "KIxFQavTJYV8aCXa";
67+
const apiUrl = "http://localhost:8888/backend";
68+
const socketUrl = "http://localhost:8888";
69+
</script>
6670
<script>
6771
document.addEventListener("DOMContentLoaded", () => {
6872
initAiCoPilot({
6973
initialMessage: "Hi Sir", // optional
70-
token: "FsQJM4nPZAAEKgqt", // required
74+
token: token, // required
7175
triggerSelector: "#triggerSelector", // optional
7276
rootId: "opencopilot-root", // optional otherwise it will create a div with id opencopilot-root
73-
apiUrl: "https://cloud.opencopilot.so/backend", // required
74-
socketUrl: "https://cloud.opencopilot.so",
77+
apiUrl: apiUrl, // required
78+
socketUrl: socketUrl, // required
7579
defaultOpen: true, // optional
7680
containerProps: {}, // optional
7781
headers: {
@@ -91,11 +95,11 @@ <h2>
9195
document.addEventListener("DOMContentLoaded", () => {
9296
initAiCoPilot({
9397
initialMessage: "Hi Sir", // optional
94-
token: "TGtuQAhEtmQkGKqJ", // required
98+
token: token, // required
9599
triggerSelector: "#triggerSelector", // optional
96100
rootId: "opencopilot-root-2", // optional otherwise it will create a div with id opencopilot-root
97-
apiUrl: "https://cloud.opencopilot.so/backend", // required
98-
socketUrl: "https://cloud.opencopilot.so",
101+
apiUrl: apiUrl, // required
102+
socketUrl: socketUrl,
99103
defaultOpen: true, // optional
100104
containerProps: {
101105
// optional

copilot-widget/lib/components/Messages.tsx

+18-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { FailedMessage, useChat } from "@lib/contexts/Controller";
1010
import { getLast, isEmpty } from "@lib/utils/utils";
1111
import { useConfigData } from "@lib/contexts/ConfigData";
1212
import useTypeWriter from "@lib/hooks/useTypeWriter";
13+
import { Vote } from "./Vote";
1314

1415
function BotIcon({ error }: { error?: boolean }) {
1516
return (
@@ -41,7 +42,16 @@ function UserIcon() {
4142
</Tooltip>
4243
);
4344
}
45+
function useVote() {
46+
const {
47+
setLastMessageId,
48+
lastMessageToVote
49+
} = useChat();
4450

51+
return {
52+
53+
}
54+
}
4555
export function BotTextMessage({
4656
message,
4757
timestamp,
@@ -51,7 +61,7 @@ export function BotTextMessage({
5161
timestamp?: number | Date;
5262
id?: string | number;
5363
}) {
54-
const { messages } = useChat();
64+
const { messages, lastMessageToVote } = useChat();
5565
const isLast = getLast(messages)?.id === id;
5666
if (isEmpty(message)) return null;
5767
return (
@@ -75,14 +85,13 @@ export function BotTextMessage({
7585
</div>
7686
</div>
7787
{isLast && (
78-
<div className="opencopilot-w-full opencopilot-ps-10 opencopilot-flex opencopilot-items-center opencopilot-justify-between">
79-
<div>
80-
{timestamp && (
81-
<span className="opencopilot-text-xs opencopilot-m-0">
82-
Bot · {format(timestamp)}
83-
</span>
84-
)}
85-
</div>
88+
<div className="opencopilot-w-full opencopilot-ps-10 opencopilot-flex-nowrap opencopilot-flex opencopilot-items-center opencopilot-justify-between">
89+
<span className="opencopilot-text-xs opencopilot-m-0">
90+
Bot
91+
</span>
92+
{
93+
lastMessageToVote && isLast && <Vote messageId={lastMessageToVote} />
94+
}
8695
</div>
8796
)}
8897
</div>
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useDownvote, useUpvote } from '@lib/hooks/useVote';
2+
import cn from '@lib/utils/cn';
3+
import {
4+
ThumbsUp,
5+
ThumbsDown,
6+
} from 'lucide-react';
7+
8+
const SIZE = 26;
9+
10+
export function Vote({ messageId }: { messageId: number }) {
11+
const [asyncUpvoteState, asyncUpvote] = useUpvote(String(messageId));
12+
const [asyncDownvoteState, asyncDownvote] = useDownvote(String(messageId));
13+
const isUpvoted = !!asyncUpvoteState.value?.data.message;
14+
const isDownvoted = !!asyncDownvoteState.value?.data.message;
15+
const userVoted = isUpvoted || isDownvoted;
16+
return (
17+
<div className='opencopilot-flex opencopilot-items-center opencopilot-justify-end opencopilot-w-full opencopilot-gap-px [&>button]:opencopilot-p-1'>
18+
{
19+
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')}>
20+
<ThumbsUp size={SIZE} className='opencopilot-transition-all opencopilot-stroke-emerald-500' />
21+
</button>
22+
<button onClick={asyncDownvote} className={cn('opencopilot-transition-all opencopilot-rounded-lg', isDownvoted ? '*:opencopilot-fill-rose-500' : 'active:*:opencopilot-scale-105')}>
23+
<ThumbsDown size={SIZE} className='opencopilot-transition-all opencopilot-stroke-rose-500' />
24+
</button></>
25+
}
26+
27+
</div>
28+
)
29+
}

copilot-widget/lib/contexts/Controller.tsx

+20-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ interface ChatContextData {
2323
loading: boolean;
2424
failedMessage: FailedMessage | null;
2525
reset: () => void;
26+
setLastMessageId: (id: number | null) => void;
27+
lastMessageToVote: number | null;
2628
}
2729
const [
2830
useChat,
@@ -37,6 +39,10 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
3739
const config = useConfigData();
3840
const { sessionId } = useSessionId(config.token);
3941
const [conversationInfo, setConversationInfo] = useState<string | null>(null);
42+
const [lastMessageToVote, setLastMessageToVote] = useState<number | null>(null);
43+
const setLastMessageId = useCallback((id: number | null) => {
44+
setLastMessageToVote(id)
45+
}, [])
4046
useEffect(() => {
4147
getInitialData(axiosInstance).then((data) => {
4248
setMessages(historyToMessages(data.history))
@@ -94,7 +100,6 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
94100

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

135140
}, [currentMessagePair, sessionId, socket, updateBotMessage]);
136-
141+
useEffect(() => {
142+
socket.on(`${sessionId}_vote`, (content) => {
143+
console.log(`${sessionId}_vote ==>`, content)
144+
if (content) {
145+
setLastMessageToVote(content)
146+
}
147+
});
148+
return () => {
149+
socket.off(`${sessionId}_vote`);
150+
setLastMessageToVote(null)
151+
};
152+
}, [sessionId, socket])
137153
function reset() {
138154
setMessages([]);
139155
}
@@ -145,6 +161,8 @@ const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
145161
loading,
146162
failedMessage,
147163
reset,
164+
setLastMessageId,
165+
lastMessageToVote
148166
};
149167

150168
return (

copilot-widget/lib/contexts/InitialDataContext.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ function InitialDataProvider({ children }: { children: ReactNode }) {
2525
const { axiosInstance } = useAxiosInstance();
2626
const [data, setData] = useState<InitialDataType | undefined>();
2727
const [loading, setLoading] = useState<boolean>(true);
28-
console.log(data)
2928
function loadData() {
3029
setLoading(true);
3130
getInitialData(axiosInstance)
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// https://github.com/streamich/react-use/blob/master/src/useAsyncFn.ts
2+
import { DependencyList, useCallback, useRef, useState } from "react";
3+
import useMountedState from "./useMountedState";
4+
import { FunctionReturningPromise, PromiseType } from "@lib/types/helpers";
5+
6+
export type AsyncState<T> =
7+
| {
8+
loading: boolean;
9+
error?: undefined;
10+
value?: undefined;
11+
}
12+
| {
13+
loading: true;
14+
error?: Error | undefined;
15+
value?: T;
16+
}
17+
| {
18+
loading: false;
19+
error: Error;
20+
value?: undefined;
21+
}
22+
| {
23+
loading: false;
24+
error?: undefined;
25+
value: T;
26+
};
27+
28+
type StateFromFunctionReturningPromise<T extends FunctionReturningPromise> =
29+
AsyncState<PromiseType<ReturnType<T>>>;
30+
31+
export type AsyncFnReturn<
32+
T extends FunctionReturningPromise = FunctionReturningPromise
33+
> = [StateFromFunctionReturningPromise<T>, T];
34+
35+
export default function useAsyncFn<T extends FunctionReturningPromise>(
36+
fn: T,
37+
deps: DependencyList = [],
38+
initialState: StateFromFunctionReturningPromise<T> = { loading: false }
39+
): AsyncFnReturn<T> {
40+
const lastCallId = useRef(0);
41+
const isMounted = useMountedState();
42+
const [state, set] =
43+
useState<StateFromFunctionReturningPromise<T>>(initialState);
44+
45+
const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
46+
const callId = ++lastCallId.current;
47+
48+
if (!state.loading) {
49+
set((prevState) => ({ ...prevState, loading: true }));
50+
}
51+
52+
return fn(...args).then(
53+
(value) => {
54+
isMounted() &&
55+
callId === lastCallId.current &&
56+
set({ value, loading: false });
57+
58+
return value;
59+
},
60+
(error) => {
61+
isMounted() &&
62+
callId === lastCallId.current &&
63+
set({ error, loading: false });
64+
65+
return error;
66+
}
67+
) as ReturnType<T>;
68+
}, deps);
69+
70+
return [state, callback as unknown as T];
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useCallback, useEffect, useRef } from "react";
2+
3+
export default function useMountedState(): () => boolean {
4+
const mountedRef = useRef<boolean>(false);
5+
const get = useCallback(() => mountedRef.current, []);
6+
7+
useEffect(() => {
8+
mountedRef.current = true;
9+
10+
return () => {
11+
mountedRef.current = false;
12+
};
13+
}, []);
14+
15+
return get;
16+
}

copilot-widget/lib/hooks/useVote.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useAxiosInstance } from "@lib/contexts/axiosInstance";
2+
import useAsyncFn from "./useAsyncFn";
3+
4+
function useUpvote(id: string, onSuccess?: () => void) {
5+
const axios = useAxiosInstance();
6+
return useAsyncFn(
7+
async () =>
8+
axios.axiosInstance.post<{
9+
message: string;
10+
}>(`/chat/vote/${id}`),
11+
[axios, id, onSuccess]
12+
);
13+
}
14+
15+
function useDownvote(id: string, onSuccess?: () => void) {
16+
const axios = useAxiosInstance();
17+
return useAsyncFn(
18+
async () =>
19+
axios.axiosInstance.delete<{
20+
message: string;
21+
}>(`/chat/vote/${id}`),
22+
[axios, id, onSuccess]
23+
);
24+
}
25+
26+
export { useUpvote, useDownvote };

copilot-widget/lib/types/helpers.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type PromiseType<P extends Promise<any>> = P extends Promise<infer T>
2+
? T
3+
: never;
4+
5+
export type FunctionReturningPromise = (...args: any[]) => Promise<any>;

copilot-widget/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@openchatai/copilot-widget",
33
"private": false,
4-
"version": "2.2.2",
4+
"version": "2.3.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",
@@ -28,12 +28,12 @@
2828
"@typescript-eslint/eslint-plugin": "^5.59.0",
2929
"@typescript-eslint/parser": "^5.59.0",
3030
"@vitejs/plugin-react": "^4.0.0",
31-
"autoprefixer": "^10.4.14",
31+
"autoprefixer": "^10.4.17",
3232
"axios": "^1.6.0",
3333
"eslint": "^8.38.0",
3434
"eslint-plugin-react-hooks": "^4.6.0",
3535
"eslint-plugin-react-refresh": "^0.3.4",
36-
"postcss": "^8.4.31",
36+
"postcss": "^8.4.33",
3737
"prettier": "^2.8.8",
3838
"react": "^18.x",
3939
"react-dom": "^18.x",
@@ -46,7 +46,7 @@
4646
"socket.io-client": "^4.7.2",
4747
"tailwind-merge": "^1.13.2",
4848
"tailwind-scrollbar": "^3.0.4",
49-
"tailwindcss": "^3.3.3",
49+
"tailwindcss": "^3.4.1",
5050
"tailwindcss-animate": "^1.0.6",
5151
"timeago.js": "^4.0.2",
5252
"typescript": "^5.0.2",

0 commit comments

Comments
 (0)