WIP on iframe embed support

This commit is contained in:
timothycarambat 2024-02-02 13:00:50 -08:00
parent 27381a612c
commit 083dc5bf1c
7 changed files with 142 additions and 7 deletions

View File

@ -3,7 +3,7 @@ import useSessionId from "@/hooks/useSessionId";
import useOpenChat from "@/hooks/useOpen";
import Head from "@/components/Head";
import OpenButton from "@/components/OpenButton";
import ChatWindow from "./components/ChatWindow";
import ChatWindow, { ChatWindowFull } from "./components/ChatWindow";
import { useEffect } from "react";
export default function App() {
@ -16,6 +16,22 @@ export default function App() {
}, [embedSettings.loaded]);
if (!embedSettings.loaded) return null;
if (embedSettings.iframe === 'enabled') {
return (
<>
<Head />
<div className={`w-screen h-screen bg-red-500`}>
<ChatWindowFull
settings={embedSettings}
sessionId={sessionId}
/>
</div>
</>
)
}
// Renders the default bubble chat if nothing is defined
return (
<>
<Head />
@ -25,11 +41,10 @@ export default function App() {
width: isChatOpen ? 320 : "auto",
height: isChatOpen ? "93vh" : "auto",
}}
className={`transition-all duration-300 ease-in-out ${
isChatOpen
? "max-w-md p-4 bg-white rounded-lg border shadow-lg w-72"
: "w-16 h-16 rounded-full"
}`}
className={`transition-all duration-300 ease-in-out ${isChatOpen
? "max-w-md p-4 bg-white rounded-lg border shadow-lg w-72"
: "w-16 h-16 rounded-full"
}`}
>
{isChatOpen && (
<ChatWindow

View File

@ -51,6 +51,46 @@ export default function ChatWindowHeader({
);
}
export function ChatFullWindowHeader({
sessionId,
settings = {},
iconUrl = null,
setChatHistory,
}) {
const [showingOptions, setShowOptions] = useState(false);
const handleChatReset = async () => {
await ChatService.resetEmbedChatSession(settings, sessionId);
setChatHistory([]);
setShowOptions(false);
};
return (
<div className="flex justify-between items-center relative">
<img
style={{ maxWidth: 100, maxHeight: 20 }}
src={iconUrl ?? AnythingLLMLogo}
alt={iconUrl ? "Brand" : "AnythingLLM Logo"}
/>
<div className="flex gap-x-1 items-center">
{settings.loaded && (
<button
type="button"
onClick={() => setShowOptions(!showingOptions)}
className="hover:bg-gray-100 rounded-sm text-slate-800"
>
<DotsThreeOutlineVertical
size={18}
weight={!showingOptions ? "regular" : "fill"}
/>
</button>
)}
</div>
<OptionsMenu showing={showingOptions} resetChat={handleChatReset} />
</div>
);
}
function OptionsMenu({ showing, resetChat }) {
if (!showing) return null;
return (

View File

@ -1,4 +1,4 @@
import ChatWindowHeader from "./Header";
import ChatWindowHeader, { ChatFullWindowHeader } from "./Header";
import SessionId from "../SessionId";
import useChatHistory from "@/hooks/chat/useChatHistory";
import ChatContainer from "./ChatContainer";
@ -30,6 +30,33 @@ export default function ChatWindow({ closeChat, settings, sessionId }) {
);
}
export function ChatWindowFull({ settings, sessionId }) {
const { chatHistory, setChatHistory, loading } = useChatHistory(
settings,
sessionId
);
if (loading) return null;
setEventDelegatorForCodeSnippets();
return (
<div className="flex flex-col">
<ChatFullWindowHeader
sessionId={sessionId}
settings={settings}
iconUrl={settings.brandImageUrl}
setChatHistory={setChatHistory}
/>
{/* <ChatContainer
sessionId={sessionId}
settings={settings}
knownHistory={chatHistory}
/>
<SessionId /> */}
</div>
);
}
// Enables us to safely markdown and sanitize all responses without risk of injection
// but still be able to attach a handler to copy code snippets on all elements
// that are code snippets.

View File

@ -17,6 +17,7 @@ const DEFAULT_SETTINGS = {
// behaviors
openOnLoad: "off", // or "on"
iframe: null, // 'enabled' is the only valid option
};
export default function useGetScriptAttributes() {

View File

@ -19,6 +19,8 @@ const { utilEndpoints } = require("./endpoints/utils");
const { developerEndpoints } = require("./endpoints/api");
const { extensionEndpoints } = require("./endpoints/extensions");
const { bootHTTP, bootSSL } = require("./utils/boot");
const { renderIFrameResponse } = require("./utils/embed/render");
const { validEmbedConfigUUID } = require("./utils/middleware/embedMiddleware");
const app = express();
const apiRouter = express.Router();
const FILE_LIMIT = "3GB";
@ -90,6 +92,10 @@ if (process.env.NODE_ENV !== "development") {
});
}
// Named embedded to not collide with public folder /embed/ directory
// when in production
app.get("/embedded/:embedId", [validEmbedConfigUUID], renderIFrameResponse);
app.all("*", function (_, response) {
response.sendStatus(404);
});

View File

@ -0,0 +1,32 @@
const HTML = ({ scriptLoc }) => `<!doctype html>
<html lang="en">
<body>
<script
data-embed-id="example-uuid"
data-base-api-url='/api/embed'
data-iframe='enabled'
src="${scriptLoc}">
</script>
</body>
</html>`
function renderIFrameResponse(request, response) {
const embed = response.locals.embedConfig;
const settings = {
scriptLoc:
process.env.NODE_ENV === 'production' ?
`/embed/anythingllm-chat-widget.min.js` :
'http://localhost:3080/dist/anythingllm-chat-widget.js', // Running yarn dev in embed folder
embedId: embed.id,
}
response.type('text/html');
response.header('Access-Control-Allow-Origin', '*');
response.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
response.header('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, Authorization');
response.send(HTML(settings)).end();
}
module.exports = {
renderIFrameResponse
}

View File

@ -27,6 +27,19 @@ function setConnectionMeta(request, response, next) {
next();
}
async function validEmbedConfigUUID(request, response, next) {
const { embedId } = request.params;
const embed = await EmbedConfig.getWithWorkspace({ uuid: embedId });
if (!embed) {
response.sendStatus(404).end();
return;
}
response.locals.embedConfig = embed;
next();
}
async function validEmbedConfigId(request, response, next) {
const { embedId } = request.params;
@ -147,5 +160,6 @@ module.exports = {
setConnectionMeta,
validEmbedConfig,
validEmbedConfigId,
validEmbedConfigUUID,
canRespond,
};