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

Commit a60d4c1

Browse files
authored
Merge pull request #600 from openchatai/widget/fixed-when-trigger-selector-is-undefined
Make the widget fixed on the right bottom corner with a trigger attached to the bottom, when the trigger selector is undefined
2 parents 2b9c0c7 + 7de99f6 commit a60d4c1

File tree

10 files changed

+190
-108
lines changed

10 files changed

+190
-108
lines changed

copilot-widget/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html lang="en" dir="rtl">
2+
<html lang="en" dir="ltr">
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -108,7 +108,7 @@ <h2>
108108
initAiCoPilot({
109109
initialMessage: "Hi Sir", // optional
110110
token: token, // required
111-
triggerSelector: "#triggerSelector", // optional
111+
// triggerSelector: "#triggerSelector", // optional
112112
rootId: "opencopilot-root-2", // optional otherwise it will create a div with id opencopilot-root
113113
apiUrl: apiUrl, // required
114114
socketUrl: socketUrl,

copilot-widget/lib/CopilotWidget.tsx

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { useEffect, useRef } from "react";
22
import { useWidgetState } from "./contexts/WidgetState";
33
import cn from "./utils/cn";
4-
import ChatScreenWithSfxs from "./screens/ChatScreen";
4+
import ChatScreen from "./screens/ChatScreen";
55
import { IS_SERVER } from "./utils/is_server";
6+
import { MessageCircle } from "lucide-react";
67

78
function useTrigger(selector?: string, toggle?: () => void) {
89
const trigger = useRef<HTMLElement | null>(
@@ -24,23 +25,62 @@ function useTrigger(selector?: string, toggle?: () => void) {
2425
}, [selector, toggle, trigger]);
2526
}
2627

28+
const TRIGGER_BOTTOM = "20px";
29+
const TRIGGER_RIGHT = "20px";
30+
2731
export function CopilotWidget({
2832
triggerSelector,
33+
__isEmbedded,
2934
}: {
30-
triggerSelector: string;
35+
triggerSelector?: string;
36+
__isEmbedded?: boolean;
3137
}) {
3238
const [open, toggle] = useWidgetState();
3339
useTrigger(triggerSelector, toggle);
40+
const SHOULD_RENDER_IN_THE_RIGHT_CORNER = !triggerSelector && __isEmbedded;
3441
return (
35-
<div
36-
data-open={open}
37-
className={cn(
38-
"w-full overflow-hidden pointer-events-auto h-full rounded-lg bg-white shadow relative",
39-
"opacity-0 transition-opacity ease",
40-
open ? "opacity-100 animate-in fade-in" : "hidden animate-out fade-out"
42+
<>
43+
<div
44+
data-open={open}
45+
className={cn(
46+
"w-full overflow-hidden pointer-events-auto h-full rounded-lg bg-white shadow relative",
47+
"opacity-0 transition-opacity ease",
48+
open
49+
? "opacity-100 animate-in fade-in-10"
50+
: "hidden animate-out fade-out",
51+
SHOULD_RENDER_IN_THE_RIGHT_CORNER && "fixed max-w-sm w-full"
52+
)}
53+
style={{
54+
right: SHOULD_RENDER_IN_THE_RIGHT_CORNER
55+
? `calc(${TRIGGER_RIGHT})`
56+
: undefined,
57+
bottom: SHOULD_RENDER_IN_THE_RIGHT_CORNER
58+
? `calc(${TRIGGER_BOTTOM} + 60px)`
59+
: undefined,
60+
height: SHOULD_RENDER_IN_THE_RIGHT_CORNER
61+
? `calc(95% - ${TRIGGER_BOTTOM} - 100px)`
62+
: "100%",
63+
}}
64+
>
65+
<ChatScreen />
66+
</div>
67+
68+
{SHOULD_RENDER_IN_THE_RIGHT_CORNER && (
69+
<div
70+
className="fixed z-50 pointer-events-auto transition-all ease-in-out duration-300"
71+
style={{
72+
bottom: TRIGGER_BOTTOM,
73+
right: TRIGGER_RIGHT,
74+
}}
75+
>
76+
<button
77+
onClick={toggle}
78+
className="rounded-full p-2.5 text-white bg-primary flex-center"
79+
>
80+
<MessageCircle className="size-8 rtl:-scale-x-100" />
81+
</button>
82+
</div>
4183
)}
42-
>
43-
<ChatScreenWithSfxs />
44-
</div>
84+
</>
4585
);
4686
}

copilot-widget/lib/components/ChatInputFooter.tsx

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import TextareaAutosize from "react-textarea-autosize";
2-
import { SendHorizonal, Redo2 } from "lucide-react";
2+
import { SendHorizonal, AlertTriangle, RotateCcw } from "lucide-react";
33
import { useChat } from "../contexts/Controller";
44
import { useRef, useState } from "react";
5-
import { Tooltip, TooltipContent, TooltipTrigger } from "./ToolTip";
65
import { getId, isEmpty } from "@lib/utils/utils";
76
import now from "@lib/utils/timenow";
87
import { useDocumentDirection } from "@lib/hooks/useDocumentDirection";
98
import { VoiceRecorder } from "./VoiceRecorder";
109
import { useInitialData } from "@lib/hooks/useInitialData";
10+
import {
11+
Dialog,
12+
DialogClose,
13+
DialogContent,
14+
DialogHeader,
15+
DialogTrigger,
16+
} from "./Dialog";
17+
import { Button } from "./Button";
1118

1219
function MessageSuggestions() {
1320
const { data } = useInitialData();
@@ -38,13 +45,44 @@ function MessageSuggestions() {
3845
</>
3946
);
4047
}
41-
// curl --location 'http://localhost:8888/backend/chat/transcribe' \
42-
// --form 'file=@"/Users/gharbat/Downloads/Neets.ai-example-us-female-2.mp3"'
43-
48+
function ResetButtonWithConfirmation() {
49+
const { reset } = useChat();
50+
const [open, setOpen] = useState(false);
51+
return (
52+
<Dialog open={open} onOpenChange={setOpen}>
53+
<DialogTrigger>
54+
<RotateCcw size={20} />
55+
</DialogTrigger>
56+
<DialogContent>
57+
<DialogHeader>
58+
<div className="mx-auto flex flex-col items-center justify-center">
59+
<span className="text-rose-500">
60+
<AlertTriangle className="size-10" />
61+
</span>
62+
<h2>are you sure?</h2>
63+
</div>
64+
</DialogHeader>
65+
<div className="flex flex-row items-center justify-center gap-2">
66+
<Button
67+
asChild
68+
variant="destructive"
69+
className="font-semibold"
70+
onClick={reset}
71+
>
72+
<DialogClose>Yes, reset</DialogClose>
73+
</Button>
74+
<Button asChild variant="secondary" className="font-semibold">
75+
<DialogClose>No, Cancel</DialogClose>
76+
</Button>
77+
</div>
78+
</DialogContent>
79+
</Dialog>
80+
);
81+
}
4482
function ChatInputFooter() {
4583
const [input, setInput] = useState("");
4684
const textAreaRef = useRef<HTMLTextAreaElement>(null);
47-
const { sendMessage, reset, messages } = useChat();
85+
const { sendMessage } = useChat();
4886
const { loading } = useChat();
4987
const canSend = input.trim().length > 0;
5088
const { direction } = useDocumentDirection();
@@ -68,14 +106,15 @@ function ChatInputFooter() {
68106
}
69107
}
70108
return (
71-
<footer className=" p-2 flex w-full flex-col gap-2">
109+
<footer className="p-2 flex w-full flex-col gap-2">
72110
<MessageSuggestions />
73111
<div className="w-full flex items-center ring-[#334155]/60 transition-colors justify-between ring-1 overflow-hidden focus-within:ring-primary gap-2 bg-accent p-2 rounded-2xl">
74-
<div className=" flex-1">
112+
<div className="flex-1">
75113
<TextareaAutosize
76114
dir="auto"
77115
ref={textAreaRef}
78116
autoFocus={true}
117+
placeholder="_"
79118
onKeyDown={(event) => {
80119
if (event.key === "Enter" && !event.shiftKey) {
81120
event.preventDefault();
@@ -87,25 +126,14 @@ function ChatInputFooter() {
87126
rows={1}
88127
value={input}
89128
onChange={handleTextareaChange}
90-
className=" w-full resize-none bg-transparent focus-visible:outline-none border-none focus:outline-none focus:border-none scrollbar-thin leading-tight whitespace-pre-wrap py-1.5 px-4 placeholder:align-middle overflow-auto outline-none text-accent2 text-[14px] placeholder:text-xs font-normal"
129+
className=" w-full resize-none bg-transparent focus-visible:outline-none border-none focus:outline-none focus:border-none scrollbar-thin leading-tight whitespace-pre-wrap py-1.5 px-4 placeholder:align-middle overflow-auto outline-none text-accent2 text-[14px] placeholder:text-xs font-normal"
91130
/>
92131
</div>
93132
<div
94133
dir={direction}
95-
className="flex items-center justify-center gap-2 h-fit px-2 text-lg"
134+
className="flex items-center justify-center gap-2 h-fit px-2 text-lg"
96135
>
97-
<Tooltip>
98-
<TooltipTrigger asChild hidden>
99-
<button
100-
onClick={reset}
101-
className=" text-xl disabled: opacity-40 disabled: pointer-events-none disabled: cursor-not-allowed text-[#5e5c5e] transition-all"
102-
disabled={!(messages.length > 0)}
103-
>
104-
<Redo2 className="rtl: rotate-180" />
105-
</button>
106-
</TooltipTrigger>
107-
<TooltipContent>reset chat</TooltipContent>
108-
</Tooltip>
136+
<ResetButtonWithConfirmation />
109137
<VoiceRecorder onSuccess={(text) => setInput(text)} />
110138
<button
111139
onClick={handleInputSubmit}

copilot-widget/lib/components/Dialog.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
ComponentPropsWithoutRef,
66
forwardRef,
77
useState,
8+
ReactNode,
89
} from "react";
10+
import { Button } from "./Button";
911

1012
type DialogProps = {
1113
open?: boolean;
@@ -78,7 +80,7 @@ const DialogContent = forwardRef<
7880
{...props}
7981
data-state={open ? "open" : "closed"}
8082
className={cn(
81-
"rounded-lg z-[100] w-full grid max-w-[70%] min-w-fit bg-white gap-2 shadow-lg p-4 animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
83+
"rounded-lg z-[100] w-full grid max-w-[70%] min-w-fit bg-white gap-2 shadow-lg p-4 animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom-3",
8284
className
8385
)}
8486
ref={ref}

copilot-widget/lib/screens/ChatScreen.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ function ChatScreen() {
6565
}
6666
}}
6767
/>
68-
{loading && conversationInfo && <BotMessageLoading displayText={conversationInfo} />}
68+
{loading && conversationInfo && (
69+
<BotMessageLoading displayText={conversationInfo} />
70+
)}
6971
{failedMessage && <BotMessageError message={failedMessage} />}
7072
</div>
7173
</main>
@@ -74,7 +76,7 @@ function ChatScreen() {
7476
);
7577
}
7678

77-
export default function ChatScreenWithSfxs() {
79+
export default function Widget() {
7880
return (
7981
<ChatProvider>
8082
<ChatScreen />

copilot-widget/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@openchatai/copilot-widget",
33
"private": false,
4-
"version": "2.4.0",
4+
"version": "2.4.2",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

copilot-widget/src/main.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ declare global {
1616
* @param rootId The id of the root element for more control, you don't need to use this unless you want more control over the widget
1717
* @description Initialize the widget
1818
*/
19-
function initAiCoPilot({ triggerSelector, containerProps, rootId, ...options }: Options & { rootId?: string }) {
19+
function initAiCoPilot({
20+
triggerSelector,
21+
containerProps,
22+
rootId,
23+
...options
24+
}: Options & { rootId?: string }) {
2025
const container = composeRoot(rootId ?? defaultRootId, rootId === undefined);
2126
createRoot(container).render(
2227
<Root
@@ -25,7 +30,7 @@ function initAiCoPilot({ triggerSelector, containerProps, rootId, ...options }:
2530
}}
2631
containerProps={containerProps}
2732
>
28-
<CopilotWidget triggerSelector={triggerSelector} />
33+
<CopilotWidget triggerSelector={triggerSelector} __isEmbedded />
2934
</Root>
3035
);
3136
}

dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"dependencies": {
1414
"@hookform/resolvers": "^3.3.1",
1515
"@kbox-labs/react-echarts": "^1.0.3",
16-
"@openchatai/copilot-widget": "^2.4.0",
16+
"@openchatai/copilot-widget": "^2.4.2",
1717
"@radix-ui/react-accordion": "^1.1.2",
1818
"@radix-ui/react-alert-dialog": "^1.0.5",
1919
"@radix-ui/react-avatar": "^1.0.4",

dashboard/pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)