mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -444,7 +444,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "مساحة العمل",
|
||||
chats: "المحادثات المرسلة",
|
||||
Active: "المجالات النشطة",
|
||||
active: "المجالات النشطة",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -440,7 +440,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "Arbeitsbereich",
|
||||
chats: "Gesendete Chats",
|
||||
Active: "Aktive Domains",
|
||||
active: "Aktive Domains",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -443,7 +443,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "Espacio de trabajo",
|
||||
chats: "Chats enviados",
|
||||
Active: "Dominios activos",
|
||||
active: "Dominios activos",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -436,7 +436,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "فضای کاری",
|
||||
chats: "گفتگوهای ارسال شده",
|
||||
Active: "دامنههای فعال",
|
||||
active: "دامنههای فعال",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -431,7 +431,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "סביבת עבודה",
|
||||
chats: "שיחות שנשלחו",
|
||||
Active: "תחומים פעילים",
|
||||
active: "תחומים פעילים",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -442,7 +442,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "Area di lavoro",
|
||||
chats: "Chat inviate",
|
||||
Active: "Domini attivi",
|
||||
active: "Domini attivi",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -442,7 +442,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "ワークスペース",
|
||||
chats: "送信済みチャット",
|
||||
Active: "有効なドメイン",
|
||||
active: "有効なドメイン",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -430,7 +430,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "워크스페이스",
|
||||
chats: "보낸 채팅",
|
||||
Active: "활성 도메인",
|
||||
active: "활성 도메인",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -439,7 +439,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "Werkruimte",
|
||||
chats: "Verzonden Chats",
|
||||
Active: "Actieve Domeinen",
|
||||
active: "Actieve Domeinen",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -615,7 +615,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "Workspace",
|
||||
chats: "Chats Enviados",
|
||||
Active: "Domínios Ativos",
|
||||
active: "Domínios Ativos",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -448,7 +448,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "Рабочее пространство",
|
||||
chats: "Отправленные чаты",
|
||||
Active: "Активные домены",
|
||||
active: "Активные домены",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -438,7 +438,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "Workspace",
|
||||
chats: "Sent Chats",
|
||||
Active: "Active Domains",
|
||||
active: "Active Domains",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -590,7 +590,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "工作区",
|
||||
chats: "已发送聊天",
|
||||
Active: "活动域",
|
||||
active: "活动域",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -423,7 +423,8 @@ const TRANSLATIONS = {
|
||||
table: {
|
||||
workspace: "工作區",
|
||||
chats: "已傳送對話",
|
||||
Active: "已啟用網域",
|
||||
active: "已啟用網域",
|
||||
created: null,
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 →
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
156
frontend/src/pages/GeneralSettings/ChatEmbedWidgets/index.jsx
Normal file
156
frontend/src/pages/GeneralSettings/ChatEmbedWidgets/index.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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`;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user