-
+
+
-
+
+ {isStreaming ? (
+
+ ) : (
+ <>
+
+
+ Send message
+
+
+ >
+ )}
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx
index d19d1f8d107..9fe241b8710 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx
@@ -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,18 @@ 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";
+import useUser from "@/hooks/useUser";
export default function ChatContainer({ workspace, knownHistory = [] }) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { user } = useUser();
const { threadSlug = null } = useParams();
const [loadingResponse, setLoadingResponse] = useState(false);
const [chatHistory, setChatHistory] = useState(knownHistory);
@@ -32,6 +42,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 +175,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
role: "assistant",
pending: true,
userMessage: text,
+ attachments,
animate: true,
},
];
@@ -174,6 +186,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,30 +323,74 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
handleWSS();
}, [socketId]);
+ const isEmpty =
+ chatHistory.length === 0 && !sessionStorage.getItem(PENDING_HOME_MESSAGE);
+
return (
{isMobile &&
}
-
- 0}
- />
-
-
+
+
+ {isEmpty ? (
+
+ {t("main-page.greeting")}
+
+ ) : (
+
+
+
+ )}
+
+ {isEmpty && (!user || user.role !== "default") && (
+
navigate(paths.settings.agentSkills())}
+ onEditWorkspace={() =>
+ navigate(
+ paths.workspace.settings.generalAppearance(workspace.slug)
+ )
+ }
+ onUploadDocument={() =>
+ document.getElementById("dnd-chat-file-uploader")?.click()
+ }
+ />
+ )}
+
+ {isEmpty && (
+
+ )}
+
diff --git a/frontend/src/components/WorkspaceChat/index.jsx b/frontend/src/components/WorkspaceChat/index.jsx
index 47a3bbebc2b..063a1d2da42 100644
--- a/frontend/src/components/WorkspaceChat/index.jsx
+++ b/frontend/src/components/WorkspaceChat/index.jsx
@@ -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
;
+ const hasPendingMessage = !!sessionStorage.getItem(PENDING_HOME_MESSAGE);
+ if (loadingHistory) {
+ if (hasPendingMessage) {
+ return (
+
+ );
+ }
+ return
;
+ }
if (!loading && !loadingHistory && !workspace) {
return (
<>
diff --git a/frontend/src/components/lib/QuickActions/index.jsx b/frontend/src/components/lib/QuickActions/index.jsx
new file mode 100644
index 00000000000..c8ecf330d9a
--- /dev/null
+++ b/frontend/src/components/lib/QuickActions/index.jsx
@@ -0,0 +1,52 @@
+import { useTranslation } from "react-i18next";
+import useUser from "@/hooks/useUser";
+
+/**
+ * Quick action buttons for home and empty workspace states.
+ * @param {Object} props
+ * @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({
+ onCreateAgent,
+ onEditWorkspace,
+ onUploadDocument,
+}) {
+ const { t } = useTranslation();
+ const { user } = useUser();
+ const role = user?.role ?? "admin";
+
+ return (
+
+ {role === "admin" && (
+
+ )}
+ {role !== "default" && (
+
+ )}
+
+
+ );
+}
+
+function QuickActionButton({ label, onClick }) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/frontend/src/components/lib/SuggestedMessages/index.jsx b/frontend/src/components/lib/SuggestedMessages/index.jsx
new file mode 100644
index 00000000000..75f120f762c
--- /dev/null
+++ b/frontend/src/components/lib/SuggestedMessages/index.jsx
@@ -0,0 +1,32 @@
+export default function SuggestedMessages({
+ suggestedMessages = [],
+ sendCommand,
+}) {
+ if (!suggestedMessages?.length) return null;
+
+ return (
+
+ {suggestedMessages.map((msg, index) => {
+ const text = msg.heading?.trim()
+ ? `${msg.heading.trim()} ${msg.message?.trim() || ""}`
+ : msg.message?.trim() || "";
+ if (!text) return null;
+
+ return (
+
+ {index > 0 && (
+
+ )}
+
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}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/locales/en/common.js b/frontend/src/locales/en/common.js
index e545ab7c145..ff7503d0ac2 100644
--- a/frontend/src/locales/en/common.js
+++ b/frontend/src/locales/en/common.js
@@ -139,6 +139,7 @@ const TRANSLATIONS = {
},
"main-page": {
+ greeting: "How may I make your day easier 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",
diff --git a/frontend/src/pages/Main/Home/Checklist/ChecklistItem/icons/SlashCommand.jsx b/frontend/src/pages/Main/Home/Checklist/ChecklistItem/icons/SlashCommand.jsx
deleted file mode 100644
index 4e981a082cd..00000000000
--- a/frontend/src/pages/Main/Home/Checklist/ChecklistItem/icons/SlashCommand.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from "react";
-
-export default function SlashCommandIcon({ className }) {
- return (
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/Main/Home/Checklist/ChecklistItem/index.jsx b/frontend/src/pages/Main/Home/Checklist/ChecklistItem/index.jsx
deleted file mode 100644
index 6e39148255e..00000000000
--- a/frontend/src/pages/Main/Home/Checklist/ChecklistItem/index.jsx
+++ /dev/null
@@ -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 (
-
- {Icon && (
-
-
-
- )}
-
-
- {title}
-
-
- {isCompleted ? (
-
-
-
- ) : (
-
- {action}
-
- )}
-
- );
-}
diff --git a/frontend/src/pages/Main/Home/Checklist/constants.js b/frontend/src/pages/Main/Home/Checklist/constants.js
deleted file mode 100644
index 667889bc6f3..00000000000
--- a/frontend/src/pages/Main/Home/Checklist/constants.js
+++ /dev/null
@@ -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,
- },
-];
diff --git a/frontend/src/pages/Main/Home/Checklist/index.jsx b/frontend/src/pages/Main/Home/Checklist/index.jsx
deleted file mode 100644
index 80e28557d64..00000000000
--- a/frontend/src/pages/Main/Home/Checklist/index.jsx
+++ /dev/null
@@ -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 (
-
-
- {isCompleted && (
-
-
-
- )}
-
-
- {t("main-page.checklist.completed")}
-
-
-
-
-
-
-
-
- {t("main-page.checklist.title")}
-
- {CHECKLIST_ITEMS().length - completedCount > 0 && (
-
- {CHECKLIST_ITEMS().length - completedCount}{" "}
- {t("main-page.checklist.tasksLeft")}
-
- )}
-
-
-
-
- {t("main-page.checklist.dismiss")}
-
-
-
-
- {CHECKLIST_ITEMS().map((item) => (
-
- ))}
-
-
- {showingNewWsModal &&
}
- {selectedWorkspace && (
-
{
- setSelectedWorkspace(null);
- hideManageWsModal();
- }}
- />
- )}
-
- );
-}
diff --git a/frontend/src/pages/Main/Home/ExploreFeatures/index.jsx b/frontend/src/pages/Main/Home/ExploreFeatures/index.jsx
deleted file mode 100644
index 7412d64a355..00000000000
--- a/frontend/src/pages/Main/Home/ExploreFeatures/index.jsx
+++ /dev/null
@@ -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 (
-
-
- {t("main-page.exploreMore.title")}
-
-
-
-
-
-
-
- );
-}
-
-function FeatureCard({
- title,
- description,
- primaryAction,
- secondaryAction,
- onPrimaryAction,
- onSecondaryAction,
- isNew,
-}) {
- return (
-
-
-
- {title}
-
-
{description}
-
-
-
- {primaryAction}
-
- {secondaryAction && (
-
- {isNew && (
-
- New
-
- )}
-
- {secondaryAction}
-
-
- )}
-
-
- );
-}
diff --git a/frontend/src/pages/Main/Home/QuickLinks/index.jsx b/frontend/src/pages/Main/Home/QuickLinks/index.jsx
deleted file mode 100644
index 206ac0d6c64..00000000000
--- a/frontend/src/pages/Main/Home/QuickLinks/index.jsx
+++ /dev/null
@@ -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 (
-
-
- {t("main-page.quickLinks.title")}
-
-
-
-
- {t("main-page.quickLinks.sendChat")}
-
-
-
- {t("main-page.quickLinks.embedDocument")}
-
-
-
- {t("main-page.quickLinks.createWorkspace")}
-
-
-
- {selectedWorkspace && (
-
{
- setSelectedWorkspace(null);
- }}
- />
- )}
-
- {showingNewWsModal && }
-
- );
-}
diff --git a/frontend/src/pages/Main/Home/Resources/index.jsx b/frontend/src/pages/Main/Home/Resources/index.jsx
deleted file mode 100644
index f85303390ca..00000000000
--- a/frontend/src/pages/Main/Home/Resources/index.jsx
+++ /dev/null
@@ -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 (
-
-
- {t("main-page.resources.title")}
-
-
-
- );
-}
diff --git a/frontend/src/pages/Main/Home/Updates/index.jsx b/frontend/src/pages/Main/Home/Updates/index.jsx
deleted file mode 100644
index 756d083ec21..00000000000
--- a/frontend/src/pages/Main/Home/Updates/index.jsx
+++ /dev/null
@@ -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 (
-
-
- {t("main-page.announcements.title")}
-
-
- {news.map((item, index) => (
-
- ))}
-
-
- );
-}
-
-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 (
-
-
-
(e.target.src = placeHolderImage)}
- className="w-[80px] h-[80px] rounded-lg flex-shrink-0 object-cover"
- />
-
-
{title}
-
- {subtitle}
-
-
- {author}
- {date ?? "Recently"}
-
-
-
-
- );
-}
-
-/**
- * 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
} - 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 };
-}
diff --git a/frontend/src/pages/Main/Home/index.jsx b/frontend/src/pages/Main/Home/index.jsx
index c0dd670f964..21700f69091 100644
--- a/frontend/src/pages/Main/Home/index.jsx
+++ b/frontend/src/pages/Main/Home/index.jsx
@@ -1,25 +1,315 @@
-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() {
+ const { workspace, message: errorMsg } = await Workspace.new({
+ name: "My Workspace",
+ });
+ if (!workspace) {
+ showToast(errorMsg || "Failed to create workspace", "error");
+ return null;
+ }
+ return workspace;
+}
export default function Home() {
+ 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();
+ 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();
+ 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 (
+
+ );
+ }
+
+ if (!workspace && user?.role === "default") {
+ return ;
+ }
+
+ if (workspace && threadSlug) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ [],
+ }}
+ >
+
+
+ );
+}
+
+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();
+ 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();
+ if (!targetWorkspace) return;
+ setWorkspace(targetWorkspace);
+ }
+
+ navigate(paths.workspace.settings.generalAppearance(targetWorkspace.slug));
+ }
+
return (
-
-
-
-
-
-
-
+ {isMobile &&
}
+
+
+
+
+ {t("main-page.greeting")}
+
+
+
navigate(paths.settings.agentSkills())}
+ onEditWorkspace={handleEditWorkspace}
+ onUploadDocument={() =>
+ document.getElementById("dnd-chat-file-uploader")?.click()
+ }
+ />
+
+
+
+
+ );
+}
+
+function NoWorkspacesAssigned() {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t("home.notAssigned")}
+
);
diff --git a/frontend/src/pages/Main/index.jsx b/frontend/src/pages/Main/index.jsx
index 12d4b9333c2..bd241eb4787 100644
--- a/frontend/src/pages/Main/index.jsx
+++ b/frontend/src/pages/Main/index.jsx
@@ -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 &&
}>;
- const user = userFromStorage();
return (
{!isMobile ? : }
- {!!user && user?.role !== "admin" ? : }
+
);
}
diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx
index 5616736dea5..4075a2efab6 100644
--- a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx
+++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx
@@ -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" : ""
}`}
>
-
{suggestion.heading}
-
{suggestion.message}
+
+ {suggestion?.heading ? `${suggestion.heading} ` : ""}
+ {suggestion?.message ?? ""}
+
))}
{editingIndex >= 0 && (
-
-
- Heading
-
-
-
Message
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 52f7108346d..f647ea584e8 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -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";