New embedded chat widget UI/UX (#3899)

* wip create skeleton for new embed chat ui/ux

* update ui for embed chats

* lint

* update sidebar/paths

* remove old embed pages

* patch broken link

* add created timestamp to differentiate embeds
update translation key to lowercase
add created at translation key

* update text colors

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield
2025-06-03 16:21:20 -07:00
committed by GitHub
parent 0c9817ad59
commit c69cb32ea6
35 changed files with 600 additions and 420 deletions

View File

@@ -63,10 +63,10 @@ const GeneralBrowserExtension = lazy(
() => import("@/pages/GeneralSettings/BrowserExtensionApiKey")
);
const WorkspaceSettings = lazy(() => import("@/pages/WorkspaceSettings"));
const EmbedConfigSetup = lazy(
() => import("@/pages/GeneralSettings/EmbedConfigs")
const ChatEmbedWidgets = lazy(
() => import("@/pages/GeneralSettings/ChatEmbedWidgets")
);
const EmbedChats = lazy(() => import("@/pages/GeneralSettings/EmbedChats"));
const PrivacyAndData = lazy(
() => import("@/pages/GeneralSettings/PrivacyAndData")
);
@@ -178,12 +178,8 @@ export default function App() {
element={<AdminRoute Component={AdminLogs} />}
/>
<Route
path="/settings/embed-config"
element={<AdminRoute Component={EmbedConfigSetup} />}
/>
<Route
path="/settings/embed-chats"
element={<AdminRoute Component={EmbedChats} />}
path="/settings/embed-chat-widgets"
element={<AdminRoute Component={ChatEmbedWidgets} />}
/>
{/* Manager */}
<Route

View File

@@ -348,14 +348,8 @@ const SidebarOptions = ({ user = null, t }) => (
childOptions={[
{
hidden: !canViewChatHistory,
btnText: t("settings.embed-chats"),
href: paths.settings.embedChats(),
flex: true,
roles: ["admin"],
},
{
btnText: t("settings.embeds"),
href: paths.settings.embedSetup(),
href: paths.settings.embedChatWidgets(),
flex: true,
roles: ["admin"],
},

View File

@@ -94,6 +94,14 @@
--theme-attachment-icon: #ffffff;
--theme-attachment-icon-spinner: #ffffff;
--theme-attachment-icon-spinner-bg: #27282a;
--theme-button-text: #a8a9ab;
--theme-button-code-hover-text: #7cd4fd;
--theme-button-code-hover-bg: #22343f;
--theme-button-disable-hover-text: #fec84b;
--theme-button-disable-hover-bg: #3a3128;
--theme-button-delete-hover-text: #f97066;
--theme-button-delete-hover-bg: #37282b;
}
[data-theme="light"] {
@@ -187,6 +195,14 @@
--theme-attachment-icon: #ffffff;
--theme-attachment-icon-spinner: #7cd4fd;
--theme-attachment-icon-spinner-bg: #ffffff;
--theme-button-text: #a8a9ab;
--theme-button-code-hover-text: #0ba5ec;
--theme-button-code-hover-bg: #e8f7fe;
--theme-button-disable-hover-text: #854708;
--theme-button-disable-hover-bg: #fef7e6;
--theme-button-delete-hover-text: #b42318;
--theme-button-delete-hover-bg: #fee4e2;
}
[data-theme="light"] .text-white {

View File

@@ -444,7 +444,8 @@ const TRANSLATIONS = {
table: {
workspace: "مساحة العمل",
chats: "المحادثات المرسلة",
Active: "المجالات النشطة",
active: "المجالات النشطة",
created: null,
},
},
"embed-chats": {

View File

@@ -446,7 +446,8 @@ const TRANSLATIONS = {
table: {
workspace: "Arbejdsområde",
chats: "Sendte chats",
Active: "Aktive domæner",
active: "Aktive domæner",
created: null,
},
},
"embed-chats": {

View File

@@ -440,7 +440,8 @@ const TRANSLATIONS = {
table: {
workspace: "Arbeitsbereich",
chats: "Gesendete Chats",
Active: "Aktive Domains",
active: "Aktive Domains",
created: null,
},
},
"embed-chats": {

View File

@@ -663,12 +663,13 @@ const TRANSLATIONS = {
table: {
workspace: "Workspace",
chats: "Sent Chats",
Active: "Active Domains",
active: "Active Domains",
created: "Created",
},
},
"embed-chats": {
title: "Embed Chats",
title: "Embed Chat History",
export: "Export",
description:
"These are all the recorded chats and messages from any embed that you have published.",

View File

@@ -443,7 +443,8 @@ const TRANSLATIONS = {
table: {
workspace: "Espacio de trabajo",
chats: "Chats enviados",
Active: "Dominios activos",
active: "Dominios activos",
created: null,
},
},
"embed-chats": {

View File

@@ -436,7 +436,8 @@ const TRANSLATIONS = {
table: {
workspace: "فضای کاری",
chats: "گفتگوهای ارسال شده",
Active: "دامنه‌های فعال",
active: "دامنه‌های فعال",
created: null,
},
},
"embed-chats": {

View File

@@ -444,7 +444,8 @@ const TRANSLATIONS = {
table: {
workspace: "Espace de travail",
chats: "Chats envoyés",
Active: "Domaines actifs",
active: "Domaines actifs",
created: null,
},
},
"embed-chats": {

View File

@@ -431,7 +431,8 @@ const TRANSLATIONS = {
table: {
workspace: "סביבת עבודה",
chats: "שיחות שנשלחו",
Active: "תחומים פעילים",
active: "תחומים פעילים",
created: null,
},
},
"embed-chats": {

View File

@@ -442,7 +442,8 @@ const TRANSLATIONS = {
table: {
workspace: "Area di lavoro",
chats: "Chat inviate",
Active: "Domini attivi",
active: "Domini attivi",
created: null,
},
},
"embed-chats": {

View File

@@ -442,7 +442,8 @@ const TRANSLATIONS = {
table: {
workspace: "ワークスペース",
chats: "送信済みチャット",
Active: "有効なドメイン",
active: "有効なドメイン",
created: null,
},
},
"embed-chats": {

View File

@@ -430,7 +430,8 @@ const TRANSLATIONS = {
table: {
workspace: "워크스페이스",
chats: "보낸 채팅",
Active: "활성 도메인",
active: "활성 도메인",
created: null,
},
},
"embed-chats": {

View File

@@ -630,7 +630,8 @@ const TRANSLATIONS = {
table: {
workspace: "Darba vieta",
chats: "Nosūtītie čati",
Active: "Aktīvie domēni",
active: "Aktīvie domēni",
created: null,
},
},
"embed-chats": {

View File

@@ -439,7 +439,8 @@ const TRANSLATIONS = {
table: {
workspace: "Werkruimte",
chats: "Verzonden Chats",
Active: "Actieve Domeinen",
active: "Actieve Domeinen",
created: null,
},
},
"embed-chats": {

View File

@@ -615,7 +615,8 @@ const TRANSLATIONS = {
table: {
workspace: "Workspace",
chats: "Chats Enviados",
Active: "Domínios Ativos",
active: "Domínios Ativos",
created: null,
},
},
"embed-chats": {

View File

@@ -448,7 +448,8 @@ const TRANSLATIONS = {
table: {
workspace: "Рабочее пространство",
chats: "Отправленные чаты",
Active: "Активные домены",
active: "Активные домены",
created: null,
},
},
"embed-chats": {

View File

@@ -439,7 +439,8 @@ const TRANSLATIONS = {
table: {
workspace: "Çalışma Alanı",
chats: "Gönderilen Sohbetler",
Active: "Aktif Alan Adları",
active: "Aktif Alan Adları",
created: null,
},
},
"embed-chats": {

View File

@@ -438,7 +438,8 @@ const TRANSLATIONS = {
table: {
workspace: "Workspace",
chats: "Sent Chats",
Active: "Active Domains",
active: "Active Domains",
created: null,
},
},
"embed-chats": {

View File

@@ -590,7 +590,8 @@ const TRANSLATIONS = {
table: {
workspace: "工作区",
chats: "已发送聊天",
Active: "活动域",
active: "活动域",
created: null,
},
},
"embed-chats": {

View File

@@ -423,7 +423,8 @@ const TRANSLATIONS = {
table: {
workspace: "工作區",
chats: "已傳送對話",
Active: "已啟用網域",
active: "已啟用網域",
created: null,
},
},
"embed-chats": {

View File

@@ -38,18 +38,17 @@ export default function ChatRow({ chat, onDelete }) {
<tr className="bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10">
<td className="px-6 font-medium whitespace-nowrap text-white">
<a
href={paths.settings.embedSetup()}
href={paths.settings.embedChatWidgets()}
target="_blank"
rel="noreferrer"
className="text-white flex items-center hover:underline"
>
<LinkSimple className="mr-2 w-5 h-5" />{" "}
{chat.embed_config.workspace.name}
</a>
</td>
<td
onClick={openConnectionDetailsModal}
className="px-6 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
className="px-6 cursor-pointer hover:shadow-lg"
>
<div className="flex flex-col">
<p>{truncate(chat.session_id, 20)}</p>
@@ -57,13 +56,13 @@ export default function ChatRow({ chat, onDelete }) {
</td>
<td
onClick={openPromptModal}
className="px-6 border-transparent cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
className="px-6 border-transparent cursor-pointer hover:shadow-lg"
>
{truncate(chat.prompt, 40)}
</td>
<td
onClick={openResponseModal}
className="px-6 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
className="px-6 cursor-pointer hover:shadow-lg"
>
{truncate(JSON.parse(chat.response)?.text, 40)}
</td>
@@ -71,9 +70,11 @@ export default function ChatRow({ chat, onDelete }) {
<td className="px-6 flex items-center gap-x-6 h-full mt-1">
<button
onClick={handleDelete}
className="text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10"
className="group text-xs font-medium text-theme-text-secondary px-2 py-1 rounded-lg hover:bg-theme-button-delete-hover-bg"
>
<Trash className="h-5 w-5" />
<span className="group-hover:text-theme-button-delete-hover-text">
Delete
</span>
</button>
</td>
</tr>

View File

@@ -0,0 +1,232 @@
import { useEffect, useState, useRef } from "react";
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow";
import Embed from "@/models/embed";
import { useTranslation } from "react-i18next";
import { CaretDown, Download } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
import { saveAs } from "file-saver";
import System from "@/models/system";
const exportOptions = {
csv: {
name: "CSV",
mimeType: "text/csv",
fileExtension: "csv",
filenameFunc: () => {
return `anythingllm-embed-chats-${new Date().toLocaleDateString()}`;
},
},
json: {
name: "JSON",
mimeType: "application/json",
fileExtension: "json",
filenameFunc: () => {
return `anythingllm-embed-chats-${new Date().toLocaleDateString()}`;
},
},
jsonl: {
name: "JSONL",
mimeType: "application/jsonl",
fileExtension: "jsonl",
filenameFunc: () => {
return `anythingllm-embed-chats-${new Date().toLocaleDateString()}-lines`;
},
},
jsonAlpaca: {
name: "JSON (Alpaca)",
mimeType: "application/json",
fileExtension: "json",
filenameFunc: () => {
return `anythingllm-embed-chats-${new Date().toLocaleDateString()}-alpaca`;
},
},
};
export default function EmbedChatsView() {
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef();
const openMenuButton = useRef();
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
const [chats, setChats] = useState([]);
const query = useQuery();
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false);
const handleDumpChats = async (exportType) => {
const chats = await System.exportChats(exportType, "embed");
if (!!chats) {
const { name, mimeType, fileExtension, filenameFunc } =
exportOptions[exportType];
const blob = new Blob([chats], { type: mimeType });
saveAs(blob, `${filenameFunc()}.${fileExtension}`);
showToast(`Embed chats exported successfully as ${name}.`, "success");
} else {
showToast("Failed to export embed chats.", "error");
}
};
const toggleMenu = () => {
setShowMenu(!showMenu);
};
useEffect(() => {
function handleClickOutside(event) {
if (
menuRef.current &&
!menuRef.current.contains(event.target) &&
!openMenuButton.current.contains(event.target)
) {
setShowMenu(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
useEffect(() => {
async function fetchChats() {
const { chats: _chats, hasPages = false } = await Embed.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
}
fetchChats();
}, [offset]);
const handlePrevious = () => {
setOffset(Math.max(offset - 1, 0));
};
const handleNext = () => {
setOffset(offset + 1);
};
const handleDeleteChat = (chatId) => {
setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId));
};
if (loading) {
return (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="var(--theme-bg-primary)"
baseColor="var(--theme-bg-secondary)"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm"
containerClassName="flex w-full"
/>
);
}
return (
<div className="flex flex-col w-full p-4 overflow-none">
<div className="w-full flex flex-col gap-y-1">
<div className="flex flex-wrap gap-4 items-center">
<p className="text-lg leading-6 font-bold text-theme-text-primary">
{t("embed-chats.title")}
</p>
<div className="relative">
<button
ref={openMenuButton}
onClick={toggleMenu}
className="flex items-center gap-x-2 px-4 py-1 rounded-lg text-theme-bg-chat bg-primary-button hover:bg-secondary hover:text-white text-xs font-semibold h-[34px] w-fit"
>
<Download size={18} weight="bold" />
{t("embed-chats.export")}
<CaretDown size={18} weight="bold" />
</button>
<div
ref={menuRef}
className={`${
showMenu ? "slide-down" : "slide-up hidden"
} z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary light:bg-theme-bg-secondary mt-2 shadow-md`}
>
<div className="py-2">
{Object.entries(exportOptions).map(([key, data]) => (
<button
key={key}
onClick={() => {
handleDumpChats(key);
setShowMenu(false);
}}
className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147] light:hover:bg-theme-sidebar-item-hover"
>
{data.name}
</button>
))}
</div>
</div>
</div>
</div>
<p className="text-xs leading-[18px] font-base text-theme-text-secondary mt-2">
{t("embed-chats.description")}
</p>
</div>
<div className="overflow-x-auto mt-6">
<table className="w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0">
<thead className="text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
{t("embed-chats.table.embed")}
</th>
<th scope="col" className="px-6 py-3">
{t("embed-chats.table.sender")}
</th>
<th scope="col" className="px-6 py-3">
{t("embed-chats.table.message")}
</th>
<th scope="col" className="px-6 py-3">
{t("embed-chats.table.response")}
</th>
<th scope="col" className="px-6 py-3">
{t("embed-chats.table.at")}
</th>
<th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "}
</th>
</tr>
</thead>
<tbody>
{chats.map((chat) => (
<ChatRow key={chat.id} chat={chat} onDelete={handleDeleteChat} />
))}
</tbody>
</table>
{(offset > 0 || canNext) && (
<div className="flex items-center justify-end gap-2 mt-4 pb-6">
<button
onClick={handlePrevious}
disabled={offset === 0}
className={`px-4 py-2 text-sm rounded-lg ${
offset === 0
? "bg-theme-bg-secondary text-theme-text-disabled cursor-not-allowed"
: "bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover"
}`}
>
{t("embed-chats.previous")}
</button>
<button
onClick={handleNext}
disabled={!canNext}
className={`px-4 py-2 text-sm rounded-lg ${
!canNext
? "bg-theme-bg-secondary text-theme-text-disabled cursor-not-allowed"
: "bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover"
}`}
>
{t("embed-chats.next")}
</button>
</div>
)}
</div>
</div>
);
}

View File

@@ -92,6 +92,7 @@ const ScriptTag = ({ embed }) => {
<a
href="https://github.com/Mintplex-Labs/anything-llm/tree/master/embed/README.md"
target="_blank"
rel="noreferrer"
className="text-blue-300 light:text-blue-500 hover:underline"
>
View all style and configuration options &rarr;

View File

@@ -1,5 +1,5 @@
import { useRef, useState } from "react";
import { DotsThreeOutline, LinkSimple, Trash } from "@phosphor-icons/react";
import { DotsThreeOutline } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
@@ -8,6 +8,7 @@ import paths from "@/utils/paths";
import { nFormatter } from "@/utils/numbers";
import EditEmbedModal from "./EditEmbedModal";
import CodeSnippetModal from "./CodeSnippetModal";
import moment from "moment";
export default function EmbedRow({ embed }) {
const rowRef = useRef(null);
@@ -75,7 +76,7 @@ export default function EmbedRow({ embed }) {
rel="noreferrer"
className="text-white flex items-center hover:underline"
>
<LinkSimple className="mr-2 w-5 h-5" /> {embed.workspace.name}
{embed.workspace.name}
</a>
</th>
<th scope="row" className="px-6 whitespace-nowrap">
@@ -84,30 +85,47 @@ export default function EmbedRow({ embed }) {
<th scope="row" className="px-6 whitespace-nowrap">
<ActiveDomains domainList={embed.allowlist_domains} />
</th>
<th
scope="row"
className="px-6 whitespace-nowrap text-theme-text-secondary !font-normal"
>
{
// If the embed was created more than a day ago, show the date, otherwise show the time ago
moment(embed.createdAt).diff(moment(), "days") > 0
? moment(embed.createdAt).format("MMM D, YYYY")
: moment(embed.createdAt).fromNow()
}
</th>
<td className="px-6 flex items-center gap-x-6 h-full mt-1">
<button
onClick={openSettingsModal}
className="text-xs font-medium text-white text-opacity-80 rounded-lg hover:text-white hover:light:text-gray-500 px-2 py-1 hover:text-opacity-60 hover:bg-white hover:bg-opacity-10"
>
<DotsThreeOutline weight="fill" className="h-5 w-5" />
</button>
<button
onClick={openSnippetModal}
className="text-xs font-medium text-blue-600 dark:text-blue-300 px-2 py-1 rounded-lg hover:bg-blue-50 hover:dark:bg-blue-800 hover:dark:bg-opacity-20"
className="group text-xs font-medium text-theme-text-secondary px-2 py-1 rounded-lg hover:bg-theme-button-code-hover-bg"
>
Show Code
<span className="group-hover:text-theme-button-code-hover-text">
Code
</span>
</button>
<button
onClick={handleSuspend}
className="text-xs font-medium text-orange-600 dark:text-orange-300 px-2 py-1 rounded-lg hover:bg-orange-50 hover:dark:bg-orange-800 hover:dark:bg-opacity-20"
className="group text-xs font-medium text-theme-text-secondary px-2 py-1 rounded-lg hover:bg-theme-button-disable-hover-bg"
>
{enabled ? "Disable" : "Enable"}
<span className="group-hover:text-theme-button-disable-hover-text">
{enabled ? "Disable" : "Enable"}
</span>
</button>
<button
onClick={handleDelete}
className="text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10"
className="group text-xs font-medium text-theme-text-secondary px-2 py-1 rounded-lg hover:bg-theme-button-delete-hover-bg"
>
<Trash className="h-5 w-5" />
<span className="group-hover:text-theme-button-delete-hover-text">
Delete
</span>
</button>
<button
onClick={openSettingsModal}
className="text-xs font-medium text-theme-button-text hover:text-theme-text-secondary hover:bg-theme-hover px-2 py-1 rounded-lg"
>
<DotsThreeOutline weight="fill" className="h-5 w-5" />
</button>
</td>
</tr>

View File

@@ -0,0 +1,97 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import { CodeBlock } from "@phosphor-icons/react";
import EmbedRow from "./EmbedRow";
import NewEmbedModal from "./NewEmbedModal";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
import Embed from "@/models/embed";
import CTAButton from "@/components/lib/CTAButton";
export default function EmbedConfigsView() {
const { isOpen, openModal, closeModal } = useModal();
const { t } = useTranslation();
const [loading, setLoading] = useState(true);
const [embeds, setEmbeds] = useState([]);
useEffect(() => {
async function fetchUsers() {
const _embeds = await Embed.embeds();
setEmbeds(_embeds);
setLoading(false);
}
fetchUsers();
}, []);
if (loading) {
return (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="var(--theme-bg-primary)"
baseColor="var(--theme-bg-secondary)"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm"
containerClassName="flex w-full"
/>
);
}
return (
<div className="flex flex-col w-full p-4">
<div className="w-full flex flex-col gap-y-1 pb-6">
<div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-theme-text-primary">
{t("embeddable.title")}
</p>
</div>
<div className="flex gap-x-10 mr-8">
<p className="text-xs leading-[18px] font-base text-theme-text-secondary mt-2">
{t("embeddable.description")}
</p>
<div>
<CTAButton onClick={openModal} className="text-theme-bg-chat">
<CodeBlock className="h-4 w-4" weight="bold" />{" "}
{t("embeddable.create")}
</CTAButton>
</div>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0">
<thead className="text-theme-text-secondary text-xs leading-[18px] uppercase border-white/10 border-b">
<tr>
<th scope="col" className="px-6 py-3">
{t("embeddable.table.workspace")}
</th>
<th scope="col" className="px-6 py-3">
{t("embeddable.table.chats")}
</th>
<th scope="col" className="px-6 py-3">
{t("embeddable.table.active")}
</th>
<th scope="col" className="px-6 py-3">
{t("embeddable.table.created")}
</th>
<th scope="col" className="px-6 py-3">
{" "}
</th>
</tr>
</thead>
<tbody>
{embeds.map((embed) => (
<EmbedRow key={embed.id} embed={embed} />
))}
</tbody>
</table>
</div>
<ModalWrapper isOpen={isOpen}>
<NewEmbedModal closeModal={closeModal} />
</ModalWrapper>
</div>
);
}

View File

@@ -0,0 +1,156 @@
import { useState } from "react";
import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import { CaretLeft, CaretRight } from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
import EmbedConfigsView from "./EmbedConfigs";
import EmbedChatsView from "./EmbedChats";
export default function ChatEmbedWidgets() {
const { t } = useTranslation();
const [selectedView, setSelectedView] = useState("configs");
const [showViewModal, setShowViewModal] = useState(false);
if (isMobile) {
return (
<WidgetLayout>
<div className="flex flex-col w-full p-4 mt-10">
<div
hidden={showViewModal}
className="flex flex-col gap-y-[18px] overflow-y-scroll no-scroll"
>
<div className="text-theme-text-primary flex items-center gap-x-2">
<p className="text-lg font-medium">Chat Embed</p>
</div>
<WidgetList
selectedView={selectedView}
handleClick={(view) => {
setSelectedView(view);
setShowViewModal(true);
}}
/>
</div>
{showViewModal && (
<div className="fixed top-0 left-0 w-full h-full bg-sidebar z-30">
<div className="flex flex-col h-full">
<div className="flex items-center p-4">
<button
type="button"
onClick={() => {
setShowViewModal(false);
setSelectedView("");
}}
className="text-white/60 hover:text-white transition-colors duration-200"
>
<div className="flex items-center text-sky-400">
<CaretLeft size={24} />
<div>Back</div>
</div>
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="bg-theme-bg-secondary text-white rounded-xl p-4 overflow-y-scroll no-scroll">
{selectedView === "configs" ? (
<EmbedConfigsView />
) : (
<EmbedChatsView />
)}
</div>
</div>
</div>
</div>
)}
</div>
</WidgetLayout>
);
}
return (
<WidgetLayout>
<div className="flex-1 flex gap-x-6 p-4 mt-10">
<div className="flex flex-col min-w-[360px] h-[calc(100vh-90px)]">
<div className="flex-none mb-4">
<div className="text-theme-text-primary flex items-center gap-x-2">
<p className="text-lg font-medium">Chat Embed</p>
</div>
</div>
<div className="flex-1 overflow-y-auto pr-2 pb-4">
<div className="space-y-4">
<WidgetList
selectedView={selectedView}
handleClick={setSelectedView}
/>
</div>
</div>
</div>
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-theme-bg-secondary text-white rounded-xl flex-1 p-4 overflow-y-scroll no-scroll">
{selectedView === "configs" ? (
<EmbedConfigsView />
) : (
<EmbedChatsView />
)}
</div>
</div>
</div>
</WidgetLayout>
);
}
function WidgetLayout({ children }) {
return (
<div
id="workspace-widget-settings-container"
className="w-screen h-screen overflow-hidden bg-theme-bg-container flex md:mt-0 mt-6"
>
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex"
>
{children}
</div>
</div>
);
}
function WidgetList({ selectedView, handleClick }) {
const views = {
configs: {
title: "Widgets",
},
chats: {
title: "History",
},
};
return (
<div
className={`bg-theme-bg-secondary text-white rounded-xl ${isMobile ? "w-full" : "min-w-[360px] w-fit"}`}
>
{Object.entries(views).map(([view, settings], index) => (
<div
key={view}
className={`py-3 px-4 flex items-center justify-between ${
index === 0 ? "rounded-t-xl" : ""
} ${
index === Object.keys(views).length - 1
? "rounded-b-xl"
: "border-b border-white/10"
} cursor-pointer transition-all duration-300 hover:bg-theme-bg-primary ${
selectedView === view ? "bg-white/10 light:bg-theme-bg-sidebar" : ""
}`}
onClick={() => handleClick?.(view)}
>
<div className="text-sm font-light">{settings.title}</div>
<CaretRight
size={14}
weight="bold"
className="text-theme-text-secondary"
/>
</div>
))}
</div>
);
}

View File

@@ -1,246 +0,0 @@
import { useEffect, useState, useRef } from "react";
import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import useQuery from "@/hooks/useQuery";
import ChatRow from "./ChatRow";
import Embed from "@/models/embed";
import { useTranslation } from "react-i18next";
import { CaretDown, Download } from "@phosphor-icons/react";
import showToast from "@/utils/toast";
import { saveAs } from "file-saver";
import System from "@/models/system";
import { CanViewChatHistory } from "@/components/CanViewChatHistory";
const exportOptions = {
csv: {
name: "CSV",
mimeType: "text/csv",
fileExtension: "csv",
filenameFunc: () => {
return `anythingllm-embed-chats-${new Date().toLocaleDateString()}`;
},
},
json: {
name: "JSON",
mimeType: "application/json",
fileExtension: "json",
filenameFunc: () => {
return `anythingllm-embed-chats-${new Date().toLocaleDateString()}`;
},
},
jsonl: {
name: "JSONL",
mimeType: "application/jsonl",
fileExtension: "jsonl",
filenameFunc: () => {
return `anythingllm-embed-chats-${new Date().toLocaleDateString()}-lines`;
},
},
jsonAlpaca: {
name: "JSON (Alpaca)",
mimeType: "application/json",
fileExtension: "json",
filenameFunc: () => {
return `anythingllm-embed-chats-${new Date().toLocaleDateString()}-alpaca`;
},
},
};
export default function EmbedChats() {
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef();
const openMenuButton = useRef();
const { t } = useTranslation();
const handleDumpChats = async (exportType) => {
const chats = await System.exportChats(exportType, "embed");
if (!!chats) {
const { name, mimeType, fileExtension, filenameFunc } =
exportOptions[exportType];
const blob = new Blob([chats], { type: mimeType });
saveAs(blob, `${filenameFunc()}.${fileExtension}`);
showToast(`Embed chats exported successfully as ${name}.`, "success");
} else {
showToast("Failed to export embed chats.", "error");
}
};
const toggleMenu = () => {
setShowMenu(!showMenu);
};
useEffect(() => {
function handleClickOutside(event) {
if (
menuRef.current &&
!menuRef.current.contains(event.target) &&
!openMenuButton.current.contains(event.target)
) {
setShowMenu(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<CanViewChatHistory>
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0"
>
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2">
<div className="flex flex-wrap gap-4 items-center">
<p className="text-lg leading-6 font-bold text-theme-text-primary">
{t("embed-chats.title")}
</p>
<div className="relative">
<button
ref={openMenuButton}
onClick={toggleMenu}
className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:light:bg-theme-bg-primary hover:text-theme-text-primary text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit"
>
<Download size={18} weight="bold" />
{t("embed-chats.export")}
<CaretDown size={18} weight="bold" />
</button>
<div
ref={menuRef}
className={`${
showMenu ? "slide-down" : "slide-up hidden"
} z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary light:bg-theme-bg-secondary mt-2 shadow-md`}
>
<div className="py-2">
{Object.entries(exportOptions).map(([key, data]) => (
<button
key={key}
onClick={() => {
handleDumpChats(key);
setShowMenu(false);
}}
className="w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147] light:hover:bg-theme-sidebar-item-hover"
>
{data.name}
</button>
))}
</div>
</div>
</div>
</div>
<p className="text-xs leading-[18px] font-base text-theme-text-secondary mt-2">
{t("embed-chats.description")}
</p>
</div>
<div className="overflow-x-auto mt-6">
<ChatsContainer />
</div>
</div>
</div>
</div>
</CanViewChatHistory>
);
}
function ChatsContainer() {
const query = useQuery();
const [loading, setLoading] = useState(true);
const [chats, setChats] = useState([]);
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false);
const { t } = useTranslation();
const handlePrevious = () => {
setOffset(Math.max(offset - 1, 0));
};
const handleNext = () => {
setOffset(offset + 1);
};
const handleDeleteChat = (chatId) => {
setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId));
};
useEffect(() => {
async function fetchChats() {
const { chats: _chats, hasPages = false } = await Embed.chats(offset);
setChats(_chats);
setCanNext(hasPages);
setLoading(false);
}
fetchChats();
}, [offset]);
if (loading) {
return (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="var(--theme-bg-primary)"
baseColor="var(--theme-bg-secondary)"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm"
containerClassName="flex w-full"
/>
);
}
return (
<>
<table className="w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0">
<thead className="text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
{t("embed-chats.table.embed")}
</th>
<th scope="col" className="px-6 py-3">
{t("embed-chats.table.sender")}
</th>
<th scope="col" className="px-6 py-3">
{t("embed-chats.table.message")}
</th>
<th scope="col" className="px-6 py-3">
{t("embed-chats.table.response")}
</th>
<th scope="col" className="px-6 py-3">
{t("embed-chats.table.at")}
</th>
<th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "}
</th>
</tr>
</thead>
<tbody>
{!!chats &&
chats.map((chat) => (
<ChatRow key={chat.id} chat={chat} onDelete={handleDeleteChat} />
))}
</tbody>
</table>
<div className="flex w-full justify-between items-center mt-6">
<button
onClick={handlePrevious}
className="px-4 py-2 rounded-lg border border-theme-text-secondary text-theme-text-secondary text-sm items-center flex gap-x-2 hover:bg-theme-text-secondary hover:text-theme-bg-secondary disabled:invisible"
disabled={offset === 0}
>
{" "}
{t("common.previous")}
</button>
<button
onClick={handleNext}
className="px-4 py-2 rounded-lg border border-theme-text-secondary text-theme-text-secondary text-sm items-center flex gap-x-2 hover:bg-theme-text-secondary hover:text-theme-bg-secondary disabled:invisible"
disabled={!canNext}
>
{t("common.next")}
</button>
</div>
</>
);
}

View File

@@ -1,110 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import { CodeBlock } from "@phosphor-icons/react";
import EmbedRow from "./EmbedRow";
import NewEmbedModal from "./NewEmbedModal";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
import Embed from "@/models/embed";
import CTAButton from "@/components/lib/CTAButton";
export default function EmbedConfigs() {
const { isOpen, openModal, closeModal } = useModal();
const { t } = useTranslation();
return (
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
<Sidebar />
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0"
>
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2">
<div className="items-center flex gap-x-4">
<p className="text-lg leading-6 font-bold text-theme-text-primary">
{t("embeddable.title")}
</p>
</div>
<p className="text-xs leading-[18px] font-base text-theme-text-secondary mt-2">
{t("embeddable.description")}
</p>
</div>
<div className="w-full justify-end flex">
<CTAButton
onClick={openModal}
className="mt-3 mr-0 mb-4 md:-mb-14 z-10"
>
<CodeBlock className="h-4 w-4" weight="bold" />{" "}
{t("embeddable.create")}
</CTAButton>
</div>
<div className="overflow-x-auto mt-6">
<EmbedContainer />
</div>
</div>
<ModalWrapper isOpen={isOpen}>
<NewEmbedModal closeModal={closeModal} />
</ModalWrapper>
</div>
</div>
);
}
function EmbedContainer() {
const [loading, setLoading] = useState(true);
const [embeds, setEmbeds] = useState([]);
const { t } = useTranslation();
useEffect(() => {
async function fetchUsers() {
const _embeds = await Embed.embeds();
setEmbeds(_embeds);
setLoading(false);
}
fetchUsers();
}, []);
if (loading) {
return (
<Skeleton.default
height="80vh"
width="100%"
highlightColor="var(--theme-bg-primary)"
baseColor="var(--theme-bg-secondary)"
count={1}
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm"
containerClassName="flex w-full"
/>
);
}
return (
<table className="w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0">
<thead className="text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b">
<tr>
<th scope="col" className="px-6 py-3 rounded-tl-lg">
{t("embeddable.table.workspace")}
</th>
<th scope="col" className="px-6 py-3">
{t("embeddable.table.chats")}
</th>
<th scope="col" className="px-6 py-3">
{t("embeddable.table.Active")}
</th>
<th scope="col" className="px-6 py-3 rounded-tr-lg">
{" "}
</th>
</tr>
</thead>
<tbody>
{embeds.map((embed) => (
<EmbedRow key={embed.id} embed={embed} />
))}
</tbody>
</table>
);
}

View File

@@ -147,11 +147,8 @@ export default {
privacy: () => {
return "/settings/privacy";
},
embedSetup: () => {
return `/settings/embed-config`;
},
embedChats: () => {
return `/settings/embed-chats`;
embedChatWidgets: () => {
return `/settings/embed-chat-widgets`;
},
browserExtension: () => {
return `/settings/browser-extension`;

View File

@@ -142,7 +142,16 @@ export default {
"button-text": 'var(--theme-checklist-button-text)',
"button-hover-bg": 'var(--theme-checklist-button-hover-bg)',
"button-hover-border": 'var(--theme-checklist-button-hover-border)',
}
},
button: {
text: 'var(--theme-button-text)',
'code-hover-text': 'var(--theme-button-code-hover-text)',
'code-hover-bg': 'var(--theme-button-code-hover-bg)',
'disable-hover-text': 'var(--theme-button-disable-hover-text)',
'disable-hover-bg': 'var(--theme-button-disable-hover-bg)',
'delete-hover-text': 'var(--theme-button-delete-hover-text)',
'delete-hover-bg': 'var(--theme-button-delete-hover-bg)',
},
},
},
backgroundImage: {

View File

@@ -27,7 +27,7 @@
"generate:cloudformation": "node cloud-deployments/aws/cloudformation/generate.mjs",
"generate::gcp_deployment": "node cloud-deployments/gcp/deployment/generate.mjs",
"verify:translations": "cd frontend/src/locales && node verifyTranslations.mjs",
"normalize:translations": "cd frontend/src/locales && node normalizeEn.mjs"
"normalize:translations": "cd frontend/src/locales && node normalizeEn.mjs && cd ../../.. && yarn lint && yarn verify:translations"
},
"private": false,
"devDependencies": {