mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2025-04-17 18:18:11 +00:00
[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:
parent
df977e5177
commit
335ac43a5a
17 changed files with 417 additions and 248 deletions
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
5
embed/src/assets/anything-llm-icon.svg
Normal file
5
embed/src/assets/anything-llm-icon.svg
Normal file
File diff suppressed because one or more lines are too long
After (image error) Size: 6.3 KiB |
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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() {
|
||||
|
|
19
embed/src/components/ResetChat/index.jsx
Normal file
19
embed/src/components/ResetChat/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
9
embed/src/utils/date.js
Normal 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
Loading…
Add table
Reference in a new issue