[FEAT] Implement new designs for embed widget ()

* 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
embed
README.mdindex.html
src
App.jsx
assets
components
ChatWindow
ChatContainer
ChatHistory
HistoricalMessage
PromptReply
index.jsx
PromptInput
Header
index.jsx
Head.jsx
ResetChat
Sponsor
hooks
main.jsx
utils
frontend/public/embed

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

(image error) 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