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 #600 from openchatai/widget/fixed-when-trigger-sel…
Browse files Browse the repository at this point in the history
…ector-is-undefined

Make the widget fixed on the right bottom corner with a trigger attached to the bottom, when the trigger selector is undefined
  • Loading branch information
faltawy authored Feb 2, 2024
2 parents 2b9c0c7 + 7de99f6 commit a60d4c1
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 108 deletions.
4 changes: 2 additions & 2 deletions copilot-widget/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" dir="rtl">
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand Down Expand Up @@ -108,7 +108,7 @@ <h2>
initAiCoPilot({
initialMessage: "Hi Sir", // optional
token: token, // required
triggerSelector: "#triggerSelector", // optional
// triggerSelector: "#triggerSelector", // optional
rootId: "opencopilot-root-2", // optional otherwise it will create a div with id opencopilot-root
apiUrl: apiUrl, // required
socketUrl: socketUrl,
Expand Down
62 changes: 51 additions & 11 deletions copilot-widget/lib/CopilotWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useRef } from "react";
import { useWidgetState } from "./contexts/WidgetState";
import cn from "./utils/cn";
import ChatScreenWithSfxs from "./screens/ChatScreen";
import ChatScreen from "./screens/ChatScreen";
import { IS_SERVER } from "./utils/is_server";
import { MessageCircle } from "lucide-react";

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

const TRIGGER_BOTTOM = "20px";
const TRIGGER_RIGHT = "20px";

export function CopilotWidget({
triggerSelector,
__isEmbedded,
}: {
triggerSelector: string;
triggerSelector?: string;
__isEmbedded?: boolean;
}) {
const [open, toggle] = useWidgetState();
useTrigger(triggerSelector, toggle);
const SHOULD_RENDER_IN_THE_RIGHT_CORNER = !triggerSelector && __isEmbedded;
return (
<div
data-open={open}
className={cn(
"w-full overflow-hidden pointer-events-auto h-full rounded-lg bg-white shadow relative",
"opacity-0 transition-opacity ease",
open ? "opacity-100 animate-in fade-in" : "hidden animate-out fade-out"
<>
<div
data-open={open}
className={cn(
"w-full overflow-hidden pointer-events-auto h-full rounded-lg bg-white shadow relative",
"opacity-0 transition-opacity ease",
open
? "opacity-100 animate-in fade-in-10"
: "hidden animate-out fade-out",
SHOULD_RENDER_IN_THE_RIGHT_CORNER && "fixed max-w-sm w-full"
)}
style={{
right: SHOULD_RENDER_IN_THE_RIGHT_CORNER
? `calc(${TRIGGER_RIGHT})`
: undefined,
bottom: SHOULD_RENDER_IN_THE_RIGHT_CORNER
? `calc(${TRIGGER_BOTTOM} + 60px)`
: undefined,
height: SHOULD_RENDER_IN_THE_RIGHT_CORNER
? `calc(95% - ${TRIGGER_BOTTOM} - 100px)`
: "100%",
}}
>
<ChatScreen />
</div>

{SHOULD_RENDER_IN_THE_RIGHT_CORNER && (
<div
className="fixed z-50 pointer-events-auto transition-all ease-in-out duration-300"
style={{
bottom: TRIGGER_BOTTOM,
right: TRIGGER_RIGHT,
}}
>
<button
onClick={toggle}
className="rounded-full p-2.5 text-white bg-primary flex-center"
>
<MessageCircle className="size-8 rtl:-scale-x-100" />
</button>
</div>
)}
>
<ChatScreenWithSfxs />
</div>
</>
);
}
72 changes: 50 additions & 22 deletions copilot-widget/lib/components/ChatInputFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import TextareaAutosize from "react-textarea-autosize";
import { SendHorizonal, Redo2 } from "lucide-react";
import { SendHorizonal, AlertTriangle, RotateCcw } from "lucide-react";
import { useChat } from "../contexts/Controller";
import { useRef, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ToolTip";
import { getId, isEmpty } from "@lib/utils/utils";
import now from "@lib/utils/timenow";
import { useDocumentDirection } from "@lib/hooks/useDocumentDirection";
import { VoiceRecorder } from "./VoiceRecorder";
import { useInitialData } from "@lib/hooks/useInitialData";
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTrigger,
} from "./Dialog";
import { Button } from "./Button";

function MessageSuggestions() {
const { data } = useInitialData();
Expand Down Expand Up @@ -38,13 +45,44 @@ function MessageSuggestions() {
</>
);
}
// curl --location 'http://localhost:8888/backend/chat/transcribe' \
// --form 'file=@"/Users/gharbat/Downloads/Neets.ai-example-us-female-2.mp3"'

function ResetButtonWithConfirmation() {
const { reset } = useChat();
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>
<RotateCcw size={20} />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<div className="mx-auto flex flex-col items-center justify-center">
<span className="text-rose-500">
<AlertTriangle className="size-10" />
</span>
<h2>are you sure?</h2>
</div>
</DialogHeader>
<div className="flex flex-row items-center justify-center gap-2">
<Button
asChild
variant="destructive"
className="font-semibold"
onClick={reset}
>
<DialogClose>Yes, reset</DialogClose>
</Button>
<Button asChild variant="secondary" className="font-semibold">
<DialogClose>No, Cancel</DialogClose>
</Button>
</div>
</DialogContent>
</Dialog>
);
}
function ChatInputFooter() {
const [input, setInput] = useState("");
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const { sendMessage, reset, messages } = useChat();
const { sendMessage } = useChat();
const { loading } = useChat();
const canSend = input.trim().length > 0;
const { direction } = useDocumentDirection();
Expand All @@ -68,14 +106,15 @@ function ChatInputFooter() {
}
}
return (
<footer className=" p-2 flex w-full flex-col gap-2">
<footer className="p-2 flex w-full flex-col gap-2">
<MessageSuggestions />
<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">
<div className=" flex-1">
<div className="flex-1">
<TextareaAutosize
dir="auto"
ref={textAreaRef}
autoFocus={true}
placeholder="_"
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
Expand All @@ -87,25 +126,14 @@ function ChatInputFooter() {
rows={1}
value={input}
onChange={handleTextareaChange}
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"
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"
/>
</div>
<div
dir={direction}
className="flex items-center justify-center gap-2 h-fit px-2 text-lg"
className="flex items-center justify-center gap-2 h-fit px-2 text-lg"
>
<Tooltip>
<TooltipTrigger asChild hidden>
<button
onClick={reset}
className=" text-xl disabled: opacity-40 disabled: pointer-events-none disabled: cursor-not-allowed text-[#5e5c5e] transition-all"
disabled={!(messages.length > 0)}
>
<Redo2 className="rtl: rotate-180" />
</button>
</TooltipTrigger>
<TooltipContent>reset chat</TooltipContent>
</Tooltip>
<ResetButtonWithConfirmation />
<VoiceRecorder onSuccess={(text) => setInput(text)} />
<button
onClick={handleInputSubmit}
Expand Down
4 changes: 3 additions & 1 deletion copilot-widget/lib/components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
ComponentPropsWithoutRef,
forwardRef,
useState,
ReactNode,
} from "react";
import { Button } from "./Button";

type DialogProps = {
open?: boolean;
Expand Down Expand Up @@ -78,7 +80,7 @@ const DialogContent = forwardRef<
{...props}
data-state={open ? "open" : "closed"}
className={cn(
"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",
"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",
className
)}
ref={ref}
Expand Down
6 changes: 4 additions & 2 deletions copilot-widget/lib/screens/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ function ChatScreen() {
}
}}
/>
{loading && conversationInfo && <BotMessageLoading displayText={conversationInfo} />}
{loading && conversationInfo && (
<BotMessageLoading displayText={conversationInfo} />
)}
{failedMessage && <BotMessageError message={failedMessage} />}
</div>
</main>
Expand All @@ -74,7 +76,7 @@ function ChatScreen() {
);
}

export default function ChatScreenWithSfxs() {
export default function Widget() {
return (
<ChatProvider>
<ChatScreen />
Expand Down
2 changes: 1 addition & 1 deletion 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.4.0",
"version": "2.4.2",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
9 changes: 7 additions & 2 deletions copilot-widget/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ declare global {
* @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
* @description Initialize the widget
*/
function initAiCoPilot({ triggerSelector, containerProps, rootId, ...options }: Options & { rootId?: string }) {
function initAiCoPilot({
triggerSelector,
containerProps,
rootId,
...options
}: Options & { rootId?: string }) {
const container = composeRoot(rootId ?? defaultRootId, rootId === undefined);
createRoot(container).render(
<Root
Expand All @@ -25,7 +30,7 @@ function initAiCoPilot({ triggerSelector, containerProps, rootId, ...options }:
}}
containerProps={containerProps}
>
<CopilotWidget triggerSelector={triggerSelector} />
<CopilotWidget triggerSelector={triggerSelector} __isEmbedded />
</Root>
);
}
Expand Down
2 changes: 1 addition & 1 deletion dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@kbox-labs/react-echarts": "^1.0.3",
"@openchatai/copilot-widget": "^2.4.0",
"@openchatai/copilot-widget": "^2.4.2",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
Expand Down
8 changes: 4 additions & 4 deletions dashboard/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a60d4c1

Please sign in to comment.