diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index ad7c0af2..f111e2a0 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -25,6 +25,7 @@ on: - 'embed/**/*' # Embed is submodule - 'browser-extension/**/*' # Chrome extension is submodule - 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images. + - 'extras/**/*' # Extra is just for news and other local content. jobs: push_multi_platform_to_registries: diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 7173dd42..7c6b0cc0 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['3000-mcp-compatibility'] # put your current branch to create a build. Core team only. + branches: ['3536-feat-new-chat-home-page'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' @@ -18,6 +18,7 @@ on: - 'embed/**/*' # Embed should be published to frontend (yarn build:publish) if any changes are introduced - 'browser-extension/**/*' # Chrome extension is submodule - 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images. + - 'extras/**/*' # Extra is just for news and other local content. jobs: push_multi_platform_to_registries: diff --git a/extras/support/announcements/2025-04-08.json b/extras/support/announcements/2025-04-08.json new file mode 100644 index 00000000..58535ecc --- /dev/null +++ b/extras/support/announcements/2025-04-08.json @@ -0,0 +1,26 @@ +[ + { + "thumbnail_url": "https://cdn.anythingllm.com/support/announcements/assets/mcp.jpg", + "title": "MCP Support", + "short_description": "Import and leverage MCP tools using AnythingLLM.", + "goto": "https://docs.anythingllm.com/mcp-compatibility/overview", + "author": "AnythingLLM", + "date": "April 8, 2025" + }, + { + "thumbnail_url": "https://blogs.nvidia.com/wp-content/uploads/2025/03/nv-raig-032525-nv-blog-1280x680-1-scaled.jpg", + "title": "NVIDIA NIM Support", + "short_description": "Unlock the power of NVIDIA NIM on Windows with RTX GPU in our latest update via the NVIDIA NIM LLM provider.", + "goto": "https://blogs.nvidia.com/blog/rtx-ai-garage-nim-blueprints-g-assist", + "author": "NVIDIA", + "date": "March 25, 2025" + }, + { + "thumbnail_url": null, + "title": "Community Hub Updates", + "short_description": "We refreshed the Community Hub with a new look and feel. Check it out!", + "goto": "https://hub.anythingllm.com", + "author": "AnythingLLM", + "date": "March 12, 2025" + } +] \ No newline at end of file diff --git a/extras/support/announcements/assets/mcp.jpg b/extras/support/announcements/assets/mcp.jpg new file mode 100644 index 00000000..b0abd8a9 Binary files /dev/null and b/extras/support/announcements/assets/mcp.jpg differ diff --git a/extras/support/announcements/list.txt b/extras/support/announcements/list.txt new file mode 100644 index 00000000..03eac2b6 --- /dev/null +++ b/extras/support/announcements/list.txt @@ -0,0 +1 @@ +2025-04-08.json \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 1f51aab2..cab7239d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "pluralize": "^8.0.0", "react": "^18.2.0", "react-beautiful-dnd": "13.1.1", + "react-confetti-explosion": "^2.1.2", "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx index c1aa3dcd..afb1c041 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx @@ -49,6 +49,16 @@ export function AvailableAgents({ }) { const formRef = useRef(null); const agentSessionActive = useIsAgentSessionActive(); + + /* + * @checklist-item + * If the URL has the #agent hash, open the agent menu for the user + * automatically when the component mounts. + */ + useEffect(() => { + if (window.location.hash === "#agent" && !showing) handleAgentClick(); + }, [promptRef.current]); + useEffect(() => { function listenForOutsideClick() { if (!showing || !formRef.current) return false; @@ -64,6 +74,12 @@ export function AvailableAgents({ setShowing(false); }; + const handleAgentClick = () => { + setShowing(false); + sendCommand("@agent ", false); + promptRef?.current?.focus(); + }; + if (agentSessionActive) return null; return ( <> @@ -74,11 +90,7 @@ export function AvailableAgents({ className="w-[600px] p-2 bg-theme-action-menu-bg rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex" > + )} + + ); +} diff --git a/frontend/src/pages/Main/Home/Checklist/constants.js b/frontend/src/pages/Main/Home/Checklist/constants.js new file mode 100644 index 00000000..84708970 --- /dev/null +++ b/frontend/src/pages/Main/Home/Checklist/constants.js @@ -0,0 +1,162 @@ +import { + SquaresFour, + ChatDots, + Files, + ChatCenteredText, + UsersThree, +} from "@phosphor-icons/react"; +import SlashCommandIcon from "./ChecklistItem/icons/SlashCommand"; +import paths from "@/utils/paths"; +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 + */ + +/** @type {ChecklistItem[]} */ +export const CHECKLIST_ITEMS = [ + { + id: "create_workspace", + title: "Create a workspace", + description: "Create your first workspace to get started", + action: "Create", + handler: ({ showNewWsModal = noop }) => { + showNewWsModal(); + return true; + }, + icon: SquaresFour, + }, + { + id: "send_chat", + title: "Send a chat", + description: "Start a conversation with your AI assistant", + action: "Chat", + handler: ({ + workspaces = [], + navigate = noop, + showToast = noop, + showNewWsModal = noop, + }) => { + if (workspaces.length === 0) { + showToast( + "Please create a workspace before starting a chat.", + "warning", + { clear: true } + ); + showNewWsModal(); + return false; + } + navigate(paths.workspace.chat(workspaces[0].slug)); + return true; + }, + icon: ChatDots, + }, + { + id: "embed_document", + title: "Embed a document", + description: "Add your first document to your workspace", + action: "Embed", + handler: ({ + workspaces = [], + setSelectedWorkspace = noop, + showManageWsModal = noop, + showToast = noop, + showNewWsModal = noop, + }) => { + if (workspaces.length === 0) { + showToast( + "Please create a workspace before embedding documents.", + "warning", + { clear: true } + ); + showNewWsModal(); + return false; + } + setSelectedWorkspace(workspaces[0]); + showManageWsModal(); + return true; + }, + icon: Files, + }, + { + id: "setup_system_prompt", + title: "Set up a system prompt", + description: "Configure your AI assistant's behavior", + action: "Set Up", + handler: ({ + workspaces = [], + navigate = noop, + showNewWsModal = noop, + showToast = noop, + }) => { + if (workspaces.length === 0) { + showToast( + "Please create a workspace before setting up system prompts.", + "warning", + { clear: true } + ); + showNewWsModal(); + return false; + } + navigate(paths.workspace.settings.chatSettings(workspaces[0].slug)); + window.location.hash = "#system-prompts"; + return true; + }, + icon: ChatCenteredText, + }, + { + id: "define_slash_command", + title: "Define a slash command", + description: "Create custom commands for your assistant", + action: "Define", + handler: ({ + workspaces = [], + navigate = noop, + showNewWsModal = noop, + showToast = noop, + }) => { + if (workspaces.length === 0) { + showToast( + "Please create a workspace before setting up slash commands.", + "warning", + { clear: true } + ); + showNewWsModal(); + return false; + } + navigate(paths.workspace.chat(workspaces[0].slug)); + window.location.hash = "#slash-commands"; + return true; + }, + icon: SlashCommandIcon, + }, + { + id: "visit_community", + title: "Visit Community Hub", + description: "Explore community resources and templates", + action: "Browse", + handler: () => window.open(paths.communityHub.website(), "_blank"), + icon: UsersThree, + }, +]; diff --git a/frontend/src/pages/Main/Home/Checklist/index.jsx b/frontend/src/pages/Main/Home/Checklist/index.jsx new file mode 100644 index 00000000..7dde88c8 --- /dev/null +++ b/frontend/src/pages/Main/Home/Checklist/index.jsx @@ -0,0 +1,213 @@ +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"; + +const MemoizedChecklistItem = React.memo(ChecklistItem); +export default function Checklist() { + 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 && ( +
+ +
+ )} +
+

+ You're on your way to becoming an AnythingLLM expert! +

+
+
+ +
+
+
+

+ Getting Started +

+ {CHECKLIST_ITEMS.length - completedCount > 0 && ( +

+ {CHECKLIST_ITEMS.length - completedCount} tasks left +

+ )} +
+ +
+ +
+
+
+ {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 new file mode 100644 index 00000000..a1ca4330 --- /dev/null +++ b/frontend/src/pages/Main/Home/ExploreFeatures/index.jsx @@ -0,0 +1,124 @@ +import { useNavigate } from "react-router-dom"; +import paths from "@/utils/paths"; +import Workspace from "@/models/workspace"; + +export default function ExploreFeatures() { + 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)); + window.location.hash = "#agent"; + } + }; + + 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)); + window.location.hash = "#slash-commands"; + } + }; + + 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)); + window.location.hash = "#system-prompts"; + } + }; + + const managePromptVariables = () => { + navigate(paths.settings.systemPromptVariables()); + }; + + return ( +
+

+ Explore more features +

+
+ + + +
+
+ ); +} + +function FeatureCard({ + title, + description, + primaryAction, + secondaryAction, + onPrimaryAction, + onSecondaryAction, + isNew, +}) { + return ( +
+
+

+ {title} +

+

{description}

+
+
+ + {secondaryAction && ( +
+ {isNew && ( +
+ New +
+ )} + +
+ )} +
+
+ ); +} diff --git a/frontend/src/pages/Main/Home/QuickLinks/index.jsx b/frontend/src/pages/Main/Home/QuickLinks/index.jsx new file mode 100644 index 00000000..eaa457d9 --- /dev/null +++ b/frontend/src/pages/Main/Home/QuickLinks/index.jsx @@ -0,0 +1,98 @@ +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"; + +export default function QuickLinks() { + 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( + "Please create a workspace before starting a chat.", + "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( + "Please create a workspace before embedding documents.", + "warning", + { clear: true } + ); + showNewWsModal(); + } + }; + + const createWorkspace = () => { + showNewWsModal(); + }; + + return ( +
+

+ Quick Links +

+
+ + + +
+ + {selectedWorkspace && ( + { + setSelectedWorkspace(null); + }} + /> + )} + + {showingNewWsModal && } +
+ ); +} diff --git a/frontend/src/pages/Main/Home/Resources/index.jsx b/frontend/src/pages/Main/Home/Resources/index.jsx new file mode 100644 index 00000000..44d6ec47 --- /dev/null +++ b/frontend/src/pages/Main/Home/Resources/index.jsx @@ -0,0 +1,32 @@ +import paths from "@/utils/paths"; +import { ArrowCircleUpRight } from "@phosphor-icons/react"; + +export default function Resources() { + return ( +
+

+ Resources +

+
+ + Docs + + + + Star on Github + + +
+
+ ); +} diff --git a/frontend/src/pages/Main/Home/Updates/index.jsx b/frontend/src/pages/Main/Home/Updates/index.jsx new file mode 100644 index 00000000..0732ea1f --- /dev/null +++ b/frontend/src/pages/Main/Home/Updates/index.jsx @@ -0,0 +1,225 @@ +import { useEffect, useState } from "react"; +import { safeJsonParse } from "@/utils/request"; +import { ArrowSquareOut } from "@phosphor-icons/react"; +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"; + +/** + * @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 { isLoading, news } = useNewsItems(); + if (isLoading || !news?.length) return null; + + return ( +
+

+ Updates & Announcements +

+
+ {news.map((item, index) => ( + + ))} +
+
+ ); +} + +function getSource(goto) { + if (!goto) return null; + const url = new URL(goto); + return url.hostname; +} + +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 source = getSource(goto); + const isExternalLink = isExternal(goto); + + return ( + +
+ {title} (e.target.src = placeHolderImage)} + className="w-[80px] h-[80px] rounded-lg flex-shrink-0 object-cover" + /> +
+
+

{source}

+ {isExternalLink && ( + + )} +
+

{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 new file mode 100644 index 00000000..63cd618f --- /dev/null +++ b/frontend/src/pages/Main/Home/index.jsx @@ -0,0 +1,26 @@ +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 { isMobile } from "react-device-detect"; + +export default function Home() { + return ( +
+
+
+ + + + + +
+
+
+ ); +} diff --git a/frontend/src/pages/Main/index.jsx b/frontend/src/pages/Main/index.jsx index 5a802b6f..12d4b933 100644 --- a/frontend/src/pages/Main/index.jsx +++ b/frontend/src/pages/Main/index.jsx @@ -1,23 +1,24 @@ import React from "react"; -import DefaultChatContainer from "@/components/DefaultChat"; -import Sidebar from "@/components/Sidebar"; import PasswordModal, { usePasswordModal } from "@/components/Modals/Password"; -import { isMobile } from "react-device-detect"; import { FullScreenLoader } from "@/components/Preloader"; -import UserMenu from "@/components/UserMenu"; +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(); if (loading) return ; - if (requiresAuth !== false) { + if (requiresAuth !== false) return <>{requiresAuth !== null && }; - } + const user = userFromStorage(); return (
- {!isMobile && } - + {!isMobile ? : } + {!!user && user?.role !== "admin" ? : }
); } diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx index f5873594..5b9ca404 100644 --- a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { chatPrompt } from "@/utils/chat"; import { useTranslation } from "react-i18next"; import SystemPromptVariable from "@/models/systemPromptVariable"; @@ -11,6 +11,7 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) { const [availableVariables, setAvailableVariables] = useState([]); const [prompt, setPrompt] = useState(chatPrompt(workspace)); const [isEditing, setIsEditing] = useState(false); + const promptRef = useRef(null); useEffect(() => { async function setupVariableHighlighting() { @@ -20,6 +21,18 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) { setupVariableHighlighting(); }, []); + useEffect(() => { + if (window.location.hash === "#system-prompts") { + setIsEditing(true); + } + }, []); + + useEffect(() => { + if (isEditing && promptRef.current) { + promptRef.current.focus(); + } + }, [isEditing]); + return (
@@ -73,6 +86,7 @@ export default function ChatPromptSettings({ workspace, setHasChanges }) { {isEditing ? (