mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-13 02:00:10 +01:00
Admin Embed Chats mgmt page
This commit is contained in:
parent
b219c5df0e
commit
8e0b08ecad
@ -44,7 +44,7 @@ const DataConnectorSetup = lazy(
|
||||
const EmbedConfigSetup = lazy(
|
||||
() => import("@/pages/GeneralSettings/EmbedConfigs")
|
||||
);
|
||||
const EmbedChats = lazy(() => import("@/pages/Admin/Users"));
|
||||
const EmbedChats = lazy(() => import("@/pages/GeneralSettings/EmbedChats"));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
|
@ -52,6 +52,29 @@ const Embed = {
|
||||
return { success: true, error: e.message };
|
||||
});
|
||||
},
|
||||
chats: async (offset = 0) => {
|
||||
return await fetch(`${API_BASE}/embed/chats`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ offset }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return [];
|
||||
});
|
||||
},
|
||||
deleteChat: async (chatId) => {
|
||||
return await fetch(`${API_BASE}/embed/chats/${chatId}`, {
|
||||
method: "DELETE",
|
||||
headers: baseHeaders(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return { success: false, error: e.message };
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default Embed;
|
||||
|
130
frontend/src/pages/GeneralSettings/EmbedChats/ChatRow/index.jsx
Normal file
130
frontend/src/pages/GeneralSettings/EmbedChats/ChatRow/index.jsx
Normal file
@ -0,0 +1,130 @@
|
||||
import { useRef } from "react";
|
||||
import truncate from "truncate";
|
||||
import { X, Trash, LinkSimple } from "@phosphor-icons/react";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import paths from "@/utils/paths";
|
||||
import Embed from "@/models/embed";
|
||||
|
||||
export default function ChatRow({ chat }) {
|
||||
const rowRef = useRef(null);
|
||||
const {
|
||||
isOpen: isPromptOpen,
|
||||
openModal: openPromptModal,
|
||||
closeModal: closePromptModal,
|
||||
} = useModal();
|
||||
const {
|
||||
isOpen: isResponseOpen,
|
||||
openModal: openResponseModal,
|
||||
closeModal: closeResponseModal,
|
||||
} = useModal();
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Are you sure you want to delete this chat?\n\nThis action is irreversible.`
|
||||
)
|
||||
)
|
||||
return false;
|
||||
rowRef?.current?.remove();
|
||||
await Embed.deleteChat(chat.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
ref={rowRef}
|
||||
className="bg-transparent text-white text-opacity-80 text-sm font-medium"
|
||||
>
|
||||
<td className="px-6 py-4 font-medium whitespace-nowrap text-white">
|
||||
<a
|
||||
href={paths.settings.embedSetup()}
|
||||
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 className="px-6 py-4 font-medium whitespace-nowrap text-white">
|
||||
<div className="flex flex-col">
|
||||
<p>{truncate(chat.session_id, 20)}</p>
|
||||
<ConnectionDetails
|
||||
connection_information={chat.connection_information}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
onClick={openPromptModal}
|
||||
className="px-6 py-4 border-transparent cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
|
||||
>
|
||||
{truncate(chat.prompt, 40)}
|
||||
</td>
|
||||
<td
|
||||
onClick={openResponseModal}
|
||||
className="px-6 py-4 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg"
|
||||
>
|
||||
{truncate(JSON.parse(chat.response)?.text, 40)}
|
||||
</td>
|
||||
<td className="px-6 py-4">{chat.createdAt}</td>
|
||||
<td className="px-6 py-4 flex items-center gap-x-6">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="font-medium text-red-300 px-2 py-1 rounded-lg hover:bg-red-800 hover:bg-opacity-20"
|
||||
>
|
||||
<Trash className="h-5 w-5" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<ModalWrapper isOpen={isPromptOpen}>
|
||||
<TextPreview text={chat.prompt} closeModal={closePromptModal} />
|
||||
</ModalWrapper>
|
||||
<ModalWrapper isOpen={isResponseOpen}>
|
||||
<TextPreview
|
||||
text={JSON.parse(chat.response)?.text}
|
||||
closeModal={closeResponseModal}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const TextPreview = ({ text, closeModal }) => {
|
||||
return (
|
||||
<div className="relative w-full md:max-w-2xl max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-600">
|
||||
<h3 className="text-xl font-semibold text-white">Viewing Text</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full p-6">
|
||||
<pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 border border-gray-500 text-white text-sm">
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectionDetails = ({ connection_information }) => {
|
||||
let details = {};
|
||||
try {
|
||||
details = JSON.parse(connection_information);
|
||||
} catch {}
|
||||
|
||||
if (Object.keys(details).length === 0) return null;
|
||||
return (
|
||||
<>
|
||||
{details.ip && <p className="text-xs text-slate-400">{details.ip}</p>}
|
||||
{details.host && <p className="text-xs text-slate-400">{details.host}</p>}
|
||||
</>
|
||||
);
|
||||
};
|
124
frontend/src/pages/GeneralSettings/EmbedChats/index.jsx
Normal file
124
frontend/src/pages/GeneralSettings/EmbedChats/index.jsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar, { SidebarMobileHeader } 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";
|
||||
|
||||
export default function EmbedChats() {
|
||||
// TODO: Add export of embed chats
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||
{!isMobile && <Sidebar />}
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
|
||||
>
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||
<div className="items-center flex gap-x-4">
|
||||
<p className="text-2xl font-semibold text-white">Embed Chats</p>
|
||||
</div>
|
||||
<p className="text-sm font-base text-white text-opacity-60">
|
||||
These are all the recorded chats and messages from any embed that
|
||||
you have published.
|
||||
</p>
|
||||
</div>
|
||||
<ChatsContainer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 handlePrevious = () => {
|
||||
setOffset(Math.max(offset - 1, 0));
|
||||
};
|
||||
const handleNext = () => {
|
||||
setOffset(offset + 1);
|
||||
};
|
||||
|
||||
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="#3D4147"
|
||||
baseColor="#2C2F35"
|
||||
count={1}
|
||||
className="w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6"
|
||||
containerClassName="flex w-full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className="w-full text-sm text-left rounded-lg mt-5">
|
||||
<thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 rounded-tl-lg">
|
||||
Embed
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Sender
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Message
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Response
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3">
|
||||
Sent 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} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex w-full justify-between items-center">
|
||||
<button
|
||||
onClick={handlePrevious}
|
||||
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
|
||||
disabled={offset === 0}
|
||||
>
|
||||
{" "}
|
||||
Previous Page
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible"
|
||||
disabled={!canNext}
|
||||
>
|
||||
Next Page
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
const { EmbedChats } = require("../models/embedChats");
|
||||
const { EmbedConfig } = require("../models/embedConfig");
|
||||
const { reqBody, userFromSession } = require("../utils/http");
|
||||
const {
|
||||
validEmbedConfig,
|
||||
validEmbedConfigId,
|
||||
} = require("../utils/middleware/embedMiddleware");
|
||||
const { validEmbedConfigId } = require("../utils/middleware/embedMiddleware");
|
||||
const {
|
||||
flexUserRoleValid,
|
||||
ROLES,
|
||||
@ -80,14 +77,14 @@ function embedManagementEndpoints(app) {
|
||||
app.post(
|
||||
"/embed/chats",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
||||
async (_, response) => {
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { offset = 0, limit = 20 } = reqBody(request);
|
||||
const embedChats = await EmbedChats.whereWithEmbed(
|
||||
const embedChats = await EmbedChats.whereWithEmbedAndWorkspace(
|
||||
{},
|
||||
limit,
|
||||
offset * limit,
|
||||
{ id: "desc" }
|
||||
{ id: "desc" },
|
||||
offset * limit
|
||||
);
|
||||
const totalChats = await EmbedChats.count();
|
||||
const hasPages = totalChats > (offset + 1) * limit;
|
||||
@ -98,6 +95,21 @@ function embedManagementEndpoints(app) {
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/embed/chats/:chatId",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { chatId } = request.params;
|
||||
await EmbedChats.delete({ id: Number(chatId) });
|
||||
response.status(200).json({ success: true, error: null });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { embedManagementEndpoints };
|
||||
|
@ -115,7 +115,7 @@ const EmbedChats = {
|
||||
}
|
||||
},
|
||||
|
||||
whereWithEmbed: async function (
|
||||
whereWithEmbedAndWorkspace: async function (
|
||||
clause = {},
|
||||
limit = null,
|
||||
orderBy = null,
|
||||
@ -124,7 +124,17 @@ const EmbedChats = {
|
||||
try {
|
||||
const chats = await prisma.embed_chats.findMany({
|
||||
where: clause,
|
||||
include: { embed_config: true },
|
||||
include: {
|
||||
embed_config: {
|
||||
select: {
|
||||
workspace: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...(limit !== null ? { take: limit } : {}),
|
||||
...(offset !== null ? { skip: offset } : {}),
|
||||
...(orderBy !== null ? { orderBy } : {}),
|
||||
|
@ -21,9 +21,8 @@ async function validEmbedConfig(request, response, next) {
|
||||
|
||||
function setConnectionMeta(request, response, next) {
|
||||
response.locals.connection = {
|
||||
host: request.hostname,
|
||||
path: request.path,
|
||||
ip: request.ip,
|
||||
host: request.headers?.origin,
|
||||
ip: request?.ip,
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user