Implement new home page redesign (#4931)
* remove legacy home page components, update home page to new layout * update PromptInput component styles to match new designs, make quick action buttons functional * home page chat creates new thread in last used workspace * fix slash commands and agent popup on home page * disable llm workspace selector action in home page * add drag and drop file support to home page * fix behavior of drag and drop on home page * handle pasting attachments in home page * update empty state of workspace chat to use new ui * update empty workspace ui to match home page design, fix flickering loading states * convert quick action buttons to component, add to empty state ws chat * fix hover state light mode in quick actions * add suggested messages subcomponent to empty ws/thread * adjust width, rounded edges of prompt input * only show quick actions for admin/manager role * fix hover states for quick actions and suggested messages component * make upload document quick action trigger parsed document upload * fix mic behavior in homepage, ws chat, ws thread chat * fix margin between prompt input and quick actions * Simplify message presets by removing heading input (#4915) * Remove heading input from message presets, merge legacy headings on edit * filter out empty messages from state after saving * mark form as dirty on input change * styling --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com> * convert SuggestedMessages to component, render SuggestedMessages in home page to target ws * fix broken handleMessageChange reference * add translations for QuickActions * lint * fix home page chat submission broken by PromptInput onChange removal * fix prompt input remount race condition, home page suggested message flicker * remove unused handleSendSuggestedMessage from ChatHistory * add greeting text to main-page translations, remove defaults * fix file deletion in parsed files menu on home page * add virtual thread sidebar state and workspace indicator on home page * show workspace llm selector on home page when workspace exists * show home page for all user roles with rbac quick actions * fix positioning of agent and slash command popups * remove workspace indicator from home page, match empty state spacing * Normalize translations for home page redesign (#4986) * normalize translations * update translations with DMR * accidentally changed es translation * normalize translations for main-page.greeting * update translations with DMR --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com> * update translations * create new workspace in native language Cleanup workspace page from empty state handling * update quick action show logic * fix send button --------- Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
parent
907bd09faf
commit
d325b07182
@ -24,12 +24,15 @@ export default function ThreadItem({
|
||||
hasNext,
|
||||
ctrlPressed = false,
|
||||
}) {
|
||||
const { slug, threadSlug = null } = useParams();
|
||||
const { slug: urlSlug, threadSlug = null } = useParams();
|
||||
const workspaceSlug = workspace?.slug ?? urlSlug;
|
||||
const optionsContainer = useRef(null);
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
const linkTo = !thread.slug
|
||||
? paths.workspace.chat(slug)
|
||||
: paths.workspace.thread(slug, thread.slug);
|
||||
const linkTo = thread.virtual
|
||||
? "/"
|
||||
: !thread.slug
|
||||
? paths.workspace.chat(workspaceSlug)
|
||||
: paths.workspace.thread(workspaceSlug, thread.slug);
|
||||
|
||||
const { ref } = useScrollActiveItemIntoView({
|
||||
isActive,
|
||||
@ -114,7 +117,7 @@ export default function ThreadItem({
|
||||
</p>
|
||||
</a>
|
||||
)}
|
||||
{!!thread.slug && !thread.deleted && (
|
||||
{!!thread.slug && !thread.deleted && !thread.virtual && (
|
||||
<div ref={optionsContainer} className="flex items-center">
|
||||
{" "}
|
||||
{/* Added flex and items-center */}
|
||||
|
||||
@ -7,7 +7,10 @@ import ThreadItem from "./ThreadItem";
|
||||
import { useParams } from "react-router-dom";
|
||||
export const THREAD_RENAME_EVENT = "renameThread";
|
||||
|
||||
export default function ThreadContainer({ workspace }) {
|
||||
export default function ThreadContainer({
|
||||
workspace,
|
||||
isVirtualThread = false,
|
||||
}) {
|
||||
const { threadSlug = null } = useParams();
|
||||
const [threads, setThreads] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -109,6 +112,12 @@ export default function ThreadContainer({ workspace }) {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function getActiveThreadIdx() {
|
||||
if (isVirtualThread) return threads.length + 1;
|
||||
const idx = threads.findIndex((t) => t?.slug === threadSlug);
|
||||
return idx >= 0 ? idx + 1 : 0;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col bg-pulse w-full h-10 items-center justify-center">
|
||||
@ -117,11 +126,7 @@ export default function ThreadContainer({ workspace }) {
|
||||
);
|
||||
}
|
||||
|
||||
const activeThreadIdx = !!threads.find(
|
||||
(thread) => thread?.slug === threadSlug
|
||||
)
|
||||
? threads.findIndex((thread) => thread?.slug === threadSlug) + 1
|
||||
: 0;
|
||||
const activeThreadIdx = getActiveThreadIdx();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col" role="list" aria-label="Threads">
|
||||
@ -129,8 +134,9 @@ export default function ThreadContainer({ workspace }) {
|
||||
idx={0}
|
||||
activeIdx={activeThreadIdx}
|
||||
isActive={activeThreadIdx === 0}
|
||||
workspace={workspace}
|
||||
thread={{ slug: null, name: "default" }}
|
||||
hasNext={threads.length > 0}
|
||||
hasNext={threads.length > 0 || isVirtualThread}
|
||||
/>
|
||||
{threads.map((thread, i) => (
|
||||
<ThreadItem
|
||||
@ -143,9 +149,19 @@ export default function ThreadContainer({ workspace }) {
|
||||
workspace={workspace}
|
||||
onRemove={removeThread}
|
||||
thread={thread}
|
||||
hasNext={i !== threads.length - 1}
|
||||
hasNext={i !== threads.length - 1 || isVirtualThread}
|
||||
/>
|
||||
))}
|
||||
{isVirtualThread && (
|
||||
<ThreadItem
|
||||
idx={activeThreadIdx}
|
||||
activeIdx={activeThreadIdx}
|
||||
isActive={true}
|
||||
workspace={workspace}
|
||||
thread={{ slug: null, name: "*New Thread", virtual: true }}
|
||||
hasNext={false}
|
||||
/>
|
||||
)}
|
||||
<DeleteAllThreadButton
|
||||
ctrlPressed={ctrlPressed}
|
||||
threads={threads}
|
||||
|
||||
@ -6,13 +6,14 @@ import ManageWorkspace, {
|
||||
useManageWorkspaceModal,
|
||||
} from "../../Modals/ManageWorkspace";
|
||||
import paths from "@/utils/paths";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useParams, useNavigate, useMatch } from "react-router-dom";
|
||||
import { GearSix, UploadSimple, DotsSixVertical } from "@phosphor-icons/react";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import ThreadContainer from "./ThreadContainer";
|
||||
import { useMatch } from "react-router-dom";
|
||||
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
||||
import showToast from "@/utils/toast";
|
||||
import { LAST_VISITED_WORKSPACE } from "@/utils/constants";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
|
||||
export default function ActiveWorkspaces() {
|
||||
const navigate = useNavigate();
|
||||
@ -23,6 +24,7 @@ export default function ActiveWorkspaces() {
|
||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||
const { user } = useUser();
|
||||
const isInWorkspaceSettings = !!useMatch("/workspace/:slug/settings/:tab");
|
||||
const isHomePage = !!useMatch("/");
|
||||
|
||||
useEffect(() => {
|
||||
async function getWorkspaces() {
|
||||
@ -71,6 +73,20 @@ export default function ActiveWorkspaces() {
|
||||
reorderWorkspaces(result.source.index, result.destination.index);
|
||||
};
|
||||
|
||||
// When on the home page, resolve which workspace should be virtually active
|
||||
const virtualActiveSlug = (() => {
|
||||
if (!isHomePage || workspaces.length === 0) return null;
|
||||
const lastVisited = safeJsonParse(
|
||||
localStorage.getItem(LAST_VISITED_WORKSPACE)
|
||||
);
|
||||
if (
|
||||
lastVisited?.slug &&
|
||||
workspaces.some((ws) => ws.slug === lastVisited.slug)
|
||||
)
|
||||
return lastVisited.slug;
|
||||
return workspaces[0]?.slug ?? null;
|
||||
})();
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="workspaces">
|
||||
@ -83,7 +99,8 @@ export default function ActiveWorkspaces() {
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{workspaces.map((workspace, index) => {
|
||||
const isActive = workspace.slug === slug;
|
||||
const isVirtuallyActive = workspace.slug === virtualActiveSlug;
|
||||
const isActive = workspace.slug === slug || isVirtuallyActive;
|
||||
return (
|
||||
<Draggable
|
||||
key={workspace.id}
|
||||
@ -191,6 +208,7 @@ export default function ActiveWorkspaces() {
|
||||
<ThreadContainer
|
||||
workspace={workspace}
|
||||
isActive={isActive}
|
||||
isVirtualThread={isVirtuallyActive}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -13,7 +13,6 @@ import { useManageWorkspaceModal } from "../../../Modals/ManageWorkspace";
|
||||
import ManageWorkspace from "../../../Modals/ManageWorkspace";
|
||||
import { ArrowDown } from "@phosphor-icons/react";
|
||||
import debounce from "lodash.debounce";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import Chartable from "./Chartable";
|
||||
import Workspace from "@/models/workspace";
|
||||
import { useParams } from "react-router-dom";
|
||||
@ -21,7 +20,6 @@ import paths from "@/utils/paths";
|
||||
import Appearance from "@/models/appearance";
|
||||
import useTextSize from "@/hooks/useTextSize";
|
||||
import useChatHistoryScrollHandle from "@/hooks/useChatHistoryScrollHandle";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment";
|
||||
import { ThoughtExpansionProvider } from "./ThoughtContainer";
|
||||
|
||||
@ -32,16 +30,13 @@ export default forwardRef(function (
|
||||
sendCommand,
|
||||
updateHistory,
|
||||
regenerateAssistantMessage,
|
||||
hasAttachments = false,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const lastScrollTopRef = useRef(0);
|
||||
const chatHistoryRef = useRef(null);
|
||||
const { user } = useUser();
|
||||
const { threadSlug = null } = useParams();
|
||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||
const { showing, hideModal } = useManageWorkspaceModal();
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||
const isStreaming = history[history.length - 1]?.animate;
|
||||
@ -98,10 +93,6 @@ export default forwardRef(function (
|
||||
scrollToBottom,
|
||||
});
|
||||
|
||||
const handleSendSuggestedMessage = (heading, message) => {
|
||||
sendCommand({ text: `${heading} ${message}`, autoSubmit: true });
|
||||
};
|
||||
|
||||
const saveEditedMessage = async ({
|
||||
editedMessage,
|
||||
chatId,
|
||||
@ -197,46 +188,6 @@ export default forwardRef(function (
|
||||
[compiledHistory.length, lastMessageInfo]
|
||||
);
|
||||
|
||||
if (history.length === 0 && !hasAttachments) {
|
||||
return (
|
||||
<div className="flex flex-col h-full md:mt-0 pb-44 md:pb-40 w-full justify-end items-center">
|
||||
<div className="flex flex-col items-center md:items-start md:max-w-[600px] w-full px-4">
|
||||
<p className="text-white/60 text-lg font-base py-4">
|
||||
{t("chat_window.welcome")}
|
||||
</p>
|
||||
{!user || user.role !== "default" ? (
|
||||
<p className="w-full items-center text-white/60 text-lg font-base flex flex-col md:flex-row gap-x-1">
|
||||
{t("chat_window.get_started")}
|
||||
<span
|
||||
className="underline font-medium cursor-pointer"
|
||||
onClick={showModal}
|
||||
>
|
||||
{t("chat_window.upload")}
|
||||
</span>
|
||||
{t("chat_window.or")}{" "}
|
||||
<b className="font-medium italic">{t("chat_window.send_chat")}</b>
|
||||
</p>
|
||||
) : (
|
||||
<p className="w-full items-center text-white/60 text-lg font-base flex flex-col md:flex-row gap-x-1">
|
||||
{t("chat_window.get_started_default")}{" "}
|
||||
<b className="font-medium italic">{t("chat_window.send_chat")}</b>
|
||||
</p>
|
||||
)}
|
||||
<WorkspaceChatSuggestions
|
||||
suggestions={workspace?.suggestedMessages ?? []}
|
||||
sendSuggestion={handleSendSuggestedMessage}
|
||||
/>
|
||||
</div>
|
||||
{showing && (
|
||||
<ManageWorkspace
|
||||
hideModal={hideModal}
|
||||
providedSlug={workspace.slug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThoughtExpansionProvider>
|
||||
<div
|
||||
@ -282,24 +233,6 @@ const getLastMessageInfo = (history) => {
|
||||
};
|
||||
};
|
||||
|
||||
function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) {
|
||||
if (suggestions.length === 0) return null;
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-theme-text-primary text-xs mt-10 w-full justify-center">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="text-left p-2.5 rounded-xl bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover border border-theme-border"
|
||||
onClick={() => sendSuggestion(suggestion.heading, suggestion.message)}
|
||||
>
|
||||
<p className="font-semibold">{suggestion.heading}</p>
|
||||
<p>{suggestion.message}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the history of messages for the chat.
|
||||
* This is mostly useful for rendering the history in a way that is easy to understand.
|
||||
|
||||
@ -16,13 +16,13 @@ export default function AvailableAgentsButton({ showing, setShowAgents }) {
|
||||
data-tooltip-content={t("chat_window.agents")}
|
||||
aria-label={t("chat_window.agents")}
|
||||
onClick={() => setShowAgents(!showing)}
|
||||
className={`flex justify-center items-center cursor-pointer ${
|
||||
className={`flex justify-center items-center cursor-pointer opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 ${
|
||||
showing ? "!opacity-100" : ""
|
||||
}`}
|
||||
>
|
||||
<At
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
className={`w-[22px] h-[22px] pointer-events-none text-theme-text-primary opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60`}
|
||||
className="w-[20px] h-[20px] pointer-events-none text-theme-text-primary"
|
||||
/>
|
||||
<Tooltip
|
||||
id="tooltip-agent-list-btn"
|
||||
@ -47,6 +47,7 @@ export function AvailableAgents({
|
||||
setShowing,
|
||||
sendCommand,
|
||||
promptRef,
|
||||
centered = false,
|
||||
}) {
|
||||
const formRef = useRef(null);
|
||||
const agentSessionActive = useIsAgentSessionActive();
|
||||
@ -88,10 +89,16 @@ export function AvailableAgents({
|
||||
return (
|
||||
<>
|
||||
<div hidden={!showing}>
|
||||
<div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4">
|
||||
<div
|
||||
className={
|
||||
centered
|
||||
? "w-full flex justify-center md:justify-start absolute top-full mt-2 left-0 z-10 px-4 md:px-0 md:pl-[57px]"
|
||||
: "flex justify-center md:justify-start absolute bottom-[130px] md:bottom-[150px] left-0 right-0 z-10 max-w-[750px] mx-auto px-4 md:px-0 md:pl-[57px]"
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={formRef}
|
||||
className="w-[600px] p-2 bg-theme-action-menu-bg rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex"
|
||||
className="w-[600px] p-2 bg-theme-action-menu-bg rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex overflow-y-auto max-h-[200px] no-scroll"
|
||||
>
|
||||
<button
|
||||
onClick={handleAgentClick}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { X, CircleNotch, Warning } from "@phosphor-icons/react";
|
||||
import Workspace from "@/models/workspace";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { nFormatter } from "@/utils/numbers";
|
||||
import showToast from "@/utils/toast";
|
||||
import pluralize from "pluralize";
|
||||
@ -17,13 +16,14 @@ export default function ParsedFilesMenu({
|
||||
setCurrentTokens,
|
||||
contextWindow,
|
||||
isLoading,
|
||||
workspaceSlug,
|
||||
threadSlug = null,
|
||||
}) {
|
||||
const { user } = useUser();
|
||||
const canEmbed = !user || user.role !== "default";
|
||||
const initialContextWindowLimitExceeded =
|
||||
contextWindow &&
|
||||
currentTokens >= contextWindow * Workspace.maxContextWindowLimit;
|
||||
const { slug, threadSlug = null } = useParams();
|
||||
const [isEmbedding, setIsEmbedding] = useState(false);
|
||||
const [embedProgress, setEmbedProgress] = useState(1);
|
||||
const [contextWindowLimitExceeded, setContextWindowLimitExceeded] = useState(
|
||||
@ -35,7 +35,7 @@ export default function ParsedFilesMenu({
|
||||
e.stopPropagation();
|
||||
if (!file?.id) return;
|
||||
|
||||
const success = await Workspace.deleteParsedFiles(slug, [file.id]);
|
||||
const success = await Workspace.deleteParsedFiles(workspaceSlug, [file.id]);
|
||||
if (!success) return;
|
||||
|
||||
// Update the local files list and current tokens
|
||||
@ -48,7 +48,7 @@ export default function ParsedFilesMenu({
|
||||
})
|
||||
);
|
||||
const { currentContextTokenCount } = await Workspace.getParsedFiles(
|
||||
slug,
|
||||
workspaceSlug,
|
||||
threadSlug
|
||||
);
|
||||
const newContextWindowLimitExceeded =
|
||||
@ -73,7 +73,7 @@ export default function ParsedFilesMenu({
|
||||
let completed = 0;
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
Workspace.embedParsedFile(slug, file.id).then(() => {
|
||||
Workspace.embedParsedFile(workspaceSlug, file.id).then(() => {
|
||||
completed++;
|
||||
setEmbedProgress(completed + 1);
|
||||
})
|
||||
@ -81,7 +81,7 @@ export default function ParsedFilesMenu({
|
||||
);
|
||||
setFiles([]);
|
||||
const { currentContextTokenCount } = await Workspace.getParsedFiles(
|
||||
slug,
|
||||
workspaceSlug,
|
||||
threadSlug
|
||||
);
|
||||
setCurrentTokens(currentContextTokenCount);
|
||||
|
||||
@ -15,10 +15,15 @@ import ParsedFilesMenu from "./ParsedFilesMenu";
|
||||
* This is a simple proxy component that clicks on the DnD file uploader for the user.
|
||||
* @returns
|
||||
*/
|
||||
export default function AttachItem() {
|
||||
export default function AttachItem({
|
||||
workspaceSlug = null,
|
||||
workspaceThreadSlug = null,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useTheme();
|
||||
const { slug, threadSlug = null } = useParams();
|
||||
const params = useParams();
|
||||
const slug = workspaceSlug || params.slug;
|
||||
const threadSlug = workspaceThreadSlug ?? params.threadSlug ?? null;
|
||||
const tooltipRef = useRef(null);
|
||||
const [isEmbedding, setIsEmbedding] = useState(false);
|
||||
const [files, setFiles] = useState([]);
|
||||
@ -93,7 +98,7 @@ export default function AttachItem() {
|
||||
<div className="relative">
|
||||
<PaperclipHorizontal
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
className="w-[22px] h-[22px] pointer-events-none text-white rotate-90 -scale-y-100"
|
||||
className="w-[20px] h-[20px] pointer-events-none text-white rotate-90 -scale-y-100"
|
||||
/>
|
||||
{files.length > 0 && (
|
||||
<div className="absolute -top-2 right-[1%] bg-white text-black light:invert text-[8px] rounded-full px-1 flex items-center justify-center">
|
||||
@ -127,6 +132,8 @@ export default function AttachItem() {
|
||||
currentTokens={currentTokens}
|
||||
setCurrentTokens={setCurrentTokens}
|
||||
contextWindow={contextWindow}
|
||||
workspaceSlug={slug}
|
||||
threadSlug={threadSlug}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@ -3,6 +3,7 @@ import { Brain, CheckCircle } from "@phosphor-icons/react";
|
||||
import LLMSelectorModal from "./index";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useUser from "@/hooks/useUser";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import SetupProvider from "./SetupProvider";
|
||||
@ -11,7 +12,9 @@ export const TOGGLE_LLM_SELECTOR_EVENT = "toggle_llm_selector";
|
||||
export const SAVE_LLM_SELECTOR_EVENT = "save_llm_selector";
|
||||
export const PROVIDER_SETUP_EVENT = "provider_setup_requested";
|
||||
|
||||
export default function LLMSelectorAction() {
|
||||
export default function LLMSelectorAction({ workspaceSlug = null }) {
|
||||
const { slug: urlSlug } = useParams();
|
||||
const slug = urlSlug ?? workspaceSlug;
|
||||
const tooltipRef = useRef(null);
|
||||
const { theme } = useTheme();
|
||||
const { user } = useUser();
|
||||
@ -87,6 +90,7 @@ export default function LLMSelectorAction() {
|
||||
// This feature is disabled for multi-user instances where the user is not an admin
|
||||
// This is because of the limitations of model selection currently and other nuances in controls.
|
||||
if (!!user && user.role !== "admin") return null;
|
||||
if (!slug) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -97,9 +101,9 @@ export default function LLMSelectorAction() {
|
||||
className={`border-none relative flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer`}
|
||||
>
|
||||
{saved ? (
|
||||
<CheckCircle className="w-[22px] h-[22px] pointer-events-none text-green-400" />
|
||||
<CheckCircle className="w-[20px] h-[20px] pointer-events-none text-green-400" />
|
||||
) : (
|
||||
<Brain className="w-[22px] h-[22px] pointer-events-none text-[var(--theme-sidebar-footer-icon-fill)]" />
|
||||
<Brain className="w-[20px] h-[20px] pointer-events-none text-[var(--theme-sidebar-footer-icon-fill)]" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
@ -117,7 +121,7 @@ export default function LLMSelectorAction() {
|
||||
}
|
||||
className="z-99 !w-[500px] !bg-theme-bg-primary !px-[5px] !rounded-lg !pointer-events-auto light:border-2 light:border-theme-modal-border"
|
||||
>
|
||||
<LLMSelectorModal tooltipRef={tooltipRef} />
|
||||
<LLMSelectorModal tooltipRef={tooltipRef} workspaceSlug={slug} />
|
||||
</Tooltip>
|
||||
<SetupProvider
|
||||
isOpen={isSetupProviderOpen}
|
||||
|
||||
@ -16,8 +16,9 @@ import showToast from "@/utils/toast";
|
||||
import Workspace from "@/models/workspace";
|
||||
import System from "@/models/system";
|
||||
|
||||
export default function LLMSelectorModal() {
|
||||
const { slug } = useParams();
|
||||
export default function LLMSelectorModal({ workspaceSlug = null }) {
|
||||
const { slug: urlSlug } = useParams();
|
||||
const slug = urlSlug ?? workspaceSlug;
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [settings, setSettings] = useState(null);
|
||||
|
||||
@ -44,6 +44,7 @@ export function validatedModelSelection(model) {
|
||||
}
|
||||
|
||||
export function hasMissingCredentials(settings, provider) {
|
||||
if (!settings) return false;
|
||||
const providerEntry = AVAILABLE_LLM_PROVIDERS.find(
|
||||
(p) => p.value === provider
|
||||
);
|
||||
|
||||
@ -15,13 +15,13 @@ export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
|
||||
data-tooltip-id="tooltip-slash-cmd-btn"
|
||||
data-tooltip-content={t("chat_window.slash")}
|
||||
onClick={() => setShowSlashCommand(!showing)}
|
||||
className={`flex justify-center items-center cursor-pointer ${
|
||||
className={`flex justify-center items-center cursor-pointer opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 ${
|
||||
showing ? "!opacity-100" : ""
|
||||
}`}
|
||||
>
|
||||
<SlashCommandIcon
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
className={`w-[20px] h-[20px] pointer-events-none opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60`}
|
||||
className="w-[18px] h-[18px] pointer-events-none"
|
||||
/>
|
||||
<Tooltip
|
||||
id="tooltip-slash-cmd-btn"
|
||||
@ -33,7 +33,13 @@ export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function SlashCommands({ showing, setShowing, sendCommand, promptRef }) {
|
||||
export function SlashCommands({
|
||||
showing,
|
||||
setShowing,
|
||||
sendCommand,
|
||||
promptRef,
|
||||
centered = false,
|
||||
}) {
|
||||
const cmdRef = useRef(null);
|
||||
useSlashCommandKeyboardNavigation({ showing });
|
||||
|
||||
@ -54,10 +60,16 @@ export function SlashCommands({ showing, setShowing, sendCommand, promptRef }) {
|
||||
|
||||
return (
|
||||
<div hidden={!showing}>
|
||||
<div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4">
|
||||
<div
|
||||
className={
|
||||
centered
|
||||
? "w-full flex justify-center md:justify-start absolute top-full mt-2 left-0 z-10 px-4 md:px-0 md:pl-[31px]"
|
||||
: "flex justify-center md:justify-start absolute bottom-[130px] md:bottom-[150px] left-0 right-0 z-10 max-w-[750px] mx-auto px-4 md:px-0 md:pl-[31px]"
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={cmdRef}
|
||||
className="w-[600px] bg-theme-action-menu-bg rounded-2xl flex shadow flex-col justify-start items-start gap-2.5 p-2 overflow-y-auto max-h-[300px] no-scroll"
|
||||
className="w-[600px] bg-theme-action-menu-bg rounded-2xl flex shadow flex-col justify-start items-start gap-2.5 p-2 overflow-y-auto max-h-[200px] no-scroll"
|
||||
>
|
||||
<ResetCommand sendCommand={sendCommand} setShowing={setShowing} />
|
||||
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />
|
||||
|
||||
@ -130,9 +130,9 @@ export default function SpeechToText({ sendCommand }) {
|
||||
}`}
|
||||
>
|
||||
<Microphone
|
||||
weight="fill"
|
||||
weight="regular"
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
className={`w-[22px] h-[22px] pointer-events-none text-theme-text-primary ${
|
||||
className={`w-[20px] h-[20px] pointer-events-none text-theme-text-primary ${
|
||||
listening ? "animate-pulse-glow" : ""
|
||||
}`}
|
||||
/>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ABORT_STREAM_EVENT } from "@/utils/chat";
|
||||
import { Stop } from "@phosphor-icons/react";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
export default function StopGenerationButton() {
|
||||
@ -13,40 +14,19 @@ export default function StopGenerationButton() {
|
||||
onClick={emitHaltEvent}
|
||||
data-tooltip-id="stop-generation-button"
|
||||
data-tooltip-content="Stop generating response"
|
||||
className="border-none text-white/60 cursor-pointer group -mr-1.5 mt-1.5"
|
||||
className="border-none inline-flex justify-center items-center rounded-full cursor-pointer w-[20px] h-[20px] light:bg-slate-800 bg-white hover:opacity-80 transition-opacity"
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ transform: "scale(1.3)" }}
|
||||
className="opacity-60 group-hover:opacity-100 light:opacity-100 light:group-hover:opacity-60"
|
||||
>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="10.562"
|
||||
r="9"
|
||||
strokeWidth="2"
|
||||
className="group-hover:stroke-primary-button stroke-white light:stroke-theme-text-secondary"
|
||||
/>
|
||||
<rect
|
||||
x="6.3999"
|
||||
y="6.96204"
|
||||
width="7.2"
|
||||
height="7.2"
|
||||
rx="2"
|
||||
className="group-hover:fill-primary-button fill-white light:fill-theme-text-secondary"
|
||||
/>
|
||||
</svg>
|
||||
<Stop
|
||||
className="w-[12px] h-[12px] light:text-white text-black"
|
||||
weight="fill"
|
||||
/>
|
||||
</button>
|
||||
<Tooltip
|
||||
id="stop-generation-button"
|
||||
place="bottom"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs z-99 -ml-1"
|
||||
className="tooltip !text-xs z-99"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -28,7 +28,7 @@ export default function TextSizeButton() {
|
||||
<TextT
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
weight="fill"
|
||||
className="w-[22px] h-[22px] pointer-events-none text-white"
|
||||
className="w-[20px] h-[20px] pointer-events-none text-white"
|
||||
/>
|
||||
</div>
|
||||
<Tooltip
|
||||
|
||||
@ -4,7 +4,7 @@ import SlashCommandsButton, {
|
||||
useSlashCommands,
|
||||
} from "./SlashCommands";
|
||||
import debounce from "lodash.debounce";
|
||||
import { PaperPlaneRight } from "@phosphor-icons/react";
|
||||
import { ArrowUp } from "@phosphor-icons/react";
|
||||
import StopGenerationButton from "./StopGenerationButton";
|
||||
import AvailableAgentsButton, {
|
||||
AvailableAgents,
|
||||
@ -30,11 +30,23 @@ export const PROMPT_INPUT_ID = "primary-prompt-input";
|
||||
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
||||
const MAX_EDIT_STACK_SIZE = 100;
|
||||
|
||||
/**
|
||||
* @param {function} props.submit - form submit handler
|
||||
* @param {boolean} props.isStreaming - disables input while streaming response
|
||||
* @param {function} props.sendCommand - handler for slash commands and agent mentions
|
||||
* @param {Array} [props.attachments] - file attachments array
|
||||
* @param {boolean} [props.centered] - renders in centered layout mode (for home page)
|
||||
* @param {string} [props.workspaceSlug] - workspace slug for home page context
|
||||
* @param {string} [props.threadSlug] - thread slug for home page context
|
||||
*/
|
||||
export default function PromptInput({
|
||||
submit,
|
||||
isStreaming,
|
||||
sendCommand,
|
||||
attachments = [],
|
||||
centered = false,
|
||||
workspaceSlug = null,
|
||||
threadSlug = null,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { isDisabled } = useIsDisabled();
|
||||
@ -247,27 +259,41 @@ export default function PromptInput({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center pwa:pb-5">
|
||||
<div
|
||||
className={
|
||||
centered
|
||||
? "w-full relative flex justify-center items-center"
|
||||
: "w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center pwa:pb-5"
|
||||
}
|
||||
>
|
||||
<SlashCommands
|
||||
showing={showSlashCommand}
|
||||
setShowing={setShowSlashCommand}
|
||||
sendCommand={sendCommand}
|
||||
promptRef={textareaRef}
|
||||
centered={centered}
|
||||
/>
|
||||
<AvailableAgents
|
||||
showing={showAgents}
|
||||
setShowing={setShowAgents}
|
||||
sendCommand={sendCommand}
|
||||
promptRef={textareaRef}
|
||||
centered={centered}
|
||||
/>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl items-center"
|
||||
className={
|
||||
centered
|
||||
? "flex flex-col gap-y-1 rounded-t-lg w-full items-center"
|
||||
: "flex flex-col gap-y-1 rounded-t-lg md:w-full w-full mx-auto max-w-[750px] items-center"
|
||||
}
|
||||
>
|
||||
<div className="flex items-center rounded-lg md:mb-4 md:w-full">
|
||||
<div className="w-[95vw] md:w-[635px] bg-theme-bg-chat-input light:bg-white light:border-solid light:border-[1px] light:border-theme-chat-input-border shadow-sm rounded-2xl pwa:rounded-3xl flex flex-col px-2 overflow-hidden">
|
||||
<div
|
||||
className={`flex items-center rounded-lg md:w-full ${centered ? "mb-0" : "mb-4"}`}
|
||||
>
|
||||
<div className="w-[95vw] md:w-[750px] bg-theme-bg-chat-input light:bg-white light:border-solid light:border-[1px] light:border-theme-chat-input-border shadow-sm rounded-[20px] pwa:rounded-3xl flex flex-col px-2 overflow-hidden">
|
||||
<AttachmentManager attachments={attachments} />
|
||||
<div className="flex items-center border-b border-theme-chat-input-border mx-3">
|
||||
<div className="flex items-center mx-[7px]">
|
||||
<textarea
|
||||
id={PROMPT_INPUT_ID}
|
||||
ref={textareaRef}
|
||||
@ -288,42 +314,13 @@ export default function PromptInput({
|
||||
className={`border-none cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] mx-2 md:mx-0 pt-[12px] w-full leading-5 text-white bg-transparent placeholder:text-white/60 light:placeholder:text-theme-text-primary resize-none active:outline-none focus:outline-none flex-grow mb-1 pwa:!text-[16px] ${textSizeClass}`}
|
||||
placeholder={t("chat_window.send_message")}
|
||||
/>
|
||||
{isStreaming ? (
|
||||
<StopGenerationButton />
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
ref={formRef}
|
||||
type="submit"
|
||||
disabled={isDisabled}
|
||||
className="border-none inline-flex justify-center rounded-2xl cursor-pointer opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 ml-4 disabled:cursor-not-allowed group"
|
||||
data-tooltip-id="send-prompt"
|
||||
data-tooltip-content={
|
||||
isDisabled
|
||||
? t("chat_window.attachments_processing")
|
||||
: t("chat_window.send")
|
||||
}
|
||||
aria-label={t("chat_window.send")}
|
||||
>
|
||||
<PaperPlaneRight
|
||||
color="var(--theme-sidebar-footer-icon-fill)"
|
||||
className="w-[22px] h-[22px] pointer-events-none text-theme-text-primary group-disabled:opacity-[25%]"
|
||||
weight="fill"
|
||||
/>
|
||||
<span className="sr-only">Send message</span>
|
||||
</button>
|
||||
<Tooltip
|
||||
id="send-prompt"
|
||||
place="bottom"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs z-99"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between py-3.5 mx-3 mb-1">
|
||||
<div className="flex gap-x-2">
|
||||
<AttachItem />
|
||||
<div className="flex justify-between items-center pt-3.5 pb-3 mx-[7px]">
|
||||
<div className="flex gap-x-2 items-center h-5 -ml-[4.5px]">
|
||||
<AttachItem
|
||||
workspaceSlug={workspaceSlug}
|
||||
workspaceThreadSlug={threadSlug}
|
||||
/>
|
||||
<SlashCommandsButton
|
||||
showing={showSlashCommand}
|
||||
setShowSlashCommand={setShowSlashCommand}
|
||||
@ -333,10 +330,41 @@ export default function PromptInput({
|
||||
setShowAgents={setShowAgents}
|
||||
/>
|
||||
<TextSizeButton />
|
||||
<LLMSelectorAction />
|
||||
<LLMSelectorAction workspaceSlug={workspaceSlug} />
|
||||
</div>
|
||||
<div className="flex gap-x-2">
|
||||
<div className="flex gap-x-2 items-center h-5">
|
||||
<SpeechToText sendCommand={sendCommand} />
|
||||
{isStreaming ? (
|
||||
<StopGenerationButton />
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
ref={formRef}
|
||||
type="submit"
|
||||
disabled={isDisabled}
|
||||
className="border-none inline-flex justify-center items-center rounded-full cursor-pointer w-[20px] h-[20px] light:bg-slate-800 bg-white disabled:cursor-not-allowed disabled:opacity-50 hover:opacity-80 transition-opacity"
|
||||
data-tooltip-id="send-prompt"
|
||||
data-tooltip-content={
|
||||
isDisabled
|
||||
? t("chat_window.attachments_processing")
|
||||
: t("chat_window.send")
|
||||
}
|
||||
aria-label={t("chat_window.send")}
|
||||
>
|
||||
<ArrowUp
|
||||
className="w-[12px] h-[12px] pointer-events-none light:text-white text-black"
|
||||
weight="bold"
|
||||
/>
|
||||
<span className="sr-only">Send message</span>
|
||||
</button>
|
||||
<Tooltip
|
||||
id="send-prompt"
|
||||
place="bottom"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs z-99"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useState, useEffect, useContext, useRef } from "react";
|
||||
import ChatHistory from "./ChatHistory";
|
||||
import { CLEAR_ATTACHMENTS_EVENT, DndUploaderContext } from "./DnDWrapper";
|
||||
import PromptInput, {
|
||||
@ -9,7 +9,7 @@ import Workspace from "@/models/workspace";
|
||||
import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { SidebarMobileHeader } from "../../Sidebar";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { v4 } from "uuid";
|
||||
import handleSocketResponse, {
|
||||
websocketURI,
|
||||
@ -23,8 +23,16 @@ import SpeechRecognition, {
|
||||
import { ChatTooltips } from "./ChatTooltips";
|
||||
import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics";
|
||||
import useChatContainerQuickScroll from "@/hooks/useChatContainerQuickScroll";
|
||||
import { PENDING_HOME_MESSAGE } from "@/utils/constants";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import paths from "@/utils/paths";
|
||||
import QuickActions from "@/components/lib/QuickActions";
|
||||
import SuggestedMessages from "@/components/lib/SuggestedMessages";
|
||||
|
||||
export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { threadSlug = null } = useParams();
|
||||
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||
const [chatHistory, setChatHistory] = useState(knownHistory);
|
||||
@ -32,6 +40,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
const [websocket, setWebsocket] = useState(null);
|
||||
const { files, parseAttachments } = useContext(DndUploaderContext);
|
||||
const { chatHistoryRef } = useChatContainerQuickScroll();
|
||||
const pendingMessageChecked = useRef(false);
|
||||
|
||||
const { listening, resetTranscript } = useSpeechRecognition({
|
||||
clearTranscriptOnListen: true,
|
||||
@ -164,6 +173,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
role: "assistant",
|
||||
pending: true,
|
||||
userMessage: text,
|
||||
attachments,
|
||||
animate: true,
|
||||
},
|
||||
];
|
||||
@ -174,6 +184,23 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
setLoadingResponse(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingMessageChecked.current || !workspace?.slug) return;
|
||||
pendingMessageChecked.current = true;
|
||||
|
||||
const pending = safeJsonParse(sessionStorage.getItem(PENDING_HOME_MESSAGE));
|
||||
if (pending?.message) {
|
||||
setTimeout(() => {
|
||||
sessionStorage.removeItem(PENDING_HOME_MESSAGE);
|
||||
sendCommand({
|
||||
text: pending.message,
|
||||
attachments: pending.attachments || [],
|
||||
autoSubmit: true,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}, [workspace?.slug]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchReply() {
|
||||
const promptMessage =
|
||||
@ -294,6 +321,53 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
handleWSS();
|
||||
}, [socketId]);
|
||||
|
||||
const isEmpty =
|
||||
chatHistory.length === 0 && !sessionStorage.getItem(PENDING_HOME_MESSAGE);
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<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-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
|
||||
>
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<DnDFileUploaderWrapper>
|
||||
<div className="flex flex-col h-full w-full items-center justify-center">
|
||||
<div className="flex flex-col items-center w-full max-w-[750px]">
|
||||
<h1 className="text-white text-xl md:text-2xl mb-11 text-center">
|
||||
{t("main-page.greeting")}
|
||||
</h1>
|
||||
<PromptInput
|
||||
submit={handleSubmit}
|
||||
isStreaming={loadingResponse}
|
||||
sendCommand={sendCommand}
|
||||
attachments={files}
|
||||
centered={true}
|
||||
/>
|
||||
<QuickActions
|
||||
hasAvailableWorkspace={!!workspace}
|
||||
onCreateAgent={() => navigate(paths.settings.agentSkills())}
|
||||
onEditWorkspace={() =>
|
||||
navigate(
|
||||
paths.workspace.settings.generalAppearance(workspace.slug)
|
||||
)
|
||||
}
|
||||
onUploadDocument={() =>
|
||||
document.getElementById("dnd-chat-file-uploader")?.click()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SuggestedMessages
|
||||
suggestedMessages={workspace?.suggestedMessages}
|
||||
sendCommand={sendCommand}
|
||||
/>
|
||||
</div>
|
||||
</DnDFileUploaderWrapper>
|
||||
<ChatTooltips />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||
@ -301,23 +375,27 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
>
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<DnDFileUploaderWrapper>
|
||||
<MetricsProvider>
|
||||
<ChatHistory
|
||||
ref={chatHistoryRef}
|
||||
history={chatHistory}
|
||||
workspace={workspace}
|
||||
sendCommand={sendCommand}
|
||||
updateHistory={setChatHistory}
|
||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||
hasAttachments={files.length > 0}
|
||||
/>
|
||||
</MetricsProvider>
|
||||
<PromptInput
|
||||
submit={handleSubmit}
|
||||
isStreaming={loadingResponse}
|
||||
sendCommand={sendCommand}
|
||||
attachments={files}
|
||||
/>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<div className="contents">
|
||||
<MetricsProvider>
|
||||
<ChatHistory
|
||||
ref={chatHistoryRef}
|
||||
history={chatHistory}
|
||||
workspace={workspace}
|
||||
sendCommand={sendCommand}
|
||||
updateHistory={setChatHistory}
|
||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||
/>
|
||||
</MetricsProvider>
|
||||
<PromptInput
|
||||
submit={handleSubmit}
|
||||
isStreaming={loadingResponse}
|
||||
sendCommand={sendCommand}
|
||||
attachments={files}
|
||||
centered={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DnDFileUploaderWrapper>
|
||||
<ChatTooltips />
|
||||
</div>
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
TTSProvider,
|
||||
useWatchForAutoPlayAssistantTTSResponse,
|
||||
} from "../contexts/TTSProvider";
|
||||
import { PENDING_HOME_MESSAGE } from "@/utils/constants";
|
||||
|
||||
export default function WorkspaceChat({ loading, workspace }) {
|
||||
useWatchForAutoPlayAssistantTTSResponse();
|
||||
@ -36,7 +37,15 @@ export default function WorkspaceChat({ loading, workspace }) {
|
||||
getHistory();
|
||||
}, [workspace, loading]);
|
||||
|
||||
if (loadingHistory) return <LoadingChat />;
|
||||
const hasPendingMessage = !!sessionStorage.getItem(PENDING_HOME_MESSAGE);
|
||||
if (loadingHistory) {
|
||||
if (hasPendingMessage) {
|
||||
return (
|
||||
<div className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full" />
|
||||
);
|
||||
}
|
||||
return <LoadingChat />;
|
||||
}
|
||||
if (!loading && !loadingHistory && !workspace) {
|
||||
return (
|
||||
<>
|
||||
|
||||
57
frontend/src/components/lib/QuickActions/index.jsx
Normal file
57
frontend/src/components/lib/QuickActions/index.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useUser from "@/hooks/useUser";
|
||||
|
||||
/**
|
||||
* Quick action buttons for home and empty workspace states.
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.hasAvailableWorkspace - Whether the user has a workspace they can use
|
||||
* @param {Function} props.onCreateAgent - Handler for "Create an Agent" action
|
||||
* @param {Function} props.onEditWorkspace - Handler for "Edit Workspace" action
|
||||
* @param {Function} props.onUploadDocument - Handler for "Upload a Document" action
|
||||
*/
|
||||
export default function QuickActions({
|
||||
hasAvailableWorkspace,
|
||||
onCreateAgent,
|
||||
onEditWorkspace,
|
||||
onUploadDocument,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useUser();
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap justify-center gap-2 mt-6">
|
||||
<QuickActionButton
|
||||
label={t("main-page.quickActions.createAgent")}
|
||||
onClick={onCreateAgent}
|
||||
show={!user || ["admin"].includes(user?.role)}
|
||||
/>
|
||||
<QuickActionButton
|
||||
label={t("main-page.quickActions.editWorkspace")}
|
||||
onClick={onEditWorkspace}
|
||||
show={
|
||||
hasAvailableWorkspace &&
|
||||
(!user || ["admin", "manager"].includes(user?.role))
|
||||
}
|
||||
/>
|
||||
<QuickActionButton
|
||||
label={t("main-page.quickActions.uploadDocument")}
|
||||
onClick={onUploadDocument}
|
||||
// Any user can upload documents.
|
||||
show={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickActionButton({ label, onClick, show = true }) {
|
||||
if (!show) return null;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="px-4 py-2 rounded-full bg-theme-bg-chat-input text-white/80 text-sm font-normal leading-5 hover:bg-zinc-700 light:hover:bg-black/20 transition-colors light:text-theme-text-primary"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
32
frontend/src/components/lib/SuggestedMessages/index.jsx
Normal file
32
frontend/src/components/lib/SuggestedMessages/index.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
export default function SuggestedMessages({
|
||||
suggestedMessages = [],
|
||||
sendCommand,
|
||||
}) {
|
||||
if (!suggestedMessages?.length) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full max-w-[650px] mt-6 px-4">
|
||||
{suggestedMessages.map((msg, index) => {
|
||||
const text = msg.heading?.trim()
|
||||
? `${msg.heading.trim()} ${msg.message?.trim() || ""}`
|
||||
: msg.message?.trim() || "";
|
||||
if (!text) return null;
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{index > 0 && (
|
||||
<div className="border-t border-zinc-800 light:border-theme-chat-input-border" />
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => sendCommand({ text, autoSubmit: true })}
|
||||
className="w-full text-left py-3 px-3 text-white/80 text-sm font-normal leading-5 hover:text-white transition-colors light:text-theme-text-primary light:hover:text-theme-text-primary/80 hover:bg-zinc-800 light:hover:bg-black/20 rounded-lg"
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -917,6 +917,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "اختصارات لوحة المفاتيح",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "إنشاء وكيل",
|
||||
editWorkspace: "تعديل مساحة العمل",
|
||||
uploadDocument: "تحميل مستند",
|
||||
},
|
||||
greeting: "كيف يمكنني مساعدتك اليوم؟",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "اختصارات لوحة المفاتيح",
|
||||
|
||||
@ -215,6 +215,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Klávesové zkratky",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Vytvořte agenta",
|
||||
editWorkspace: "Upravit pracovní prostor",
|
||||
uploadDocument: "Nahrajte dokument",
|
||||
},
|
||||
greeting: "Jak vám mohu dnes pomoci?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "Nový pracovní prostor",
|
||||
|
||||
@ -942,6 +942,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Tastaturgenveje",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Opret en agent",
|
||||
editWorkspace: "Rediger arbejdsområdet",
|
||||
uploadDocument: "Upload en fil",
|
||||
},
|
||||
greeting: "Hvordan kan jeg hjælpe dig i dag?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "Tastaturgenveje",
|
||||
|
||||
@ -210,6 +210,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Tastaturkürzel",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Erstelle einen Agenten",
|
||||
editWorkspace: "Arbeitsbereich bearbeiten",
|
||||
uploadDocument: "Ein Dokument hochladen",
|
||||
},
|
||||
greeting: "Wie kann ich Ihnen heute helfen?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "Neuer Workspace",
|
||||
|
||||
@ -139,6 +139,7 @@ const TRANSLATIONS = {
|
||||
},
|
||||
|
||||
"main-page": {
|
||||
greeting: "How can I help you today?",
|
||||
noWorkspaceError: "Please create a workspace before starting a chat.",
|
||||
checklist: {
|
||||
title: "Getting Started",
|
||||
@ -178,6 +179,11 @@ const TRANSLATIONS = {
|
||||
},
|
||||
},
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Create an Agent",
|
||||
editWorkspace: "Edit Workspace",
|
||||
uploadDocument: "Upload a Document",
|
||||
},
|
||||
quickLinks: {
|
||||
title: "Quick Links",
|
||||
sendChat: "Send Chat",
|
||||
|
||||
@ -210,6 +210,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Atajos de teclado",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Crear un agente",
|
||||
editWorkspace: "Editar espacio de trabajo",
|
||||
uploadDocument: "Cargar un documento",
|
||||
},
|
||||
greeting: "¿Cómo puedo ayudarte hoy?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "Nuevo espacio de trabajo",
|
||||
|
||||
@ -203,6 +203,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Klaviatuuri otseteed",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Loo agent",
|
||||
editWorkspace: "Redige tööruum",
|
||||
uploadDocument: "Lae fail üles",
|
||||
},
|
||||
greeting: "Kuidas saan teid täna aidata?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "Uus tööruum",
|
||||
|
||||
@ -932,6 +932,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "کلیدهای میانبر",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "ایجاد یک عامل",
|
||||
editWorkspace: "ویرایش فضای کاری",
|
||||
uploadDocument: "بارگذاری یک سند",
|
||||
},
|
||||
greeting: "امروز چگونه میتوانم به شما کمک کنم؟",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "کلیدهای میانبر",
|
||||
|
||||
@ -942,6 +942,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Raccourcis clavier",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Créer un agent",
|
||||
editWorkspace: "Modifier l'espace de travail",
|
||||
uploadDocument: "Télécharger un document",
|
||||
},
|
||||
greeting: "Comment puis-je vous aider aujourd'hui ?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "Raccourcis clavier",
|
||||
|
||||
@ -203,6 +203,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "קיצורי מקלדת",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "צור סוכן",
|
||||
editWorkspace: "ערוך את סביבת העבודה",
|
||||
uploadDocument: "העלה מסמך",
|
||||
},
|
||||
greeting: "במה אוכל לעזור לך היום?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "סביבת עבודה חדשה",
|
||||
|
||||
@ -959,6 +959,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Combinazioni di tasti",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Creare un agente",
|
||||
editWorkspace: "Modifica l'area di lavoro",
|
||||
uploadDocument: "Caricare un documento",
|
||||
},
|
||||
greeting: "Come posso aiutarti oggi?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "Combinazioni di tasti",
|
||||
|
||||
@ -928,6 +928,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "キーボードショートカット",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "エージェントを作成する",
|
||||
editWorkspace: "ワークスペースの編集",
|
||||
uploadDocument: "ドキュメントをアップロードする",
|
||||
},
|
||||
greeting: "今日はどのようにお手伝いできますか?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "キーボードショートカット",
|
||||
|
||||
@ -204,6 +204,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "단축키 안내",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "에이전트 생성",
|
||||
editWorkspace: "워크스페이스 편집",
|
||||
uploadDocument: "문서 업로드",
|
||||
},
|
||||
greeting: "오늘 어떻게 도와드릴까요?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "새 워크스페이스",
|
||||
|
||||
@ -207,6 +207,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Taustiņu atvieglojumi",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Izveidot aģentu",
|
||||
editWorkspace: "Rediģēt darba telpu",
|
||||
uploadDocument: "August failu",
|
||||
},
|
||||
greeting: "Kā es varu jums šodien palīdzēt?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "Jauna darba telpa",
|
||||
|
||||
@ -937,6 +937,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Sneltoetsen",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Maak een agent",
|
||||
editWorkspace: "Werkruimte bewerken",
|
||||
uploadDocument: "Upload een document",
|
||||
},
|
||||
greeting: "Hoe kan ik u vandaag helpen?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "Sneltoetsen",
|
||||
|
||||
@ -209,6 +209,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Skróty klawiaturowe",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Utwórz agenta",
|
||||
editWorkspace: "Edytuj przestrzeń roboczą",
|
||||
uploadDocument: "Załaduj dokument",
|
||||
},
|
||||
greeting: "W czym mogę Ci dzisiaj pomóc?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "Nowy obszar roboczy",
|
||||
|
||||
@ -205,6 +205,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Atalhos de Teclado",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Criar um Agente",
|
||||
editWorkspace: "Editar o Espaço de Trabalho",
|
||||
uploadDocument: "Enviar um documento",
|
||||
},
|
||||
greeting: "Como posso ajudá-lo hoje?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "Novo Workspace",
|
||||
|
||||
@ -209,6 +209,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Scurtături de tastatură",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Creați un agent",
|
||||
editWorkspace: "Modifică spațiul de lucru",
|
||||
uploadDocument: "Încărcați un document",
|
||||
},
|
||||
greeting: "Cu ce vă pot ajuta astăzi?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "Spațiu de lucru nou",
|
||||
|
||||
@ -946,6 +946,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Сочетания клавиш",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Создать агента",
|
||||
editWorkspace: "Редактировать рабочее пространство",
|
||||
uploadDocument: "Загрузить документ",
|
||||
},
|
||||
greeting: "Чем я могу вам помочь сегодня?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "Сочетания клавиш",
|
||||
|
||||
@ -934,6 +934,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Klavye Kısayolları",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Bir temsilci oluşturun",
|
||||
editWorkspace: "Çalışma Alanını Düzenle",
|
||||
uploadDocument: "Bir belge yükleyin",
|
||||
},
|
||||
greeting: "Bugün size nasıl yardımcı olabilirim?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "Klavye Kısayolları",
|
||||
|
||||
@ -930,6 +930,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "Phím tắt",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "Tạo một đại lý",
|
||||
editWorkspace: "Chỉnh sửa không gian làm việc",
|
||||
uploadDocument: "Tải lên một tài liệu",
|
||||
},
|
||||
greeting: "Hôm nay tôi có thể giúp gì cho bạn?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "Phím tắt",
|
||||
|
||||
@ -199,6 +199,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "键盘快捷键",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "创建代理",
|
||||
editWorkspace: "编辑工作区",
|
||||
uploadDocument: "上传文件",
|
||||
},
|
||||
greeting: "今天我能帮您什么?",
|
||||
},
|
||||
"new-workspace": {
|
||||
title: "新工作区",
|
||||
|
||||
@ -874,6 +874,12 @@ const TRANSLATIONS = {
|
||||
},
|
||||
keyboardShortcuts: "鍵盤快捷鍵",
|
||||
},
|
||||
quickActions: {
|
||||
createAgent: "建立一個代理",
|
||||
editWorkspace: "編輯工作區",
|
||||
uploadDocument: "上傳文件",
|
||||
},
|
||||
greeting: "今天我能幫您什麼?",
|
||||
},
|
||||
"keyboard-shortcuts": {
|
||||
title: "鍵盤快捷鍵",
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function SlashCommandIcon({ className }) {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 14 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect
|
||||
x="0.5"
|
||||
y="1"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="3.5"
|
||||
stroke="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M4.18103 10.7974L9.8189 4.20508"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { CHECKLIST_STORAGE_KEY, CHECKLIST_UPDATED_EVENT } from "../constants";
|
||||
import { Check } from "@phosphor-icons/react";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
|
||||
export function ChecklistItem({ id, title, action, onAction, icon: Icon }) {
|
||||
const [isCompleted, setIsCompleted] = useState(() => {
|
||||
const stored = window.localStorage.getItem(CHECKLIST_STORAGE_KEY);
|
||||
if (!stored) return false;
|
||||
const completedItems = safeJsonParse(stored, {});
|
||||
return completedItems[id] || false;
|
||||
});
|
||||
|
||||
const handleClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!isCompleted) {
|
||||
const shouldComplete = await onAction();
|
||||
if (shouldComplete) {
|
||||
const stored = window.localStorage.getItem(CHECKLIST_STORAGE_KEY);
|
||||
const completedItems = safeJsonParse(stored, {});
|
||||
completedItems[id] = true;
|
||||
window.localStorage.setItem(
|
||||
CHECKLIST_STORAGE_KEY,
|
||||
JSON.stringify(completedItems)
|
||||
);
|
||||
setIsCompleted(true);
|
||||
window.dispatchEvent(new CustomEvent(CHECKLIST_UPDATED_EVENT));
|
||||
}
|
||||
} else {
|
||||
await onAction();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center gap-x-4 transition-colors cursor-pointer rounded-lg p-3 group hover:bg-theme-checklist-item-bg-hover ${
|
||||
isCompleted
|
||||
? "bg-theme-checklist-item-completed-bg"
|
||||
: "bg-theme-checklist-item-bg"
|
||||
}`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{Icon && (
|
||||
<div className="flex-shrink-0">
|
||||
<Icon
|
||||
size={18}
|
||||
className={
|
||||
isCompleted
|
||||
? "text-theme-checklist-item-completed-text"
|
||||
: "text-theme-checklist-item-text"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<h3
|
||||
className={`text-sm font-medium transition-colors duration-200 ${
|
||||
isCompleted
|
||||
? "text-theme-checklist-item-completed-text line-through"
|
||||
: "text-theme-checklist-item-text"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
{isCompleted ? (
|
||||
<div className="w-5 h-5 rounded-full bg-theme-checklist-checkbox-fill flex items-center justify-center">
|
||||
<Check
|
||||
size={14}
|
||||
weight="bold"
|
||||
className="text-theme-checklist-checkbox-text"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button className="w-[64px] h-[24px] rounded-md bg-white/10 light:bg-white/70 text-theme-checklist-item-text font-semibold text-xs transition-all duration-200 flex items-center justify-center hover:bg-white/20 light:hover:bg-white/60">
|
||||
{action}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,168 +0,0 @@
|
||||
import {
|
||||
SquaresFour,
|
||||
ChatDots,
|
||||
Files,
|
||||
ChatCenteredText,
|
||||
UsersThree,
|
||||
} from "@phosphor-icons/react";
|
||||
import SlashCommandIcon from "./ChecklistItem/icons/SlashCommand";
|
||||
import paths from "@/utils/paths";
|
||||
import { t } from "i18next";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
export const CHECKLIST_UPDATED_EVENT = "anythingllm_checklist_updated";
|
||||
export const CHECKLIST_STORAGE_KEY = "anythingllm_checklist_completed";
|
||||
export const CHECKLIST_HIDDEN = "anythingllm_checklist_dismissed";
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChecklistItemHandlerParams
|
||||
* @property {Object[]} workspaces - Array of workspaces
|
||||
* @property {Function} navigate - Function to navigate to a path
|
||||
* @property {Function} setSelectedWorkspace - Function to set the selected workspace
|
||||
* @property {Function} showManageWsModal - Function to show the manage workspace modal
|
||||
* @property {Function} showToast - Function to show a toast
|
||||
* @property {Function} showNewWsModal - Function to show the new workspace modal
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} ChecklistItem
|
||||
* @property {string} id
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {string} action
|
||||
* @property {(params: ChecklistItemHandlerParams) => boolean} handler
|
||||
* @property {string} icon
|
||||
* @property {boolean} completed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function to generate the checklist items
|
||||
* @returns {ChecklistItem[]}
|
||||
*/
|
||||
export const CHECKLIST_ITEMS = () => [
|
||||
{
|
||||
id: "create_workspace",
|
||||
title: t("main-page.checklist.tasks.create_workspace.title"),
|
||||
description: t("main-page.checklist.tasks.create_workspace.description"),
|
||||
action: t("main-page.checklist.tasks.create_workspace.action"),
|
||||
handler: ({ showNewWsModal = noop }) => {
|
||||
showNewWsModal();
|
||||
return true;
|
||||
},
|
||||
icon: SquaresFour,
|
||||
},
|
||||
{
|
||||
id: "send_chat",
|
||||
title: t("main-page.checklist.tasks.send_chat.title"),
|
||||
description: t("main-page.checklist.tasks.send_chat.description"),
|
||||
action: t("main-page.checklist.tasks.send_chat.action"),
|
||||
handler: ({
|
||||
workspaces = [],
|
||||
navigate = noop,
|
||||
showToast = noop,
|
||||
showNewWsModal = noop,
|
||||
}) => {
|
||||
if (workspaces.length === 0) {
|
||||
showToast(t("main-page.noWorkspaceError"), "warning", {
|
||||
clear: true,
|
||||
});
|
||||
showNewWsModal();
|
||||
return false;
|
||||
}
|
||||
navigate(paths.workspace.chat(workspaces[0].slug));
|
||||
return true;
|
||||
},
|
||||
icon: ChatDots,
|
||||
},
|
||||
{
|
||||
id: "embed_document",
|
||||
title: t("main-page.checklist.tasks.embed_document.title"),
|
||||
description: t("main-page.checklist.tasks.embed_document.description"),
|
||||
action: t("main-page.checklist.tasks.embed_document.action"),
|
||||
handler: ({
|
||||
workspaces = [],
|
||||
setSelectedWorkspace = noop,
|
||||
showManageWsModal = noop,
|
||||
showToast = noop,
|
||||
showNewWsModal = noop,
|
||||
}) => {
|
||||
if (workspaces.length === 0) {
|
||||
showToast(t("main-page.noWorkspaceError"), "warning", {
|
||||
clear: true,
|
||||
});
|
||||
showNewWsModal();
|
||||
return false;
|
||||
}
|
||||
setSelectedWorkspace(workspaces[0]);
|
||||
showManageWsModal();
|
||||
return true;
|
||||
},
|
||||
icon: Files,
|
||||
},
|
||||
{
|
||||
id: "setup_system_prompt",
|
||||
title: t("main-page.checklist.tasks.setup_system_prompt.title"),
|
||||
description: t("main-page.checklist.tasks.setup_system_prompt.description"),
|
||||
action: t("main-page.checklist.tasks.setup_system_prompt.action"),
|
||||
handler: ({
|
||||
workspaces = [],
|
||||
navigate = noop,
|
||||
showNewWsModal = noop,
|
||||
showToast = noop,
|
||||
}) => {
|
||||
if (workspaces.length === 0) {
|
||||
showToast(t("main-page.noWorkspaceError"), "warning", {
|
||||
clear: true,
|
||||
});
|
||||
showNewWsModal();
|
||||
return false;
|
||||
}
|
||||
navigate(
|
||||
paths.workspace.settings.chatSettings(workspaces[0].slug, {
|
||||
search: { action: "focus-system-prompt" },
|
||||
})
|
||||
);
|
||||
return true;
|
||||
},
|
||||
icon: ChatCenteredText,
|
||||
},
|
||||
{
|
||||
id: "define_slash_command",
|
||||
title: t("main-page.checklist.tasks.define_slash_command.title"),
|
||||
description: t(
|
||||
"main-page.checklist.tasks.define_slash_command.description"
|
||||
),
|
||||
action: t("main-page.checklist.tasks.define_slash_command.action"),
|
||||
handler: ({
|
||||
workspaces = [],
|
||||
navigate = noop,
|
||||
showNewWsModal = noop,
|
||||
showToast = noop,
|
||||
}) => {
|
||||
if (workspaces.length === 0) {
|
||||
showToast(t("main-page.noWorkspaceError"), "warning", { clear: true });
|
||||
showNewWsModal();
|
||||
return false;
|
||||
}
|
||||
navigate(
|
||||
paths.workspace.chat(workspaces[0].slug, {
|
||||
search: { action: "open-new-slash-command-modal" },
|
||||
})
|
||||
);
|
||||
return true;
|
||||
},
|
||||
icon: SlashCommandIcon,
|
||||
},
|
||||
{
|
||||
id: "visit_community",
|
||||
title: t("main-page.checklist.tasks.visit_community.title"),
|
||||
description: t("main-page.checklist.tasks.visit_community.description"),
|
||||
action: t("main-page.checklist.tasks.visit_community.action"),
|
||||
handler: () => {
|
||||
window.open(paths.communityHub.website(), "_blank");
|
||||
return true;
|
||||
},
|
||||
icon: UsersThree,
|
||||
},
|
||||
];
|
||||
@ -1,216 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import ManageWorkspace, {
|
||||
useManageWorkspaceModal,
|
||||
} from "@/components/Modals/ManageWorkspace";
|
||||
import NewWorkspaceModal, {
|
||||
useNewWorkspaceModal,
|
||||
} from "@/components/Modals/NewWorkspace";
|
||||
import Workspace from "@/models/workspace";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ChecklistItem } from "./ChecklistItem";
|
||||
import showToast from "@/utils/toast";
|
||||
import {
|
||||
CHECKLIST_HIDDEN,
|
||||
CHECKLIST_STORAGE_KEY,
|
||||
CHECKLIST_ITEMS,
|
||||
CHECKLIST_UPDATED_EVENT,
|
||||
} from "./constants";
|
||||
import ConfettiExplosion from "react-confetti-explosion";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const MemoizedChecklistItem = React.memo(ChecklistItem);
|
||||
export default function Checklist() {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
const [completedCount, setCompletedCount] = useState(0);
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState(null);
|
||||
const [workspaces, setWorkspaces] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const containerRef = useRef(null);
|
||||
const {
|
||||
showModal: showNewWsModal,
|
||||
hideModal: hideNewWsModal,
|
||||
showing: showingNewWsModal,
|
||||
} = useNewWorkspaceModal();
|
||||
const { showModal: showManageWsModal, hideModal: hideManageWsModal } =
|
||||
useManageWorkspaceModal();
|
||||
|
||||
const createItemHandler = useCallback(
|
||||
(item) => {
|
||||
return () =>
|
||||
item.handler({
|
||||
workspaces,
|
||||
navigate,
|
||||
setSelectedWorkspace,
|
||||
showManageWsModal,
|
||||
showToast,
|
||||
showNewWsModal,
|
||||
});
|
||||
},
|
||||
[
|
||||
workspaces,
|
||||
navigate,
|
||||
setSelectedWorkspace,
|
||||
showManageWsModal,
|
||||
showToast,
|
||||
showNewWsModal,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function initialize() {
|
||||
try {
|
||||
const hidden = window.localStorage.getItem(CHECKLIST_HIDDEN);
|
||||
setIsHidden(!!hidden);
|
||||
// If the checklist is hidden, don't bother evaluating it.
|
||||
if (hidden) return;
|
||||
|
||||
// If the checklist is completed then dont continue and just show the completed state.
|
||||
const checklist = window.localStorage.getItem(CHECKLIST_STORAGE_KEY);
|
||||
const existingChecklist = checklist ? safeJsonParse(checklist, {}) : {};
|
||||
const isCompleted =
|
||||
Object.keys(existingChecklist).length === CHECKLIST_ITEMS().length;
|
||||
setIsCompleted(isCompleted);
|
||||
if (isCompleted) return;
|
||||
|
||||
// Otherwise, we can fetch workspaces for our checklist tasks as well
|
||||
// as determine if the create_workspace task is completed for pre-checking.
|
||||
const workspaces = await Workspace.all();
|
||||
setWorkspaces(workspaces);
|
||||
if (workspaces.length > 0) {
|
||||
existingChecklist["create_workspace"] = true;
|
||||
window.localStorage.setItem(
|
||||
CHECKLIST_STORAGE_KEY,
|
||||
JSON.stringify(existingChecklist)
|
||||
);
|
||||
}
|
||||
|
||||
evaluateChecklist(); // Evaluate checklist on mount.
|
||||
window.addEventListener(CHECKLIST_UPDATED_EVENT, evaluateChecklist);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
initialize();
|
||||
return () => {
|
||||
window.removeEventListener(CHECKLIST_UPDATED_EVENT, evaluateChecklist);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchWorkspaces = async () => {
|
||||
const workspaces = await Workspace.all();
|
||||
setWorkspaces(workspaces);
|
||||
};
|
||||
fetchWorkspaces();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCompleted) {
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
}, 5_000);
|
||||
}
|
||||
}, [isCompleted]);
|
||||
|
||||
const evaluateChecklist = useCallback(() => {
|
||||
try {
|
||||
const checklist = window.localStorage.getItem(CHECKLIST_STORAGE_KEY);
|
||||
if (!checklist) return;
|
||||
const completedItems = safeJsonParse(checklist, {});
|
||||
setCompletedCount(Object.keys(completedItems).length);
|
||||
setIsCompleted(
|
||||
Object.keys(completedItems).length === CHECKLIST_ITEMS().length
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
window.localStorage.setItem(CHECKLIST_HIDDEN, "true");
|
||||
if (containerRef?.current) containerRef.current.style.height = "0px";
|
||||
}, []);
|
||||
if (isHidden || loading) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="transition-height duration-300 h-[100%] overflow-y-hidden relative"
|
||||
>
|
||||
<div
|
||||
className={`${isCompleted ? "checklist-completed" : "hidden"} absolute top-0 left-0 w-full h-full p-2 z-10 transition-all duration-300`}
|
||||
>
|
||||
{isCompleted && (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<ConfettiExplosion force={0.25} duration={3000} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{}}
|
||||
className="bg-[rgba(54,70,61,0.5)] light:bg-[rgba(216,243,234,0.5)] w-full h-full flex items-center justify-center bg-theme-checklist-item-completed-bg/50 rounded-lg"
|
||||
>
|
||||
<p className="text-theme-checklist-item-completed-text text-lg font-bold">
|
||||
{t("main-page.checklist.completed")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`rounded-lg p-4 lg:p-6 bg-theme-home-bg-card relative ${isCompleted ? "blur-sm" : ""}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<h1 className="text-theme-home-text uppercase text-sm font-semibold">
|
||||
{t("main-page.checklist.title")}
|
||||
</h1>
|
||||
{CHECKLIST_ITEMS().length - completedCount > 0 && (
|
||||
<p className="text-theme-home-text-secondary text-xs">
|
||||
{CHECKLIST_ITEMS().length - completedCount}{" "}
|
||||
{t("main-page.checklist.tasksLeft")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-theme-home-text-secondary bg-theme-home-bg-button px-3 py-1 rounded-xl hover:bg-white/10 transition-colors text-xs light:bg-black-100"
|
||||
>
|
||||
{t("main-page.checklist.dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{CHECKLIST_ITEMS().map((item) => (
|
||||
<MemoizedChecklistItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
title={item.title}
|
||||
action={item.action}
|
||||
icon={item.icon}
|
||||
completed={item.completed}
|
||||
onAction={createItemHandler(item)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />}
|
||||
{selectedWorkspace && (
|
||||
<ManageWorkspace
|
||||
providedSlug={selectedWorkspace.slug}
|
||||
hideModal={() => {
|
||||
setSelectedWorkspace(null);
|
||||
hideManageWsModal();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import paths from "@/utils/paths";
|
||||
import Workspace from "@/models/workspace";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ExploreFeatures() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const chatWithAgent = async () => {
|
||||
const workspaces = await Workspace.all();
|
||||
if (workspaces.length > 0) {
|
||||
const firstWorkspace = workspaces[0];
|
||||
navigate(
|
||||
paths.workspace.chat(firstWorkspace.slug, {
|
||||
search: { action: "set-agent-chat" },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const buildAgentFlow = () => navigate(paths.agents.builder());
|
||||
const setSlashCommand = async () => {
|
||||
const workspaces = await Workspace.all();
|
||||
if (workspaces.length > 0) {
|
||||
const firstWorkspace = workspaces[0];
|
||||
navigate(
|
||||
paths.workspace.chat(firstWorkspace.slug, {
|
||||
search: { action: "open-new-slash-command-modal" },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const exploreSlashCommands = () => {
|
||||
window.open(paths.communityHub.viewMoreOfType("slash-commands"), "_blank");
|
||||
};
|
||||
|
||||
const setSystemPrompt = async () => {
|
||||
const workspaces = await Workspace.all();
|
||||
if (workspaces.length > 0) {
|
||||
const firstWorkspace = workspaces[0];
|
||||
navigate(
|
||||
paths.workspace.settings.chatSettings(firstWorkspace.slug, {
|
||||
search: { action: "focus-system-prompt" },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const managePromptVariables = () => {
|
||||
navigate(paths.settings.systemPromptVariables());
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-theme-home-text uppercase text-sm font-semibold mb-4">
|
||||
{t("main-page.exploreMore.title")}
|
||||
</h1>
|
||||
<div className="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<FeatureCard
|
||||
title={t("main-page.exploreMore.features.customAgents.title")}
|
||||
description={t(
|
||||
"main-page.exploreMore.features.customAgents.description"
|
||||
)}
|
||||
primaryAction={t(
|
||||
"main-page.exploreMore.features.customAgents.primaryAction"
|
||||
)}
|
||||
secondaryAction={t(
|
||||
"main-page.exploreMore.features.customAgents.secondaryAction"
|
||||
)}
|
||||
onPrimaryAction={chatWithAgent}
|
||||
onSecondaryAction={buildAgentFlow}
|
||||
/>
|
||||
<FeatureCard
|
||||
title={t("main-page.exploreMore.features.slashCommands.title")}
|
||||
description={t(
|
||||
"main-page.exploreMore.features.slashCommands.description"
|
||||
)}
|
||||
primaryAction={t(
|
||||
"main-page.exploreMore.features.slashCommands.primaryAction"
|
||||
)}
|
||||
secondaryAction={t(
|
||||
"main-page.exploreMore.features.slashCommands.secondaryAction"
|
||||
)}
|
||||
onPrimaryAction={setSlashCommand}
|
||||
onSecondaryAction={exploreSlashCommands}
|
||||
isNew={false}
|
||||
/>
|
||||
<FeatureCard
|
||||
title={t("main-page.exploreMore.features.systemPrompts.title")}
|
||||
description={t(
|
||||
"main-page.exploreMore.features.systemPrompts.description"
|
||||
)}
|
||||
primaryAction={t(
|
||||
"main-page.exploreMore.features.systemPrompts.primaryAction"
|
||||
)}
|
||||
secondaryAction={t(
|
||||
"main-page.exploreMore.features.systemPrompts.secondaryAction"
|
||||
)}
|
||||
onPrimaryAction={setSystemPrompt}
|
||||
onSecondaryAction={managePromptVariables}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureCard({
|
||||
title,
|
||||
description,
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
onPrimaryAction,
|
||||
onSecondaryAction,
|
||||
isNew,
|
||||
}) {
|
||||
return (
|
||||
<div className="border border-theme-home-border rounded-lg py-4 px-5 flex flex-col justify-between gap-y-4">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<h2 className="text-theme-home-text font-semibold flex items-center gap-x-2">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-theme-home-text-secondary text-sm">{description}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-[10px]">
|
||||
<button
|
||||
onClick={onPrimaryAction}
|
||||
className="w-full h-[36px] border border-white/20 light:border-theme-home-button-secondary-border light:hover:border-theme-home-button-secondary-border-hover text-white rounded-lg text-theme-home-button-primary-text text-sm font-medium flex items-center justify-center gap-x-2.5 transition-all duration-200 light:hover:bg-transparent hover:bg-theme-home-button-secondary-hover hover:text-theme-home-button-secondary-hover-text"
|
||||
>
|
||||
{primaryAction}
|
||||
</button>
|
||||
{secondaryAction && (
|
||||
<div className="relative w-full">
|
||||
{isNew && (
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 px-2 font-semibold rounded-md text-[10px] text-theme-checklist-item-text bg-theme-checklist-item-bg light:bg-white/60">
|
||||
New
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={onSecondaryAction}
|
||||
className="w-full h-[36px] bg-theme-home-button-secondary rounded-lg text-theme-home-button-secondary-text text-sm font-medium flex items-center justify-center transition-all duration-200 hover:bg-theme-home-button-secondary-hover hover:text-theme-home-button-secondary-hover-text"
|
||||
>
|
||||
{secondaryAction}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
import { ChatCenteredDots, FileArrowDown, Plus } from "@phosphor-icons/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Workspace from "@/models/workspace";
|
||||
import paths from "@/utils/paths";
|
||||
import { useManageWorkspaceModal } from "@/components/Modals/ManageWorkspace";
|
||||
import ManageWorkspace from "@/components/Modals/ManageWorkspace";
|
||||
import { useState } from "react";
|
||||
import { useNewWorkspaceModal } from "@/components/Modals/NewWorkspace";
|
||||
import NewWorkspaceModal from "@/components/Modals/NewWorkspace";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function QuickLinks() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { showModal } = useManageWorkspaceModal();
|
||||
const [selectedWorkspace, setSelectedWorkspace] = useState(null);
|
||||
const {
|
||||
showing: showingNewWsModal,
|
||||
showModal: showNewWsModal,
|
||||
hideModal: hideNewWsModal,
|
||||
} = useNewWorkspaceModal();
|
||||
|
||||
const sendChat = async () => {
|
||||
const workspaces = await Workspace.all();
|
||||
if (workspaces.length > 0) {
|
||||
const firstWorkspace = workspaces[0];
|
||||
navigate(paths.workspace.chat(firstWorkspace.slug));
|
||||
} else {
|
||||
showToast(t("main-page.noWorkspaceError"), "warning", {
|
||||
clear: true,
|
||||
});
|
||||
showNewWsModal();
|
||||
}
|
||||
};
|
||||
|
||||
const embedDocument = async () => {
|
||||
const workspaces = await Workspace.all();
|
||||
if (workspaces.length > 0) {
|
||||
const firstWorkspace = workspaces[0];
|
||||
setSelectedWorkspace(firstWorkspace);
|
||||
showModal();
|
||||
} else {
|
||||
showToast(t("main-page.noWorkspaceError"), "warning", {
|
||||
clear: true,
|
||||
});
|
||||
showNewWsModal();
|
||||
}
|
||||
};
|
||||
|
||||
const createWorkspace = () => {
|
||||
showNewWsModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-theme-home-text uppercase text-sm font-semibold mb-4">
|
||||
{t("main-page.quickLinks.title")}
|
||||
</h1>
|
||||
<div className="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={sendChat}
|
||||
className="h-[45px] text-sm font-semibold bg-theme-home-button-secondary rounded-lg text-theme-home-button-secondary-text flex items-center justify-center gap-x-2.5 transition-all duration-200 hover:bg-theme-home-button-secondary-hover hover:text-theme-home-button-secondary-hover-text"
|
||||
>
|
||||
<ChatCenteredDots size={16} />
|
||||
{t("main-page.quickLinks.sendChat")}
|
||||
</button>
|
||||
<button
|
||||
onClick={embedDocument}
|
||||
className="h-[45px] text-sm font-semibold bg-theme-home-button-secondary rounded-lg text-theme-home-button-secondary-text flex items-center justify-center gap-x-2.5 transition-all duration-200 hover:bg-theme-home-button-secondary-hover hover:text-theme-home-button-secondary-hover-text"
|
||||
>
|
||||
<FileArrowDown size={16} />
|
||||
{t("main-page.quickLinks.embedDocument")}
|
||||
</button>
|
||||
<button
|
||||
onClick={createWorkspace}
|
||||
className="h-[45px] text-sm font-semibold bg-theme-home-button-secondary rounded-lg text-theme-home-button-secondary-text flex items-center justify-center gap-x-2.5 transition-all duration-200 hover:bg-theme-home-button-secondary-hover hover:text-theme-home-button-secondary-hover-text"
|
||||
>
|
||||
<Plus size={16} />
|
||||
{t("main-page.quickLinks.createWorkspace")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{selectedWorkspace && (
|
||||
<ManageWorkspace
|
||||
providedSlug={selectedWorkspace.slug}
|
||||
hideModal={() => {
|
||||
setSelectedWorkspace(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
import paths from "@/utils/paths";
|
||||
import { ArrowCircleUpRight } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { KEYBOARD_SHORTCUTS_HELP_EVENT } from "@/utils/keyboardShortcuts";
|
||||
|
||||
export default function Resources() {
|
||||
const { t } = useTranslation();
|
||||
const showKeyboardShortcuts = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(KEYBOARD_SHORTCUTS_HELP_EVENT, { detail: { show: true } })
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-theme-home-text uppercase text-sm font-semibold mb-4">
|
||||
{t("main-page.resources.title")}
|
||||
</h1>
|
||||
<div className="flex gap-x-6">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer "
|
||||
href={paths.docs()}
|
||||
className="text-theme-home-text text-sm flex items-center gap-x-2 hover:opacity-70"
|
||||
>
|
||||
{t("main-page.resources.links.docs")}
|
||||
<ArrowCircleUpRight weight="fill" size={16} />
|
||||
</a>
|
||||
<a
|
||||
href={paths.github()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-theme-home-text text-sm flex items-center gap-x-2 hover:opacity-70"
|
||||
>
|
||||
{t("main-page.resources.links.star")}
|
||||
<ArrowCircleUpRight weight="fill" size={16} />
|
||||
</a>
|
||||
<button
|
||||
onClick={showKeyboardShortcuts}
|
||||
className="text-theme-home-text text-sm flex items-center gap-x-2 hover:opacity-70"
|
||||
>
|
||||
{t("main-page.resources.keyboardShortcuts")}
|
||||
<ArrowCircleUpRight weight="fill" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,209 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
import { Link } from "react-router-dom";
|
||||
import PlaceholderOne from "@/media/announcements/placeholder-1.png";
|
||||
import PlaceholderTwo from "@/media/announcements/placeholder-2.png";
|
||||
import PlaceholderThree from "@/media/announcements/placeholder-3.png";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* @typedef {Object} NewsItem
|
||||
* @property {string} title
|
||||
* @property {string|null} thumbnail_url
|
||||
* @property {string} short_description
|
||||
* @property {string|null} goto
|
||||
* @property {string|null} source
|
||||
* @property {string|null} date
|
||||
*/
|
||||
|
||||
const NEWS_CACHE_CONFIG = {
|
||||
articles: "https://cdn.anythingllm.com/support/announcements/list.txt",
|
||||
announcementsDir: "https://cdn.anythingllm.com/support/announcements",
|
||||
cacheKey: "anythingllm_announcements",
|
||||
ttl: 7 * 24 * 60 * 60 * 1000, // 1 week
|
||||
};
|
||||
|
||||
const PLACEHOLDERS = [PlaceholderOne, PlaceholderTwo, PlaceholderThree];
|
||||
|
||||
function randomPlaceholder() {
|
||||
return PLACEHOLDERS[Math.floor(Math.random() * PLACEHOLDERS.length)];
|
||||
}
|
||||
|
||||
export default function Updates() {
|
||||
const { t } = useTranslation();
|
||||
const { isLoading, news } = useNewsItems();
|
||||
if (isLoading || !news?.length) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-theme-home-text uppercase text-sm font-semibold mb-4">
|
||||
{t("main-page.announcements.title")}
|
||||
</h1>
|
||||
<div className="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{news.map((item, index) => (
|
||||
<AnnouncementCard
|
||||
key={index}
|
||||
thumbnail_url={item.thumbnail_url}
|
||||
title={item.title}
|
||||
subtitle={item.short_description}
|
||||
source={item.source}
|
||||
date={item.date}
|
||||
goto={item.goto}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isExternal(goto) {
|
||||
if (!goto) return false;
|
||||
const url = new URL(goto);
|
||||
return url.hostname !== window.location.hostname;
|
||||
}
|
||||
|
||||
function AnnouncementCard({
|
||||
thumbnail_url = null,
|
||||
title = "",
|
||||
subtitle = "",
|
||||
author = "AnythingLLM",
|
||||
date = null,
|
||||
goto = "#",
|
||||
}) {
|
||||
const placeHolderImage = randomPlaceholder();
|
||||
const isExternalLink = isExternal(goto);
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={goto}
|
||||
target={isExternalLink ? "_blank" : "_self"}
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<div className="bg-theme-home-update-card-bg rounded-xl p-4 flex gap-x-4 hover:bg-theme-home-update-card-hover transition-colors">
|
||||
<img
|
||||
src={thumbnail_url ?? placeHolderImage}
|
||||
alt={title}
|
||||
loading="lazy"
|
||||
onError={(e) => (e.target.src = placeHolderImage)}
|
||||
className="w-[80px] h-[80px] rounded-lg flex-shrink-0 object-cover"
|
||||
/>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h3 className="text-theme-home-text font-medium text-sm">{title}</h3>
|
||||
<p className="text-theme-home-text-secondary text-xs line-clamp-2">
|
||||
{subtitle}
|
||||
</p>
|
||||
<div className="flex items-center gap-x-4 text-xs text-theme-home-text-secondary">
|
||||
<span className="text-theme-home-update-source">{author}</span>
|
||||
<span>{date ?? "Recently"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached news from localStorage if it exists and is valid by ttl timestamp
|
||||
* @returns {null|NewsItem[]} - Array of news items
|
||||
*/
|
||||
function getCachedNews() {
|
||||
try {
|
||||
const cachedNews = localStorage.getItem(NEWS_CACHE_CONFIG.cacheKey);
|
||||
if (!cachedNews) return null;
|
||||
|
||||
/** @type {{news: NewsItem[]|null, timestamp: number|null}|null} */
|
||||
const parsedNews = safeJsonParse(cachedNews, null);
|
||||
if (!parsedNews || !parsedNews?.news?.length || !parsedNews.timestamp)
|
||||
return null;
|
||||
|
||||
const now = new Date();
|
||||
const cacheExpiration = new Date(
|
||||
parsedNews.timestamp + NEWS_CACHE_CONFIG.ttl
|
||||
);
|
||||
if (now < cacheExpiration) return parsedNews.news;
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("Error fetching cached news:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch news from remote source and cache it in localStorage
|
||||
* @returns {Promise<NewsItem[]|null>} - Array of news items
|
||||
*/
|
||||
async function fetchRemoteNews() {
|
||||
try {
|
||||
const latestArticleDateRef = await fetch(NEWS_CACHE_CONFIG.articles)
|
||||
.then((res) => {
|
||||
if (!res.ok)
|
||||
throw new Error(
|
||||
`${res.status} - Failed to fetch remote news from ${NEWS_CACHE_CONFIG.articles}`
|
||||
);
|
||||
return res.text();
|
||||
})
|
||||
.then((text) => text?.split("\n")?.shift()?.trim())
|
||||
.catch((err) => {
|
||||
console.error(err.message);
|
||||
return null;
|
||||
});
|
||||
if (!latestArticleDateRef) return null;
|
||||
|
||||
const dataURL = `${NEWS_CACHE_CONFIG.announcementsDir}/${latestArticleDateRef}${latestArticleDateRef.endsWith(".json") ? "" : ".json"}`;
|
||||
/** @type {NewsItem[]|null} */
|
||||
const announcementData = await fetch(dataURL)
|
||||
.then((res) => {
|
||||
if (!res.ok)
|
||||
throw new Error(
|
||||
`${res.status} - Failed to fetch remote news from ${dataURL}`
|
||||
);
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err.message);
|
||||
return [];
|
||||
});
|
||||
|
||||
if (!announcementData?.length) return null;
|
||||
localStorage.setItem(
|
||||
NEWS_CACHE_CONFIG.cacheKey,
|
||||
JSON.stringify({
|
||||
news: announcementData,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
);
|
||||
|
||||
return announcementData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching remote news:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{news: NewsItem[], isLoading: boolean}}
|
||||
*/
|
||||
function useNewsItems() {
|
||||
const [news, setNews] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchAnnouncements() {
|
||||
try {
|
||||
const cachedNews = getCachedNews();
|
||||
if (cachedNews) return setNews(cachedNews);
|
||||
|
||||
const remoteNews = await fetchRemoteNews();
|
||||
if (remoteNews) return setNews(remoteNews);
|
||||
} catch (error) {
|
||||
console.error("Error fetching cached news:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchAnnouncements();
|
||||
}, []);
|
||||
|
||||
return { news, isLoading };
|
||||
}
|
||||
@ -1,25 +1,321 @@
|
||||
import React from "react";
|
||||
import QuickLinks from "./QuickLinks";
|
||||
import ExploreFeatures from "./ExploreFeatures";
|
||||
import Updates from "./Updates";
|
||||
import Resources from "./Resources";
|
||||
import Checklist from "./Checklist";
|
||||
import React, { useState, useEffect, useRef, useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { SidebarMobileHeader } from "@/components/Sidebar";
|
||||
import PromptInput, {
|
||||
PROMPT_INPUT_EVENT,
|
||||
PROMPT_INPUT_ID,
|
||||
} from "@/components/WorkspaceChat/ChatContainer/PromptInput";
|
||||
import DnDFileUploaderWrapper, {
|
||||
DndUploaderContext,
|
||||
DnDFileUploaderProvider,
|
||||
PASTE_ATTACHMENT_EVENT,
|
||||
} from "@/components/WorkspaceChat/ChatContainer/DnDWrapper";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
LAST_VISITED_WORKSPACE,
|
||||
PENDING_HOME_MESSAGE,
|
||||
} from "@/utils/constants";
|
||||
import Workspace from "@/models/workspace";
|
||||
import paths from "@/utils/paths";
|
||||
import showToast from "@/utils/toast";
|
||||
import { safeJsonParse } from "@/utils/request";
|
||||
import QuickActions from "@/components/lib/QuickActions";
|
||||
import SuggestedMessages from "@/components/lib/SuggestedMessages";
|
||||
import useUser from "@/hooks/useUser";
|
||||
|
||||
async function getTargetWorkspace() {
|
||||
const lastVisited = safeJsonParse(
|
||||
localStorage.getItem(LAST_VISITED_WORKSPACE)
|
||||
);
|
||||
if (lastVisited?.slug) {
|
||||
const workspace = await Workspace.bySlug(lastVisited.slug);
|
||||
if (workspace) return workspace;
|
||||
}
|
||||
|
||||
const workspaces = await Workspace.all();
|
||||
return workspaces.length > 0 ? workspaces[0] : null;
|
||||
}
|
||||
|
||||
async function createDefaultWorkspace(workspaceName = "My Workspace") {
|
||||
const { workspace, message: errorMsg } = await Workspace.new({
|
||||
name: workspaceName,
|
||||
});
|
||||
if (!workspace) {
|
||||
showToast(errorMsg || "Failed to create workspace", "error");
|
||||
return null;
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useUser();
|
||||
const [workspace, setWorkspace] = useState(null);
|
||||
const [threadSlug, setThreadSlug] = useState(null);
|
||||
const [workspaceLoading, setWorkspaceLoading] = useState(true);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const pendingFilesRef = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
const ws = await getTargetWorkspace();
|
||||
if (ws) {
|
||||
const [suggestedMessages, pfpUrl] = await Promise.all([
|
||||
Workspace.getSuggestedMessages(ws.slug),
|
||||
Workspace.fetchPfp(ws.slug),
|
||||
]);
|
||||
setWorkspace({ ...ws, suggestedMessages, pfpUrl });
|
||||
}
|
||||
setWorkspaceLoading(false);
|
||||
}
|
||||
init();
|
||||
}, []);
|
||||
|
||||
// When workspace/thread becomes available and we have pending files, trigger upload
|
||||
useEffect(() => {
|
||||
if (workspace && threadSlug && pendingFilesRef.current.length > 0) {
|
||||
const files = pendingFilesRef.current;
|
||||
pendingFilesRef.current = [];
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(PASTE_ATTACHMENT_EVENT, { detail: { files } })
|
||||
);
|
||||
}
|
||||
}, [workspace, threadSlug]);
|
||||
|
||||
// Handle paste events when no thread exists yet
|
||||
useEffect(() => {
|
||||
if (threadSlug) return;
|
||||
|
||||
async function handlePaste(e) {
|
||||
const files = e.detail?.files;
|
||||
if (!files?.length) return;
|
||||
|
||||
pendingFilesRef.current = files;
|
||||
let ws = workspace;
|
||||
if (!ws) {
|
||||
ws = await createDefaultWorkspace(t("new-workspace.placeholder"));
|
||||
if (!ws) return;
|
||||
setWorkspace(ws);
|
||||
}
|
||||
const { thread } = await Workspace.threads.new(ws.slug);
|
||||
if (thread) setThreadSlug(thread.slug);
|
||||
}
|
||||
|
||||
window.addEventListener(PASTE_ATTACHMENT_EVENT, handlePaste);
|
||||
return () =>
|
||||
window.removeEventListener(PASTE_ATTACHMENT_EVENT, handlePaste);
|
||||
}, [workspace, threadSlug]);
|
||||
|
||||
async function handleDropWithoutWorkspace(acceptedFiles) {
|
||||
setDragging(false);
|
||||
pendingFilesRef.current = acceptedFiles;
|
||||
const ws = await createDefaultWorkspace(t("new-workspace.placeholder"));
|
||||
if (!ws) return;
|
||||
setWorkspace(ws);
|
||||
const { thread } = await Workspace.threads.new(ws.slug);
|
||||
if (thread) setThreadSlug(thread.slug);
|
||||
}
|
||||
|
||||
async function handleDropWithWorkspace(acceptedFiles) {
|
||||
setDragging(false);
|
||||
pendingFilesRef.current = acceptedFiles;
|
||||
const { thread } = await Workspace.threads.new(workspace.slug);
|
||||
if (thread) setThreadSlug(thread.slug);
|
||||
}
|
||||
|
||||
if (workspaceLoading) {
|
||||
return (
|
||||
<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-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspace && user?.role === "default") {
|
||||
return <NoWorkspacesAssigned />;
|
||||
}
|
||||
|
||||
if (workspace && threadSlug) {
|
||||
return (
|
||||
<DnDFileUploaderProvider workspace={workspace} threadSlug={threadSlug}>
|
||||
<HomeContent
|
||||
workspace={workspace}
|
||||
setWorkspace={setWorkspace}
|
||||
threadSlug={threadSlug}
|
||||
setThreadSlug={setThreadSlug}
|
||||
/>
|
||||
</DnDFileUploaderProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DndUploaderContext.Provider
|
||||
value={{
|
||||
files: [],
|
||||
ready: true,
|
||||
dragging,
|
||||
setDragging,
|
||||
onDrop: workspace
|
||||
? handleDropWithWorkspace
|
||||
: handleDropWithoutWorkspace,
|
||||
parseAttachments: () => [],
|
||||
}}
|
||||
>
|
||||
<HomeContent
|
||||
workspace={workspace}
|
||||
setWorkspace={setWorkspace}
|
||||
threadSlug={null}
|
||||
setThreadSlug={setThreadSlug}
|
||||
/>
|
||||
</DndUploaderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeContent({ workspace, setWorkspace, threadSlug, setThreadSlug }) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { files, parseAttachments } = useContext(DndUploaderContext);
|
||||
|
||||
useEffect(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(PROMPT_INPUT_EVENT, {
|
||||
detail: { messageContent: "", writeMode: "replace" },
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
async function submitMessage(message, attachments = []) {
|
||||
if (!message || loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
let targetWorkspace = workspace;
|
||||
let targetThread = threadSlug;
|
||||
|
||||
if (!targetWorkspace) {
|
||||
targetWorkspace = await createDefaultWorkspace(
|
||||
t("new-workspace.placeholder")
|
||||
);
|
||||
if (!targetWorkspace) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setWorkspace(targetWorkspace);
|
||||
}
|
||||
|
||||
if (!targetThread) {
|
||||
const { thread } = await Workspace.threads.new(targetWorkspace.slug);
|
||||
targetThread = thread?.slug;
|
||||
if (thread) setThreadSlug(thread.slug);
|
||||
}
|
||||
|
||||
sessionStorage.setItem(
|
||||
PENDING_HOME_MESSAGE,
|
||||
JSON.stringify({ message, attachments })
|
||||
);
|
||||
|
||||
if (targetThread) {
|
||||
navigate(paths.workspace.thread(targetWorkspace.slug, targetThread));
|
||||
} else {
|
||||
navigate(paths.workspace.chat(targetWorkspace.slug));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting message:", error);
|
||||
showToast("Failed to send message", "error");
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const currentMessage =
|
||||
document.getElementById(PROMPT_INPUT_ID)?.value?.trim() || "";
|
||||
await submitMessage(currentMessage, parseAttachments());
|
||||
}
|
||||
|
||||
function sendCommand({
|
||||
text = "",
|
||||
autoSubmit = false,
|
||||
writeMode = "replace",
|
||||
}) {
|
||||
if (autoSubmit) {
|
||||
submitMessage(text.trim());
|
||||
return;
|
||||
}
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(PROMPT_INPUT_EVENT, {
|
||||
detail: { messageContent: text, writeMode },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function handleEditWorkspace() {
|
||||
let targetWorkspace = workspace;
|
||||
|
||||
if (!targetWorkspace) {
|
||||
targetWorkspace = await createDefaultWorkspace(
|
||||
t("new-workspace.placeholder")
|
||||
);
|
||||
if (!targetWorkspace) return;
|
||||
setWorkspace(targetWorkspace);
|
||||
}
|
||||
|
||||
navigate(paths.workspace.settings.generalAppearance(targetWorkspace.slug));
|
||||
}
|
||||
|
||||
return (
|
||||
<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-container w-full h-full"
|
||||
className="transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
|
||||
>
|
||||
<div className="w-full h-full flex flex-col items-center overflow-y-auto no-scroll">
|
||||
<div className="w-full max-w-[1200px] flex flex-col gap-y-[24px] p-4 pt-16 md:p-12 md:pt-11">
|
||||
<Checklist />
|
||||
<QuickLinks />
|
||||
<ExploreFeatures />
|
||||
<Updates />
|
||||
<Resources />
|
||||
{isMobile && <SidebarMobileHeader />}
|
||||
<DnDFileUploaderWrapper>
|
||||
<div className="flex flex-col h-full w-full items-center justify-center">
|
||||
<div className="flex flex-col items-center w-full max-w-[750px]">
|
||||
<h1 className="text-white text-xl md:text-2xl mb-11 text-center">
|
||||
{t("main-page.greeting")}
|
||||
</h1>
|
||||
<PromptInput
|
||||
submit={handleSubmit}
|
||||
isStreaming={loading}
|
||||
sendCommand={sendCommand}
|
||||
attachments={files}
|
||||
centered={true}
|
||||
workspaceSlug={workspace?.slug}
|
||||
threadSlug={threadSlug}
|
||||
/>
|
||||
<QuickActions
|
||||
hasAvailableWorkspace={!!workspace}
|
||||
onCreateAgent={() => navigate(paths.settings.agentSkills())}
|
||||
onEditWorkspace={handleEditWorkspace}
|
||||
onUploadDocument={() =>
|
||||
document.getElementById("dnd-chat-file-uploader")?.click()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SuggestedMessages
|
||||
suggestedMessages={workspace?.suggestedMessages}
|
||||
sendCommand={sendCommand}
|
||||
/>
|
||||
</div>
|
||||
</DnDFileUploaderWrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoWorkspacesAssigned() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<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-[16px] bg-theme-bg-secondary w-full h-full overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col h-full w-full items-center justify-center">
|
||||
<p className="text-white/60 text-sm text-center whitespace-pre-line">
|
||||
{t("home.notAssigned")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -2,10 +2,8 @@ import React from "react";
|
||||
import PasswordModal, { usePasswordModal } from "@/components/Modals/Password";
|
||||
import { FullScreenLoader } from "@/components/Preloader";
|
||||
import Home from "./Home";
|
||||
import DefaultChatContainer from "@/components/DefaultChat";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import Sidebar, { SidebarMobileHeader } from "@/components/Sidebar";
|
||||
import { userFromStorage } from "@/utils/request";
|
||||
|
||||
export default function Main() {
|
||||
const { loading, requiresAuth, mode } = usePasswordModal();
|
||||
@ -14,11 +12,10 @@ export default function Main() {
|
||||
if (requiresAuth !== false)
|
||||
return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;
|
||||
|
||||
const user = userFromStorage();
|
||||
return (
|
||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
|
||||
{!isMobile ? <Sidebar /> : <SidebarMobileHeader />}
|
||||
{!!user && user?.role !== "admin" ? <DefaultChatContainer /> : <Home />}
|
||||
<Home />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -24,8 +24,7 @@ export default function SuggestedChatMessages({ slug }) {
|
||||
|
||||
const handleSaveSuggestedMessages = async () => {
|
||||
const validMessages = suggestedMessages.filter(
|
||||
(msg) =>
|
||||
msg?.heading?.trim()?.length > 0 || msg?.message?.trim()?.length > 0
|
||||
(msg) => msg?.message?.trim()?.length > 0
|
||||
);
|
||||
const { success, error } = await Workspace.setSuggestedMessages(
|
||||
slug,
|
||||
@ -35,6 +34,8 @@ export default function SuggestedChatMessages({ slug }) {
|
||||
showToast(`Failed to update welcome messages: ${error}`, "error");
|
||||
return;
|
||||
}
|
||||
setSuggestedMessages(validMessages);
|
||||
setEditingIndex(-1);
|
||||
showToast("Successfully updated welcome messages.", "success");
|
||||
setHasChanges(false);
|
||||
};
|
||||
@ -46,8 +47,8 @@ export default function SuggestedChatMessages({ slug }) {
|
||||
return;
|
||||
}
|
||||
const defaultMessage = {
|
||||
heading: t("general.message.heading"),
|
||||
message: t("general.message.body"),
|
||||
heading: "",
|
||||
message: `${t("general.message.heading")} ${t("general.message.body")}`,
|
||||
};
|
||||
setNewMessage(defaultMessage);
|
||||
setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]);
|
||||
@ -64,7 +65,21 @@ export default function SuggestedChatMessages({ slug }) {
|
||||
const startEditing = (e, index) => {
|
||||
e.preventDefault();
|
||||
setEditingIndex(index);
|
||||
setNewMessage({ ...suggestedMessages[index] });
|
||||
const suggestion = suggestedMessages[index];
|
||||
// Legacy messages may have a separate heading field. Merge it into the message
|
||||
// on edit so the user can manage everything in a single input going forward.
|
||||
if (suggestion.heading) {
|
||||
const merged = {
|
||||
heading: "",
|
||||
message: `${suggestion.heading} ${suggestion.message}`,
|
||||
};
|
||||
setNewMessage(merged);
|
||||
setSuggestedMessages(
|
||||
suggestedMessages.map((msg, i) => (i === index ? merged : msg))
|
||||
);
|
||||
} else {
|
||||
setNewMessage({ ...suggestion });
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveMessage = (index) => {
|
||||
@ -134,26 +149,16 @@ export default function SuggestedChatMessages({ slug }) {
|
||||
editingIndex === index ? "border-sky-400" : ""
|
||||
}`}
|
||||
>
|
||||
<p className="font-semibold">{suggestion.heading}</p>
|
||||
<p>{suggestion.message}</p>
|
||||
<p className="line-clamp-2 text-theme-text-primary">
|
||||
{suggestion?.heading ? `${suggestion.heading} ` : ""}
|
||||
{suggestion?.message ?? ""}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{editingIndex >= 0 && (
|
||||
<div className="flex flex-col gap-y-4 mr-2 mt-8">
|
||||
<div className="w-1/2">
|
||||
<label className="text-white text-sm font-semibold block mb-2">
|
||||
Heading
|
||||
</label>
|
||||
<input
|
||||
placeholder="Message heading"
|
||||
className="border-none bg-theme-settings-input-bg text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block p-2.5 w-full"
|
||||
value={newMessage.heading}
|
||||
name="heading"
|
||||
onChange={onEditChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<label className="text-white text-sm font-semibold block mb-2">
|
||||
Message
|
||||
|
||||
@ -9,6 +9,7 @@ export const SEEN_DOC_PIN_ALERT = "anythingllm_pinned_document_alert";
|
||||
export const SEEN_WATCH_ALERT = "anythingllm_watched_document_alert";
|
||||
export const LAST_VISITED_WORKSPACE = "anythingllm_last_visited_workspace";
|
||||
export const USER_PROMPT_INPUT_MAP = "anythingllm_user_prompt_input_map";
|
||||
export const PENDING_HOME_MESSAGE = "anythingllm_pending_home_message";
|
||||
|
||||
export const APPEARANCE_SETTINGS = "anythingllm_appearance_settings";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user