[FEAT] Implement new designs for embed widget (#976)

* WIP implement new embed designs

* WIP embed designs

* WIP embed UI

* UI complete for desktop styles

* desktop UI fixes

* UI fixes

* mobile view ui changes

* fix placement of open button

* small tweaks to UI

* add support for positioning embed chat

* finalize docs for embed
Publish new version

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2024-04-02 13:03:42 -07:00 committed by GitHub
parent df977e5177
commit 335ac43a5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 417 additions and 248 deletions

View File

@ -16,7 +16,7 @@ The AnythingLLM Embedded chat widget allows you to expose a workspace and its em
### Security
- Users will _not_ be able to view or read context snippets like they can in the core AnythingLLM application
- Users are assigned a random session ID that they use to persist a chat session.
- **Recommended** You can limit both the number of chats an embedding can process **and** per-session.
- **Recommended** You can limit both the number of chats an embedding can process **and** per-session.
_by using the AnythingLLM embedded chat widget you are responsible for securing and configuration of the embed as to not allow excessive chat model abuse of your instance_
@ -35,13 +35,13 @@ While in development mode (`yarn dev`) the script will rebuild on any changes to
The primary way of embedding a workspace as a chat widget is via a simple `<script>`
```html
<!--
<!--
An example of a script tag embed
REQUIRED data attributes:
data-embed-id // The unique id of your embed with its default settings
data-base-api-url // The URL of your anythingLLM instance backend
-->
<script
<script
data-embed-id="5fc05aaf-2f2c-4c84-87a3-367a4692c1ee"
data-base-api-url="http://localhost:3001/api/embed"
src="http://localhost:3000/embed/anythingllm-chat-widget.min.js">
@ -76,6 +76,8 @@ REQUIRED data attributes:
- `data-sponsor-text` — The text displays in sponsor text in the footer of an open chat window.
- `data-position` - Adjust the positioning of the embed chat widget and open chat button. Default `bottom-right`. Options are `bottom-right`, `bottom-left`, `top-right`, `top-left`.
**Behavior Overrides**
- `data-open-on-load` — Once loaded, open the chat as default. It can still be closed by the user.

View File

@ -1,13 +1,11 @@
<!doctype html>
<html lang="en">
<body>
<h1>This is an example testing page for embedded AnythingLLM.</h1>
<!--
<!--
<script data-embed-id="example-uuid" data-base-api-url='http://localhost:3001/api/embed' data-open-on-load="on"
src="/dist/anythingllm-chat-widget.js"> USE THIS SRC FOR DEVELOPMENT SO CHANGES APPEAR!
</script>
</script>
-->
</body>
</html>
</html>

View File

@ -18,20 +18,22 @@ export default function App() {
}, [embedSettings.loaded]);
if (!embedSettings.loaded) return null;
const positionClasses = {
"bottom-left": "bottom-0 left-0 ml-4",
"bottom-right": "bottom-0 right-0 mr-4",
"top-left": "top-0 left-0 ml-4 mt-4",
"top-right": "top-0 right-0 mr-4 mt-4",
};
const position = embedSettings.position || "bottom-right";
return (
<>
<Head />
<div className="fixed bottom-0 right-0 mb-4 mr-4 z-50">
<div className={`fixed inset-0 z-50 ${isChatOpen ? "block" : "hidden"}`}>
<div
style={{
width: isChatOpen ? 320 : "auto",
height: isChatOpen ? "93vh" : "auto",
}}
className={`${
isChatOpen
? "max-w-md px-4 py-2 bg-white rounded-lg border shadow-lg w-72"
: "w-16 h-16 rounded-full"
}`}
className={`w-full h-full bg-white md:max-w-[400px] md:max-h-[700px] md:fixed md:bottom-0 md:right-0 md:mb-4 md:mr-4 md:rounded-2xl md:border md:border-gray-300 md:shadow-[0_4px_14px_rgba(0,0,0,0.25)] ${positionClasses[position]}`}
>
{isChatOpen && (
<ChatWindow
@ -40,13 +42,19 @@ export default function App() {
sessionId={sessionId}
/>
)}
</div>
</div>
{!isChatOpen && (
<div
className={`fixed bottom-0 ${positionClasses[position]} mb-4 z-50`}
>
<OpenButton
settings={embedSettings}
isOpen={isChatOpen}
toggleOpen={() => toggleOpenChat(true)}
/>
</div>
</div>
)}
</>
);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,57 +1,81 @@
import React, { memo, forwardRef } from "react";
import { Warning } from "@phosphor-icons/react";
// import Actions from "./Actions";
import renderMarkdown from "@/utils/chat/markdown";
import { embedderSettings } from "@/main";
import { v4 } from "uuid";
import createDOMPurify from "dompurify";
import AnythingLLMIcon from "@/assets/anything-llm-icon.svg";
import { formatDate } from "@/utils/date";
const DOMPurify = createDOMPurify(window);
const HistoricalMessage = forwardRef(
({ uuid = v4(), message, role, sources = [], error = false }, ref) => {
(
{ uuid = v4(), message, role, sources = [], error = false, sentAt },
ref
) => {
return (
<div
key={uuid}
ref={ref}
className={`flex rounded-lg justify-center items-end w-full h-fit ${
error
? "bg-red-200"
: role === "user"
? embedderSettings.USER_BACKGROUND_COLOR
: embedderSettings.AI_BACKGROUND_COLOR
}`}
>
<div
style={{ wordBreak: "break-word" }}
className={`py-2 px-2 w-full flex flex-col`}
>
<div className="flex">
{error ? (
<div className="p-2 rounded-lg bg-red-50 text-red-500">
<span className={`inline-block `}>
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
respond to message.
</span>
<p className="text-xs font-mono mt-2 border-l-2 border-red-500 pl-2 bg-red-300 p-2 rounded-sm">
{error}
</p>
</div>
) : (
<span
className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(renderMarkdown(message)),
}}
/>
)}
<div className="py-[5px]">
{role === "assistant" && (
<div
className={`text-[10px] font-medium text-gray-400 ml-[54px] mr-6 mb-2 text-left`}
>
AnythingLLM Chat Assistant
</div>
{/* {role === "assistant" && !error && (
<div className="flex gap-x-5">
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden" />
<Actions message={DOMPurify.sanitize(message)} />
)}
<div
key={uuid}
ref={ref}
className={`flex items-start w-full h-fit ${
role === "user" ? "justify-end" : "justify-start"
}`}
>
{role === "assistant" && (
<img
src={AnythingLLMIcon}
alt="Anything LLM Icon"
className="w-9 h-9 flex-shrink-0 ml-2 mt-2"
/>
)}
<div
style={{ wordBreak: "break-word" }}
className={`py-[11px] px-4 flex flex-col ${
error
? "bg-red-200 rounded-lg mr-[37px] ml-[9px]"
: role === "user"
? embedderSettings.USER_STYLES
: embedderSettings.ASSISTANT_STYLES
} shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
>
<div className="flex">
{error ? (
<div className="p-2 rounded-lg bg-red-50 text-red-500">
<span className={`inline-block `}>
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
respond to message.
</span>
<p className="text-xs font-mono mt-2 border-l-2 border-red-500 pl-2 bg-red-300 p-2 rounded-sm">
{error}
</p>
</div>
) : (
<span
className={`whitespace-pre-line font-medium flex flex-col gap-y-1 text-sm leading-[20px]`}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(renderMarkdown(message)),
}}
/>
)}
</div>
)} */}
</div>
</div>
{sentAt && (
<div
className={`text-[10px] font-medium text-gray-400 ml-[54px] mr-6 mt-2 ${role === "user" ? "text-right" : "text-left"}`}
>
{formatDate(sentAt)}
</div>
)}
</div>
);
}

View File

@ -2,6 +2,8 @@ import { forwardRef, memo } from "react";
import { Warning } from "@phosphor-icons/react";
import renderMarkdown from "@/utils/chat/markdown";
import { embedderSettings } from "@/main";
import AnythingLLMIcon from "@/assets/anything-llm-icon.svg";
import { formatDate } from "@/utils/date";
const PromptReply = forwardRef(
({ uuid, reply, pending, error, sources = [] }, ref) => {
@ -9,13 +11,18 @@ const PromptReply = forwardRef(
if (pending) {
return (
<div
ref={ref}
className={`flex justify-center items-end rounded-lg w-full ${embedderSettings.AI_BACKGROUND_COLOR}`}
>
<div className="py-2 px-2 w-full flex flex-col">
<div className={`flex items-start w-full h-fit justify-start`}>
<img
src={AnythingLLMIcon}
alt="Anything LLM Icon"
className="w-9 h-9 flex-shrink-0 ml-2"
/>
<div
style={{ wordBreak: "break-word" }}
className={`py-[11px] px-4 flex flex-col ${embedderSettings.ASSISTANT_STYLES} shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
>
<div className="flex gap-x-5">
<div className="mt-3 ml-5 dot-falling"></div>
<div className="mx-4 my-1 dot-falling"></div>
</div>
</div>
</div>
@ -24,8 +31,16 @@ const PromptReply = forwardRef(
if (error) {
return (
<div className={`flex justify-center items-end w-full bg-red-200`}>
<div className="py-2 px-4 w-full flex gap-x-5 flex-col">
<div className={`flex items-end w-full h-fit justify-start`}>
<img
src={AnythingLLMIcon}
alt="Anything LLM Icon"
className="w-9 h-9 flex-shrink-0 ml-2"
/>
<div
style={{ wordBreak: "break-word" }}
className={`py-[11px] px-4 rounded-lg flex flex-col bg-red-200 shadow-[0_4px_14px_rgba(0,0,0,0.25)] mr-[37px] ml-[9px]`}
>
<div className="flex gap-x-5">
<span
className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`}
@ -41,22 +56,41 @@ const PromptReply = forwardRef(
}
return (
<div
key={uuid}
ref={ref}
className={`flex justify-center items-end w-full ${embedderSettings.AI_BACKGROUND_COLOR}`}
>
<div className="py-[5px]">
<div
style={{ wordBreak: "break-word" }}
className="py-2 px-2 w-full flex flex-col"
className={`text-[10px] font-medium text-gray-400 ml-[54px] mr-6 mb-2 text-left`}
>
<div className="flex gap-x-5">
<span
className={`reply whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }}
/>
AnythingLLM Chat Assistant
</div>
<div
key={uuid}
ref={ref}
className={`flex items-start w-full h-fit justify-start`}
>
<img
src={AnythingLLMIcon}
alt="Anything LLM Icon"
className="w-9 h-9 flex-shrink-0 ml-2"
/>
<div
style={{ wordBreak: "break-word" }}
className={`py-[11px] px-4 flex flex-col ${
error ? "bg-red-200" : embedderSettings.ASSISTANT_STYLES
} shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
>
<div className="flex gap-x-5">
<span
className={`reply whitespace-pre-line font-normal text-sm md:text-sm flex flex-col gap-y-1`}
dangerouslySetInnerHTML={{ __html: renderMarkdown(reply) }}
/>
</div>
</div>
</div>
<div
className={`text-[10px] font-medium text-gray-400 ml-[54px] mr-6 mt-2 text-left`}
>
{formatDate(Date.now() / 1000)}
</div>
</div>
);
}

View File

@ -46,10 +46,10 @@ export default function ChatHistory({ settings = {}, history = [] }) {
if (history.length === 0) {
return (
<div className="h-full max-h-[82vh] pb-[100px] pt-[5px] bg-gray-100 rounded-lg px-2 h-full mt-2 gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll">
<div className="pb-[100px] pt-[5px] rounded-lg px-2 h-full mt-2 gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll">
<div className="flex h-full flex-col items-center justify-center">
<p className="text-slate-400 text-sm font-base py-4 text-center">
{settings?.greeting ?? "Send a chat to get started!"}
{settings?.greeting ?? "Send a chat to get started."}
</p>
</div>
</div>
@ -58,7 +58,7 @@ export default function ChatHistory({ settings = {}, history = [] }) {
return (
<div
className="h-full max-h-[82vh] pb-[100px] pt-[5px] bg-gray-100 rounded-lg px-2 h-full mt-2 gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll"
className="pb-[30px] pt-[5px] rounded-lg px-2 h-full gap-y-2 overflow-y-scroll flex flex-col justify-start no-scroll md:max-h-[500px] max-h-[calc(100vh-200px)]"
id="chat-history"
ref={chatHistoryRef}
>
@ -87,6 +87,7 @@ export default function ChatHistory({ settings = {}, history = [] }) {
key={index}
ref={isLastMessage ? replyRef : null}
message={props.content}
sentAt={props.sentAt || Date.now() / 1000}
role={props.role}
sources={props.sources}
chatId={props.chatId}
@ -96,12 +97,12 @@ export default function ChatHistory({ settings = {}, history = [] }) {
);
})}
{!isAtBottom && (
<div className="fixed bottom-[10rem] right-[3rem] z-50 cursor-pointer animate-pulse">
<div className="fixed bottom-[10rem] right-[50px] z-50 cursor-pointer animate-pulse">
<div className="flex flex-col items-center">
<div className="p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white">
<div className="p-1 rounded-full border border-white/10 bg-black/20 hover:bg-black/50">
<ArrowDown
weight="bold"
className="text-white/60 w-5 h-5"
className="text-white/50 w-5 h-5"
onClick={scrollToBottom}
/>
</div>

View File

@ -1,5 +1,5 @@
import { CircleNotch, PaperPlaneRight } from "@phosphor-icons/react";
import React, { useState, useRef } from "react";
import React, { useState, useRef, useEffect } from "react";
export default function PromptInput({
message,
@ -9,13 +9,27 @@ export default function PromptInput({
buttonDisabled,
}) {
const formRef = useRef(null);
const textareaRef = useRef(null);
const [_, setFocused] = useState(false);
useEffect(() => {
if (!inputDisabled && textareaRef.current) {
textareaRef.current.focus();
}
resetTextAreaHeight();
}, [inputDisabled]);
const handleSubmit = (e) => {
setFocused(false);
submit(e);
};
const resetTextAreaHeight = () => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
}
};
const captureEnter = (event) => {
if (event.keyCode == 13) {
if (!event.shiftKey) {
@ -32,15 +46,16 @@ export default function PromptInput({
};
return (
<div className="w-full absolute left-0 bottom-[5px] z-10 flex justify-center items-center">
<div className="w-full absolute left-0 bottom-[25px] z-10 flex justify-center items-center px-5">
<form
onSubmit={handleSubmit}
className="flex flex-col gap-y-1 rounded-t-lg w-full items-center justify-center"
>
<div className="flex items-center rounded-lg">
<div className="bg-white border border-white/50 rounded-2xl flex flex-col px-4 overflow-hidden">
<div className="flex items-center w-full">
<div className="bg-white border-[1.5px] border-[#22262833]/20 rounded-2xl flex flex-col px-4 overflow-hidden w-full">
<div className="flex items-center w-full">
<textarea
ref={textareaRef}
onKeyUp={adjustTextArea}
onKeyDown={captureEnter}
onChange={onChange}
@ -64,7 +79,11 @@ export default function PromptInput({
{buttonDisabled ? (
<CircleNotch className="w-4 h-4 animate-spin" />
) : (
<PaperPlaneRight className="w-4 h-4 my-3" weight="fill" />
<PaperPlaneRight
size={24}
className="my-3 text-[#22262899]/60 group-hover:text-[#22262899]/90"
weight="fill"
/>
)}
<span className="sr-only">Send message</span>
</button>

View File

@ -1,12 +1,14 @@
import AnythingLLMLogo from "@/assets/anything-llm-dark.png";
import AnythingLLMIcon from "@/assets/anything-llm-icon.svg";
import ChatService from "@/models/chatService";
import {
ArrowCounterClockwise,
Check,
Copy,
DotsThreeOutlineVertical,
Envelope,
Lightning,
X,
} from "@phosphor-icons/react";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
export default function ChatWindowHeader({
sessionId,
@ -16,31 +18,49 @@ export default function ChatWindowHeader({
setChatHistory,
}) {
const [showingOptions, setShowOptions] = useState(false);
const menuRef = useRef();
const buttonRef = useRef();
const handleChatReset = async () => {
await ChatService.resetEmbedChatSession(settings, sessionId);
setChatHistory([]);
setShowOptions(false);
};
useEffect(() => {
function handleClickOutside(event) {
if (
menuRef.current &&
!menuRef.current.contains(event.target) &&
buttonRef.current &&
!buttonRef.current.contains(event.target)
) {
setShowOptions(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [menuRef]);
return (
<div className="flex justify-between items-center relative">
<img
style={{ maxWidth: 100, maxHeight: 20 }}
src={iconUrl ?? AnythingLLMLogo}
alt={iconUrl ? "Brand" : "AnythingLLM Logo"}
/>
<div className="flex gap-x-1 items-center">
<div className="flex items-center relative rounded-t-2xl bg-black/10">
<div className="flex justify-center items-center w-full h-[76px]">
<img
style={{ maxWidth: 48, maxHeight: 48 }}
src={iconUrl ?? AnythingLLMIcon}
alt={iconUrl ? "Brand" : "AnythingLLM Logo"}
/>
</div>
<div className="absolute right-0 flex gap-x-1 items-center px-[22px]">
{settings.loaded && (
<button
ref={buttonRef}
type="button"
onClick={() => setShowOptions(!showingOptions)}
className="hover:bg-gray-100 rounded-sm text-slate-800"
>
<DotsThreeOutlineVertical
size={18}
weight={!showingOptions ? "regular" : "fill"}
/>
<DotsThreeOutlineVertical size={20} weight="fill" />
</button>
)}
<button
@ -48,34 +68,71 @@ export default function ChatWindowHeader({
onClick={closeChat}
className="hover:bg-gray-100 rounded-sm text-slate-800"
>
<X size={18} />
<X size={20} weight="bold" />
</button>
</div>
<OptionsMenu
settings={settings}
showing={showingOptions}
resetChat={handleChatReset}
sessionId={sessionId}
menuRef={menuRef}
/>
</div>
);
}
function OptionsMenu({ settings, showing, resetChat }) {
function OptionsMenu({ settings, showing, resetChat, sessionId, menuRef }) {
if (!showing) return null;
return (
<div className="absolute z-10 bg-white flex flex-col gap-y-1 rounded-lg shadow-lg border border-gray-300 top-[23px] right-[20px] max-w-[150px]">
<div
ref={menuRef}
className="absolute z-10 bg-white flex flex-col gap-y-1 rounded-xl shadow-lg border border-gray-300 top-[64px] right-[46px]"
>
<button
onClick={resetChat}
className="flex items-center gap-x-1 hover:bg-gray-100 text-sm text-gray-700 p-2 rounded-lg"
className="flex items-center gap-x-2 hover:bg-gray-100 text-sm text-gray-700 py-2.5 px-4 rounded-xl"
>
<Lightning size={14} />
<p>Reset Chat</p>
<ArrowCounterClockwise size={24} />
<p className="text-sm text-[#7A7D7E] font-bold">Reset Chat</p>
</button>
<ContactSupport email={settings.supportEmail} />
<SessionID sessionId={sessionId} />
</div>
);
}
function SessionID({ sessionId }) {
if (!sessionId) return null;
const [sessionIdCopied, setSessionIdCopied] = useState(false);
const copySessionId = () => {
navigator.clipboard.writeText(sessionId);
setSessionIdCopied(true);
setTimeout(() => setSessionIdCopied(false), 1000);
};
if (sessionIdCopied) {
return (
<div className="flex items-center gap-x-2 hover:bg-gray-100 text-sm text-gray-700 py-2.5 px-4 rounded-xl">
<Check size={24} />
<p className="text-sm text-[#7A7D7E] font-bold">Copied!</p>
</div>
);
}
return (
<button
onClick={copySessionId}
className="flex items-center gap-x-2 hover:bg-gray-100 text-sm text-gray-700 py-2.5 px-4 rounded-xl"
>
<Copy size={24} />
<p className="text-sm text-[#7A7D7E] font-bold">Session ID</p>
</button>
);
}
function ContactSupport({ email = null }) {
if (!email) return null;
@ -83,10 +140,10 @@ function ContactSupport({ email = null }) {
return (
<a
href={`mailto:${email}?Subject=${encodeURIComponent(subject)}`}
className="flex items-center gap-x-1 hover:bg-gray-100 text-sm text-gray-700 p-2 rounded-lg"
className="flex items-center gap-x-2 hover:bg-gray-100 text-sm text-gray-700 py-2.5 px-4 rounded-xl"
>
<Envelope size={14} />
<p>Email support</p>
<Envelope size={24} />
<p className="text-sm text-[#7A7D7E] font-bold">Email Support</p>
</a>
);
}

View File

@ -4,6 +4,7 @@ import useChatHistory from "@/hooks/chat/useChatHistory";
import ChatContainer from "./ChatContainer";
import Sponsor from "../Sponsor";
import { ChatHistoryLoading } from "./ChatContainer/ChatHistory";
import ResetChat from "../ResetChat";
export default function ChatWindow({ closeChat, settings, sessionId }) {
const { chatHistory, setChatHistory, loading } = useChatHistory(
@ -45,9 +46,13 @@ export default function ChatWindow({ closeChat, settings, sessionId }) {
settings={settings}
knownHistory={chatHistory}
/>
<div className="pt-4 pb-2 h-fit gap-y-1">
<SessionId />
<div className="-mt-2 pb-6 h-fit gap-y-2 z-10">
<Sponsor settings={settings} />
<ResetChat
setChatHistory={setChatHistory}
settings={settings}
sessionId={sessionId}
/>
</div>
</div>
);

View File

@ -11,154 +11,141 @@ pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5p
`;
const customCss = `
/**
* ==============================================
* Dot Falling
* ==============================================
*/
.dot-falling {
position: relative;
left: -9999px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #eeeeee;
color: #5fa4fa;
box-shadow: 9999px 0 0 0 #eeeeee;
animation: dot-falling 1.5s infinite linear;
animation-delay: 0.1s;
}
.dot-falling::before,
.dot-falling::after {
content: "";
display: inline-block;
position: absolute;
top: 0;
}
.dot-falling::before {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #eeeeee;
color: #eeeeee;
animation: dot-falling-before 1.5s infinite linear;
animation-delay: 0s;
}
.dot-falling::after {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #eeeeee;
color: #eeeeee;
animation: dot-falling-after 1.5s infinite linear;
animation-delay: 0.2s;
}
@keyframes dot-falling {
0% {
box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0);
/**
* ==============================================
* Dot Falling
* ==============================================
*/
.dot-falling {
position: relative;
left: -9999px;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #000000;
color: #5fa4fa;
box-shadow: 9999px 0 0 0 #000000;
animation: dot-falling 1.5s infinite linear;
animation-delay: 0.1s;
}
25%,
50%,
75% {
box-shadow: 9999px 0 0 0 #eeeeee;
.dot-falling::before,
.dot-falling::after {
content: "";
display: inline-block;
position: absolute;
top: 0;
}
100% {
box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0);
}
}
@keyframes dot-falling-before {
0% {
box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0);
.dot-falling::before {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #000000;
color: #000000;
animation: dot-falling-before 1.5s infinite linear;
animation-delay: 0s;
}
25%,
50%,
75% {
box-shadow: 9984px 0 0 0 #eeeeee;
.dot-falling::after {
width: 10px;
height: 10px;
border-radius: 5px;
background-color: #000000;
color: #000000;
animation: dot-falling-after 1.5s infinite linear;
animation-delay: 0.2s;
}
100% {
box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0);
}
}
@keyframes dot-falling-after {
0% {
box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0);
@keyframes dot-falling {
0% {
box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 9999px 0 0 0 #000000;
}
100% {
box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0);
}
}
25%,
50%,
75% {
box-shadow: 10014px 0 0 0 #eeeeee;
@keyframes dot-falling-before {
0% {
box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 9984px 0 0 0 #000000;
}
100% {
box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0);
}
}
100% {
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
@keyframes dot-falling-after {
0% {
box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0);
}
25%,
50%,
75% {
box-shadow: 10014px 0 0 0 #000000;
}
100% {
box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);
}
}
}
#chat-history::-webkit-scrollbar,
#chat-container::-webkit-scrollbar,
.no-scroll::-webkit-scrollbar {
display: none !important;
}
#chat-history::-webkit-scrollbar,
#chat-container::-webkit-scrollbar,
.no-scroll::-webkit-scrollbar {
display: none !important;
}
/* Hide scrollbar for IE, Edge and Firefox */
#chat-history,
#chat-container,
.no-scroll {
-ms-overflow-style: none !important;
/* IE and Edge */
scrollbar-width: none !important;
/* Firefox */
}
/* Hide scrollbar for IE, Edge and Firefox */
#chat-history,
#chat-container,
.no-scroll {
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
.animate-slow-pulse {
transform: scale(1);
animation: subtlePulse 20s infinite;
will-change: transform;
}
@keyframes subtlePulse {
0% {
.animate-slow-pulse {
transform: scale(1);
animation: subtlePulse 20s infinite;
will-change: transform;
}
50% {
transform: scale(1.1);
@keyframes subtlePulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
100% {
transform: scale(1);
}
}
@keyframes subtleShift {
0% {
background-position: 0% 50%;
@keyframes subtleShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
50% {
background-position: 100% 50%;
.bg-black-900 {
background: #141414;
}
100% {
background-position: 0% 50%;
}
}
.bg-black-900 {
background: #141414;
}
`;
export default function Head() {

View File

@ -0,0 +1,19 @@
import ChatService from "@/models/chatService";
export default function ResetChat({ setChatHistory, settings, sessionId }) {
const handleChatReset = async () => {
await ChatService.resetEmbedChatSession(settings, sessionId);
setChatHistory([]);
};
return (
<div className="w-full flex justify-center">
<button
className="text-sm text-[#7A7D7E] hover:text-[#7A7D7E]/80 hover:underline"
onClick={() => handleChatReset()}
>
Reset Chat
</button>
</div>
);
}

View File

@ -7,7 +7,7 @@ export default function Sponsor({ settings }) {
href={settings.sponsorLink ?? "#"}
target="_blank"
rel="noreferrer"
className="text-xs text-gray-300 hover:text-blue-300 hover:underline"
className="text-xs text-[#0119D9] hover:text-[#0119D9]/80 hover:underline"
>
{settings.sponsorText}
</a>

View File

@ -20,6 +20,7 @@ const DEFAULT_SETTINGS = {
noSponsor: null, // Shows sponsor in footer of chat
sponsorText: "Powered by AnythingLLM", // default sponsor text
sponsorLink: "https://useanything.com", // default sponsor link
position: "bottom-right", // position of chat button/window
// behaviors
openOnLoad: "off", // or "on"

View File

@ -17,6 +17,6 @@ const scriptSettings = Object.assign(
);
export const embedderSettings = {
settings: scriptSettings,
USER_BACKGROUND_COLOR: `bg-[${scriptSettings?.userBgColor ?? "#2C2F35"}]`,
AI_BACKGROUND_COLOR: `bg-[${scriptSettings?.assistantBgColor ?? "#2563eb"}]`,
USER_STYLES: `bg-[${scriptSettings?.userBgColor ?? "#3DBEF5"}] text-white rounded-t-[18px] rounded-bl-[18px] rounded-br-[4px] mx-[20px]`,
ASSISTANT_STYLES: `bg-[${scriptSettings?.assistantBgColor ?? "#FFFFFF"}] text-[#222628] rounded-t-[18px] rounded-br-[18px] rounded-bl-[4px] mr-[37px] ml-[9px]`,
};

9
embed/src/utils/date.js Normal file
View File

@ -0,0 +1,9 @@
export function formatDate(sentAt) {
const date = new Date(sentAt * 1000);
const timeString = date.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
return timeString;
}

File diff suppressed because one or more lines are too long