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,
|
hasNext,
|
||||||
ctrlPressed = false,
|
ctrlPressed = false,
|
||||||
}) {
|
}) {
|
||||||
const { slug, threadSlug = null } = useParams();
|
const { slug: urlSlug, threadSlug = null } = useParams();
|
||||||
|
const workspaceSlug = workspace?.slug ?? urlSlug;
|
||||||
const optionsContainer = useRef(null);
|
const optionsContainer = useRef(null);
|
||||||
const [showOptions, setShowOptions] = useState(false);
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
const linkTo = !thread.slug
|
const linkTo = thread.virtual
|
||||||
? paths.workspace.chat(slug)
|
? "/"
|
||||||
: paths.workspace.thread(slug, thread.slug);
|
: !thread.slug
|
||||||
|
? paths.workspace.chat(workspaceSlug)
|
||||||
|
: paths.workspace.thread(workspaceSlug, thread.slug);
|
||||||
|
|
||||||
const { ref } = useScrollActiveItemIntoView({
|
const { ref } = useScrollActiveItemIntoView({
|
||||||
isActive,
|
isActive,
|
||||||
@ -114,7 +117,7 @@ export default function ThreadItem({
|
|||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{!!thread.slug && !thread.deleted && (
|
{!!thread.slug && !thread.deleted && !thread.virtual && (
|
||||||
<div ref={optionsContainer} className="flex items-center">
|
<div ref={optionsContainer} className="flex items-center">
|
||||||
{" "}
|
{" "}
|
||||||
{/* Added flex and items-center */}
|
{/* Added flex and items-center */}
|
||||||
|
|||||||
@ -7,7 +7,10 @@ import ThreadItem from "./ThreadItem";
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
export const THREAD_RENAME_EVENT = "renameThread";
|
export const THREAD_RENAME_EVENT = "renameThread";
|
||||||
|
|
||||||
export default function ThreadContainer({ workspace }) {
|
export default function ThreadContainer({
|
||||||
|
workspace,
|
||||||
|
isVirtualThread = false,
|
||||||
|
}) {
|
||||||
const { threadSlug = null } = useParams();
|
const { threadSlug = null } = useParams();
|
||||||
const [threads, setThreads] = useState([]);
|
const [threads, setThreads] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -109,6 +112,12 @@ export default function ThreadContainer({ workspace }) {
|
|||||||
}, 500);
|
}, 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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col bg-pulse w-full h-10 items-center justify-center">
|
<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(
|
const activeThreadIdx = getActiveThreadIdx();
|
||||||
(thread) => thread?.slug === threadSlug
|
|
||||||
)
|
|
||||||
? threads.findIndex((thread) => thread?.slug === threadSlug) + 1
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col" role="list" aria-label="Threads">
|
<div className="flex flex-col" role="list" aria-label="Threads">
|
||||||
@ -129,8 +134,9 @@ export default function ThreadContainer({ workspace }) {
|
|||||||
idx={0}
|
idx={0}
|
||||||
activeIdx={activeThreadIdx}
|
activeIdx={activeThreadIdx}
|
||||||
isActive={activeThreadIdx === 0}
|
isActive={activeThreadIdx === 0}
|
||||||
|
workspace={workspace}
|
||||||
thread={{ slug: null, name: "default" }}
|
thread={{ slug: null, name: "default" }}
|
||||||
hasNext={threads.length > 0}
|
hasNext={threads.length > 0 || isVirtualThread}
|
||||||
/>
|
/>
|
||||||
{threads.map((thread, i) => (
|
{threads.map((thread, i) => (
|
||||||
<ThreadItem
|
<ThreadItem
|
||||||
@ -143,9 +149,19 @@ export default function ThreadContainer({ workspace }) {
|
|||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
onRemove={removeThread}
|
onRemove={removeThread}
|
||||||
thread={thread}
|
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
|
<DeleteAllThreadButton
|
||||||
ctrlPressed={ctrlPressed}
|
ctrlPressed={ctrlPressed}
|
||||||
threads={threads}
|
threads={threads}
|
||||||
|
|||||||
@ -6,13 +6,14 @@ import ManageWorkspace, {
|
|||||||
useManageWorkspaceModal,
|
useManageWorkspaceModal,
|
||||||
} from "../../Modals/ManageWorkspace";
|
} from "../../Modals/ManageWorkspace";
|
||||||
import paths from "@/utils/paths";
|
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 { GearSix, UploadSimple, DotsSixVertical } from "@phosphor-icons/react";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import ThreadContainer from "./ThreadContainer";
|
import ThreadContainer from "./ThreadContainer";
|
||||||
import { useMatch } from "react-router-dom";
|
|
||||||
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
||||||
import showToast from "@/utils/toast";
|
import showToast from "@/utils/toast";
|
||||||
|
import { LAST_VISITED_WORKSPACE } from "@/utils/constants";
|
||||||
|
import { safeJsonParse } from "@/utils/request";
|
||||||
|
|
||||||
export default function ActiveWorkspaces() {
|
export default function ActiveWorkspaces() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -23,6 +24,7 @@ export default function ActiveWorkspaces() {
|
|||||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const isInWorkspaceSettings = !!useMatch("/workspace/:slug/settings/:tab");
|
const isInWorkspaceSettings = !!useMatch("/workspace/:slug/settings/:tab");
|
||||||
|
const isHomePage = !!useMatch("/");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getWorkspaces() {
|
async function getWorkspaces() {
|
||||||
@ -71,6 +73,20 @@ export default function ActiveWorkspaces() {
|
|||||||
reorderWorkspaces(result.source.index, result.destination.index);
|
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 (
|
return (
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
<Droppable droppableId="workspaces">
|
<Droppable droppableId="workspaces">
|
||||||
@ -83,7 +99,8 @@ export default function ActiveWorkspaces() {
|
|||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
>
|
>
|
||||||
{workspaces.map((workspace, index) => {
|
{workspaces.map((workspace, index) => {
|
||||||
const isActive = workspace.slug === slug;
|
const isVirtuallyActive = workspace.slug === virtualActiveSlug;
|
||||||
|
const isActive = workspace.slug === slug || isVirtuallyActive;
|
||||||
return (
|
return (
|
||||||
<Draggable
|
<Draggable
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
@ -191,6 +208,7 @@ export default function ActiveWorkspaces() {
|
|||||||
<ThreadContainer
|
<ThreadContainer
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
isVirtualThread={isVirtuallyActive}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import { useManageWorkspaceModal } from "../../../Modals/ManageWorkspace";
|
|||||||
import ManageWorkspace from "../../../Modals/ManageWorkspace";
|
import ManageWorkspace from "../../../Modals/ManageWorkspace";
|
||||||
import { ArrowDown } from "@phosphor-icons/react";
|
import { ArrowDown } from "@phosphor-icons/react";
|
||||||
import debounce from "lodash.debounce";
|
import debounce from "lodash.debounce";
|
||||||
import useUser from "@/hooks/useUser";
|
|
||||||
import Chartable from "./Chartable";
|
import Chartable from "./Chartable";
|
||||||
import Workspace from "@/models/workspace";
|
import Workspace from "@/models/workspace";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
@ -21,7 +20,6 @@ import paths from "@/utils/paths";
|
|||||||
import Appearance from "@/models/appearance";
|
import Appearance from "@/models/appearance";
|
||||||
import useTextSize from "@/hooks/useTextSize";
|
import useTextSize from "@/hooks/useTextSize";
|
||||||
import useChatHistoryScrollHandle from "@/hooks/useChatHistoryScrollHandle";
|
import useChatHistoryScrollHandle from "@/hooks/useChatHistoryScrollHandle";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment";
|
import { useChatMessageAlignment } from "@/hooks/useChatMessageAlignment";
|
||||||
import { ThoughtExpansionProvider } from "./ThoughtContainer";
|
import { ThoughtExpansionProvider } from "./ThoughtContainer";
|
||||||
|
|
||||||
@ -32,16 +30,13 @@ export default forwardRef(function (
|
|||||||
sendCommand,
|
sendCommand,
|
||||||
updateHistory,
|
updateHistory,
|
||||||
regenerateAssistantMessage,
|
regenerateAssistantMessage,
|
||||||
hasAttachments = false,
|
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const { t } = useTranslation();
|
|
||||||
const lastScrollTopRef = useRef(0);
|
const lastScrollTopRef = useRef(0);
|
||||||
const chatHistoryRef = useRef(null);
|
const chatHistoryRef = useRef(null);
|
||||||
const { user } = useUser();
|
|
||||||
const { threadSlug = null } = useParams();
|
const { threadSlug = null } = useParams();
|
||||||
const { showing, showModal, hideModal } = useManageWorkspaceModal();
|
const { showing, hideModal } = useManageWorkspaceModal();
|
||||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||||
const isStreaming = history[history.length - 1]?.animate;
|
const isStreaming = history[history.length - 1]?.animate;
|
||||||
@ -98,10 +93,6 @@ export default forwardRef(function (
|
|||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSendSuggestedMessage = (heading, message) => {
|
|
||||||
sendCommand({ text: `${heading} ${message}`, autoSubmit: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveEditedMessage = async ({
|
const saveEditedMessage = async ({
|
||||||
editedMessage,
|
editedMessage,
|
||||||
chatId,
|
chatId,
|
||||||
@ -197,46 +188,6 @@ export default forwardRef(function (
|
|||||||
[compiledHistory.length, lastMessageInfo]
|
[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 (
|
return (
|
||||||
<ThoughtExpansionProvider>
|
<ThoughtExpansionProvider>
|
||||||
<div
|
<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.
|
* Builds the history of messages for the chat.
|
||||||
* This is mostly useful for rendering the history in a way that is easy to understand.
|
* 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")}
|
data-tooltip-content={t("chat_window.agents")}
|
||||||
aria-label={t("chat_window.agents")}
|
aria-label={t("chat_window.agents")}
|
||||||
onClick={() => setShowAgents(!showing)}
|
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" : ""
|
showing ? "!opacity-100" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<At
|
<At
|
||||||
color="var(--theme-sidebar-footer-icon-fill)"
|
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
|
<Tooltip
|
||||||
id="tooltip-agent-list-btn"
|
id="tooltip-agent-list-btn"
|
||||||
@ -47,6 +47,7 @@ export function AvailableAgents({
|
|||||||
setShowing,
|
setShowing,
|
||||||
sendCommand,
|
sendCommand,
|
||||||
promptRef,
|
promptRef,
|
||||||
|
centered = false,
|
||||||
}) {
|
}) {
|
||||||
const formRef = useRef(null);
|
const formRef = useRef(null);
|
||||||
const agentSessionActive = useIsAgentSessionActive();
|
const agentSessionActive = useIsAgentSessionActive();
|
||||||
@ -88,10 +89,16 @@ export function AvailableAgents({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div hidden={!showing}>
|
<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
|
<div
|
||||||
ref={formRef}
|
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
|
<button
|
||||||
onClick={handleAgentClick}
|
onClick={handleAgentClick}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { X, CircleNotch, Warning } from "@phosphor-icons/react";
|
import { X, CircleNotch, Warning } from "@phosphor-icons/react";
|
||||||
import Workspace from "@/models/workspace";
|
import Workspace from "@/models/workspace";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { nFormatter } from "@/utils/numbers";
|
import { nFormatter } from "@/utils/numbers";
|
||||||
import showToast from "@/utils/toast";
|
import showToast from "@/utils/toast";
|
||||||
import pluralize from "pluralize";
|
import pluralize from "pluralize";
|
||||||
@ -17,13 +16,14 @@ export default function ParsedFilesMenu({
|
|||||||
setCurrentTokens,
|
setCurrentTokens,
|
||||||
contextWindow,
|
contextWindow,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
workspaceSlug,
|
||||||
|
threadSlug = null,
|
||||||
}) {
|
}) {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const canEmbed = !user || user.role !== "default";
|
const canEmbed = !user || user.role !== "default";
|
||||||
const initialContextWindowLimitExceeded =
|
const initialContextWindowLimitExceeded =
|
||||||
contextWindow &&
|
contextWindow &&
|
||||||
currentTokens >= contextWindow * Workspace.maxContextWindowLimit;
|
currentTokens >= contextWindow * Workspace.maxContextWindowLimit;
|
||||||
const { slug, threadSlug = null } = useParams();
|
|
||||||
const [isEmbedding, setIsEmbedding] = useState(false);
|
const [isEmbedding, setIsEmbedding] = useState(false);
|
||||||
const [embedProgress, setEmbedProgress] = useState(1);
|
const [embedProgress, setEmbedProgress] = useState(1);
|
||||||
const [contextWindowLimitExceeded, setContextWindowLimitExceeded] = useState(
|
const [contextWindowLimitExceeded, setContextWindowLimitExceeded] = useState(
|
||||||
@ -35,7 +35,7 @@ export default function ParsedFilesMenu({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!file?.id) return;
|
if (!file?.id) return;
|
||||||
|
|
||||||
const success = await Workspace.deleteParsedFiles(slug, [file.id]);
|
const success = await Workspace.deleteParsedFiles(workspaceSlug, [file.id]);
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
|
|
||||||
// Update the local files list and current tokens
|
// Update the local files list and current tokens
|
||||||
@ -48,7 +48,7 @@ export default function ParsedFilesMenu({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
const { currentContextTokenCount } = await Workspace.getParsedFiles(
|
const { currentContextTokenCount } = await Workspace.getParsedFiles(
|
||||||
slug,
|
workspaceSlug,
|
||||||
threadSlug
|
threadSlug
|
||||||
);
|
);
|
||||||
const newContextWindowLimitExceeded =
|
const newContextWindowLimitExceeded =
|
||||||
@ -73,7 +73,7 @@ export default function ParsedFilesMenu({
|
|||||||
let completed = 0;
|
let completed = 0;
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
files.map((file) =>
|
files.map((file) =>
|
||||||
Workspace.embedParsedFile(slug, file.id).then(() => {
|
Workspace.embedParsedFile(workspaceSlug, file.id).then(() => {
|
||||||
completed++;
|
completed++;
|
||||||
setEmbedProgress(completed + 1);
|
setEmbedProgress(completed + 1);
|
||||||
})
|
})
|
||||||
@ -81,7 +81,7 @@ export default function ParsedFilesMenu({
|
|||||||
);
|
);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
const { currentContextTokenCount } = await Workspace.getParsedFiles(
|
const { currentContextTokenCount } = await Workspace.getParsedFiles(
|
||||||
slug,
|
workspaceSlug,
|
||||||
threadSlug
|
threadSlug
|
||||||
);
|
);
|
||||||
setCurrentTokens(currentContextTokenCount);
|
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.
|
* This is a simple proxy component that clicks on the DnD file uploader for the user.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function AttachItem() {
|
export default function AttachItem({
|
||||||
|
workspaceSlug = null,
|
||||||
|
workspaceThreadSlug = null,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useTheme();
|
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 tooltipRef = useRef(null);
|
||||||
const [isEmbedding, setIsEmbedding] = useState(false);
|
const [isEmbedding, setIsEmbedding] = useState(false);
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
@ -93,7 +98,7 @@ export default function AttachItem() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<PaperclipHorizontal
|
<PaperclipHorizontal
|
||||||
color="var(--theme-sidebar-footer-icon-fill)"
|
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 && (
|
{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">
|
<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}
|
currentTokens={currentTokens}
|
||||||
setCurrentTokens={setCurrentTokens}
|
setCurrentTokens={setCurrentTokens}
|
||||||
contextWindow={contextWindow}
|
contextWindow={contextWindow}
|
||||||
|
workspaceSlug={slug}
|
||||||
|
threadSlug={threadSlug}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Brain, CheckCircle } from "@phosphor-icons/react";
|
|||||||
import LLMSelectorModal from "./index";
|
import LLMSelectorModal from "./index";
|
||||||
import { useTheme } from "@/hooks/useTheme";
|
import { useTheme } from "@/hooks/useTheme";
|
||||||
import { useRef, useEffect, useState } from "react";
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import { useModal } from "@/hooks/useModal";
|
import { useModal } from "@/hooks/useModal";
|
||||||
import SetupProvider from "./SetupProvider";
|
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 SAVE_LLM_SELECTOR_EVENT = "save_llm_selector";
|
||||||
export const PROVIDER_SETUP_EVENT = "provider_setup_requested";
|
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 tooltipRef = useRef(null);
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const { user } = useUser();
|
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 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.
|
// This is because of the limitations of model selection currently and other nuances in controls.
|
||||||
if (!!user && user.role !== "admin") return null;
|
if (!!user && user.role !== "admin") return null;
|
||||||
|
if (!slug) return null;
|
||||||
|
|
||||||
return (
|
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`}
|
className={`border-none relative flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer`}
|
||||||
>
|
>
|
||||||
{saved ? (
|
{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>
|
</div>
|
||||||
<Tooltip
|
<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"
|
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>
|
</Tooltip>
|
||||||
<SetupProvider
|
<SetupProvider
|
||||||
isOpen={isSetupProviderOpen}
|
isOpen={isSetupProviderOpen}
|
||||||
|
|||||||
@ -16,8 +16,9 @@ import showToast from "@/utils/toast";
|
|||||||
import Workspace from "@/models/workspace";
|
import Workspace from "@/models/workspace";
|
||||||
import System from "@/models/system";
|
import System from "@/models/system";
|
||||||
|
|
||||||
export default function LLMSelectorModal() {
|
export default function LLMSelectorModal({ workspaceSlug = null }) {
|
||||||
const { slug } = useParams();
|
const { slug: urlSlug } = useParams();
|
||||||
|
const slug = urlSlug ?? workspaceSlug;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [settings, setSettings] = useState(null);
|
const [settings, setSettings] = useState(null);
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export function validatedModelSelection(model) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function hasMissingCredentials(settings, provider) {
|
export function hasMissingCredentials(settings, provider) {
|
||||||
|
if (!settings) return false;
|
||||||
const providerEntry = AVAILABLE_LLM_PROVIDERS.find(
|
const providerEntry = AVAILABLE_LLM_PROVIDERS.find(
|
||||||
(p) => p.value === provider
|
(p) => p.value === provider
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,13 +15,13 @@ export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
|
|||||||
data-tooltip-id="tooltip-slash-cmd-btn"
|
data-tooltip-id="tooltip-slash-cmd-btn"
|
||||||
data-tooltip-content={t("chat_window.slash")}
|
data-tooltip-content={t("chat_window.slash")}
|
||||||
onClick={() => setShowSlashCommand(!showing)}
|
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" : ""
|
showing ? "!opacity-100" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<SlashCommandIcon
|
<SlashCommandIcon
|
||||||
color="var(--theme-sidebar-footer-icon-fill)"
|
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
|
<Tooltip
|
||||||
id="tooltip-slash-cmd-btn"
|
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);
|
const cmdRef = useRef(null);
|
||||||
useSlashCommandKeyboardNavigation({ showing });
|
useSlashCommandKeyboardNavigation({ showing });
|
||||||
|
|
||||||
@ -54,10 +60,16 @@ export function SlashCommands({ showing, setShowing, sendCommand, promptRef }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div hidden={!showing}>
|
<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
|
<div
|
||||||
ref={cmdRef}
|
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} />
|
<ResetCommand sendCommand={sendCommand} setShowing={setShowing} />
|
||||||
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />
|
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />
|
||||||
|
|||||||
@ -130,9 +130,9 @@ export default function SpeechToText({ sendCommand }) {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Microphone
|
<Microphone
|
||||||
weight="fill"
|
weight="regular"
|
||||||
color="var(--theme-sidebar-footer-icon-fill)"
|
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" : ""
|
listening ? "animate-pulse-glow" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ABORT_STREAM_EVENT } from "@/utils/chat";
|
import { ABORT_STREAM_EVENT } from "@/utils/chat";
|
||||||
|
import { Stop } from "@phosphor-icons/react";
|
||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
|
|
||||||
export default function StopGenerationButton() {
|
export default function StopGenerationButton() {
|
||||||
@ -13,40 +14,19 @@ export default function StopGenerationButton() {
|
|||||||
onClick={emitHaltEvent}
|
onClick={emitHaltEvent}
|
||||||
data-tooltip-id="stop-generation-button"
|
data-tooltip-id="stop-generation-button"
|
||||||
data-tooltip-content="Stop generating response"
|
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"
|
aria-label="Stop generating"
|
||||||
>
|
>
|
||||||
<svg
|
<Stop
|
||||||
width="28"
|
className="w-[12px] h-[12px] light:text-white text-black"
|
||||||
height="28"
|
weight="fill"
|
||||||
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>
|
|
||||||
</button>
|
</button>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
id="stop-generation-button"
|
id="stop-generation-button"
|
||||||
place="bottom"
|
place="bottom"
|
||||||
delayShow={300}
|
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
|
<TextT
|
||||||
color="var(--theme-sidebar-footer-icon-fill)"
|
color="var(--theme-sidebar-footer-icon-fill)"
|
||||||
weight="fill"
|
weight="fill"
|
||||||
className="w-[22px] h-[22px] pointer-events-none text-white"
|
className="w-[20px] h-[20px] pointer-events-none text-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import SlashCommandsButton, {
|
|||||||
useSlashCommands,
|
useSlashCommands,
|
||||||
} from "./SlashCommands";
|
} from "./SlashCommands";
|
||||||
import debounce from "lodash.debounce";
|
import debounce from "lodash.debounce";
|
||||||
import { PaperPlaneRight } from "@phosphor-icons/react";
|
import { ArrowUp } from "@phosphor-icons/react";
|
||||||
import StopGenerationButton from "./StopGenerationButton";
|
import StopGenerationButton from "./StopGenerationButton";
|
||||||
import AvailableAgentsButton, {
|
import AvailableAgentsButton, {
|
||||||
AvailableAgents,
|
AvailableAgents,
|
||||||
@ -30,11 +30,23 @@ export const PROMPT_INPUT_ID = "primary-prompt-input";
|
|||||||
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
export const PROMPT_INPUT_EVENT = "set_prompt_input";
|
||||||
const MAX_EDIT_STACK_SIZE = 100;
|
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({
|
export default function PromptInput({
|
||||||
submit,
|
submit,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
sendCommand,
|
sendCommand,
|
||||||
attachments = [],
|
attachments = [],
|
||||||
|
centered = false,
|
||||||
|
workspaceSlug = null,
|
||||||
|
threadSlug = null,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isDisabled } = useIsDisabled();
|
const { isDisabled } = useIsDisabled();
|
||||||
@ -247,27 +259,41 @@ export default function PromptInput({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<SlashCommands
|
||||||
showing={showSlashCommand}
|
showing={showSlashCommand}
|
||||||
setShowing={setShowSlashCommand}
|
setShowing={setShowSlashCommand}
|
||||||
sendCommand={sendCommand}
|
sendCommand={sendCommand}
|
||||||
promptRef={textareaRef}
|
promptRef={textareaRef}
|
||||||
|
centered={centered}
|
||||||
/>
|
/>
|
||||||
<AvailableAgents
|
<AvailableAgents
|
||||||
showing={showAgents}
|
showing={showAgents}
|
||||||
setShowing={setShowAgents}
|
setShowing={setShowAgents}
|
||||||
sendCommand={sendCommand}
|
sendCommand={sendCommand}
|
||||||
promptRef={textareaRef}
|
promptRef={textareaRef}
|
||||||
|
centered={centered}
|
||||||
/>
|
/>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
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
|
||||||
<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">
|
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} />
|
<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
|
<textarea
|
||||||
id={PROMPT_INPUT_ID}
|
id={PROMPT_INPUT_ID}
|
||||||
ref={textareaRef}
|
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}`}
|
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")}
|
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>
|
||||||
<div className="flex justify-between py-3.5 mx-3 mb-1">
|
<div className="flex justify-between items-center pt-3.5 pb-3 mx-[7px]">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2 items-center h-5 -ml-[4.5px]">
|
||||||
<AttachItem />
|
<AttachItem
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
workspaceThreadSlug={threadSlug}
|
||||||
|
/>
|
||||||
<SlashCommandsButton
|
<SlashCommandsButton
|
||||||
showing={showSlashCommand}
|
showing={showSlashCommand}
|
||||||
setShowSlashCommand={setShowSlashCommand}
|
setShowSlashCommand={setShowSlashCommand}
|
||||||
@ -333,10 +330,41 @@ export default function PromptInput({
|
|||||||
setShowAgents={setShowAgents}
|
setShowAgents={setShowAgents}
|
||||||
/>
|
/>
|
||||||
<TextSizeButton />
|
<TextSizeButton />
|
||||||
<LLMSelectorAction />
|
<LLMSelectorAction workspaceSlug={workspaceSlug} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2 items-center h-5">
|
||||||
<SpeechToText sendCommand={sendCommand} />
|
<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>
|
</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 ChatHistory from "./ChatHistory";
|
||||||
import { CLEAR_ATTACHMENTS_EVENT, DndUploaderContext } from "./DnDWrapper";
|
import { CLEAR_ATTACHMENTS_EVENT, DndUploaderContext } from "./DnDWrapper";
|
||||||
import PromptInput, {
|
import PromptInput, {
|
||||||
@ -9,7 +9,7 @@ import Workspace from "@/models/workspace";
|
|||||||
import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat";
|
import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import { SidebarMobileHeader } from "../../Sidebar";
|
import { SidebarMobileHeader } from "../../Sidebar";
|
||||||
import { useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import handleSocketResponse, {
|
import handleSocketResponse, {
|
||||||
websocketURI,
|
websocketURI,
|
||||||
@ -23,8 +23,16 @@ import SpeechRecognition, {
|
|||||||
import { ChatTooltips } from "./ChatTooltips";
|
import { ChatTooltips } from "./ChatTooltips";
|
||||||
import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics";
|
import { MetricsProvider } from "./ChatHistory/HistoricalMessage/Actions/RenderMetrics";
|
||||||
import useChatContainerQuickScroll from "@/hooks/useChatContainerQuickScroll";
|
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 = [] }) {
|
export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { t } = useTranslation();
|
||||||
const { threadSlug = null } = useParams();
|
const { threadSlug = null } = useParams();
|
||||||
const [loadingResponse, setLoadingResponse] = useState(false);
|
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||||
const [chatHistory, setChatHistory] = useState(knownHistory);
|
const [chatHistory, setChatHistory] = useState(knownHistory);
|
||||||
@ -32,6 +40,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
const [websocket, setWebsocket] = useState(null);
|
const [websocket, setWebsocket] = useState(null);
|
||||||
const { files, parseAttachments } = useContext(DndUploaderContext);
|
const { files, parseAttachments } = useContext(DndUploaderContext);
|
||||||
const { chatHistoryRef } = useChatContainerQuickScroll();
|
const { chatHistoryRef } = useChatContainerQuickScroll();
|
||||||
|
const pendingMessageChecked = useRef(false);
|
||||||
|
|
||||||
const { listening, resetTranscript } = useSpeechRecognition({
|
const { listening, resetTranscript } = useSpeechRecognition({
|
||||||
clearTranscriptOnListen: true,
|
clearTranscriptOnListen: true,
|
||||||
@ -164,6 +173,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
role: "assistant",
|
role: "assistant",
|
||||||
pending: true,
|
pending: true,
|
||||||
userMessage: text,
|
userMessage: text,
|
||||||
|
attachments,
|
||||||
animate: true,
|
animate: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -174,6 +184,23 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
setLoadingResponse(true);
|
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(() => {
|
useEffect(() => {
|
||||||
async function fetchReply() {
|
async function fetchReply() {
|
||||||
const promptMessage =
|
const promptMessage =
|
||||||
@ -294,6 +321,53 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
handleWSS();
|
handleWSS();
|
||||||
}, [socketId]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
@ -301,23 +375,27 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
|||||||
>
|
>
|
||||||
{isMobile && <SidebarMobileHeader />}
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<DnDFileUploaderWrapper>
|
<DnDFileUploaderWrapper>
|
||||||
<MetricsProvider>
|
<div className="flex flex-col h-full w-full">
|
||||||
<ChatHistory
|
<div className="contents">
|
||||||
ref={chatHistoryRef}
|
<MetricsProvider>
|
||||||
history={chatHistory}
|
<ChatHistory
|
||||||
workspace={workspace}
|
ref={chatHistoryRef}
|
||||||
sendCommand={sendCommand}
|
history={chatHistory}
|
||||||
updateHistory={setChatHistory}
|
workspace={workspace}
|
||||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
sendCommand={sendCommand}
|
||||||
hasAttachments={files.length > 0}
|
updateHistory={setChatHistory}
|
||||||
/>
|
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||||
</MetricsProvider>
|
/>
|
||||||
<PromptInput
|
</MetricsProvider>
|
||||||
submit={handleSubmit}
|
<PromptInput
|
||||||
isStreaming={loadingResponse}
|
submit={handleSubmit}
|
||||||
sendCommand={sendCommand}
|
isStreaming={loadingResponse}
|
||||||
attachments={files}
|
sendCommand={sendCommand}
|
||||||
/>
|
attachments={files}
|
||||||
|
centered={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DnDFileUploaderWrapper>
|
</DnDFileUploaderWrapper>
|
||||||
<ChatTooltips />
|
<ChatTooltips />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
TTSProvider,
|
TTSProvider,
|
||||||
useWatchForAutoPlayAssistantTTSResponse,
|
useWatchForAutoPlayAssistantTTSResponse,
|
||||||
} from "../contexts/TTSProvider";
|
} from "../contexts/TTSProvider";
|
||||||
|
import { PENDING_HOME_MESSAGE } from "@/utils/constants";
|
||||||
|
|
||||||
export default function WorkspaceChat({ loading, workspace }) {
|
export default function WorkspaceChat({ loading, workspace }) {
|
||||||
useWatchForAutoPlayAssistantTTSResponse();
|
useWatchForAutoPlayAssistantTTSResponse();
|
||||||
@ -36,7 +37,15 @@ export default function WorkspaceChat({ loading, workspace }) {
|
|||||||
getHistory();
|
getHistory();
|
||||||
}, [workspace, loading]);
|
}, [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) {
|
if (!loading && !loadingHistory && !workspace) {
|
||||||
return (
|
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: "اختصارات لوحة المفاتيح",
|
keyboardShortcuts: "اختصارات لوحة المفاتيح",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "إنشاء وكيل",
|
||||||
|
editWorkspace: "تعديل مساحة العمل",
|
||||||
|
uploadDocument: "تحميل مستند",
|
||||||
|
},
|
||||||
|
greeting: "كيف يمكنني مساعدتك اليوم؟",
|
||||||
},
|
},
|
||||||
"keyboard-shortcuts": {
|
"keyboard-shortcuts": {
|
||||||
title: "اختصارات لوحة المفاتيح",
|
title: "اختصارات لوحة المفاتيح",
|
||||||
|
|||||||
@ -215,6 +215,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Klávesové zkratky",
|
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": {
|
"new-workspace": {
|
||||||
title: "Nový pracovní prostor",
|
title: "Nový pracovní prostor",
|
||||||
|
|||||||
@ -942,6 +942,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Tastaturgenveje",
|
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": {
|
"keyboard-shortcuts": {
|
||||||
title: "Tastaturgenveje",
|
title: "Tastaturgenveje",
|
||||||
|
|||||||
@ -210,6 +210,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Tastaturkürzel",
|
keyboardShortcuts: "Tastaturkürzel",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "Erstelle einen Agenten",
|
||||||
|
editWorkspace: "Arbeitsbereich bearbeiten",
|
||||||
|
uploadDocument: "Ein Dokument hochladen",
|
||||||
|
},
|
||||||
|
greeting: "Wie kann ich Ihnen heute helfen?",
|
||||||
},
|
},
|
||||||
"new-workspace": {
|
"new-workspace": {
|
||||||
title: "Neuer Workspace",
|
title: "Neuer Workspace",
|
||||||
|
|||||||
@ -139,6 +139,7 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
"main-page": {
|
"main-page": {
|
||||||
|
greeting: "How can I help you today?",
|
||||||
noWorkspaceError: "Please create a workspace before starting a chat.",
|
noWorkspaceError: "Please create a workspace before starting a chat.",
|
||||||
checklist: {
|
checklist: {
|
||||||
title: "Getting Started",
|
title: "Getting Started",
|
||||||
@ -178,6 +179,11 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "Create an Agent",
|
||||||
|
editWorkspace: "Edit Workspace",
|
||||||
|
uploadDocument: "Upload a Document",
|
||||||
|
},
|
||||||
quickLinks: {
|
quickLinks: {
|
||||||
title: "Quick Links",
|
title: "Quick Links",
|
||||||
sendChat: "Send Chat",
|
sendChat: "Send Chat",
|
||||||
|
|||||||
@ -210,6 +210,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Atajos de teclado",
|
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": {
|
"new-workspace": {
|
||||||
title: "Nuevo espacio de trabajo",
|
title: "Nuevo espacio de trabajo",
|
||||||
|
|||||||
@ -203,6 +203,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Klaviatuuri otseteed",
|
keyboardShortcuts: "Klaviatuuri otseteed",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "Loo agent",
|
||||||
|
editWorkspace: "Redige tööruum",
|
||||||
|
uploadDocument: "Lae fail üles",
|
||||||
|
},
|
||||||
|
greeting: "Kuidas saan teid täna aidata?",
|
||||||
},
|
},
|
||||||
"new-workspace": {
|
"new-workspace": {
|
||||||
title: "Uus tööruum",
|
title: "Uus tööruum",
|
||||||
|
|||||||
@ -932,6 +932,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "کلیدهای میانبر",
|
keyboardShortcuts: "کلیدهای میانبر",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "ایجاد یک عامل",
|
||||||
|
editWorkspace: "ویرایش فضای کاری",
|
||||||
|
uploadDocument: "بارگذاری یک سند",
|
||||||
|
},
|
||||||
|
greeting: "امروز چگونه میتوانم به شما کمک کنم؟",
|
||||||
},
|
},
|
||||||
"keyboard-shortcuts": {
|
"keyboard-shortcuts": {
|
||||||
title: "کلیدهای میانبر",
|
title: "کلیدهای میانبر",
|
||||||
|
|||||||
@ -942,6 +942,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Raccourcis clavier",
|
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": {
|
"keyboard-shortcuts": {
|
||||||
title: "Raccourcis clavier",
|
title: "Raccourcis clavier",
|
||||||
|
|||||||
@ -203,6 +203,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "קיצורי מקלדת",
|
keyboardShortcuts: "קיצורי מקלדת",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "צור סוכן",
|
||||||
|
editWorkspace: "ערוך את סביבת העבודה",
|
||||||
|
uploadDocument: "העלה מסמך",
|
||||||
|
},
|
||||||
|
greeting: "במה אוכל לעזור לך היום?",
|
||||||
},
|
},
|
||||||
"new-workspace": {
|
"new-workspace": {
|
||||||
title: "סביבת עבודה חדשה",
|
title: "סביבת עבודה חדשה",
|
||||||
|
|||||||
@ -959,6 +959,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Combinazioni di tasti",
|
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": {
|
"keyboard-shortcuts": {
|
||||||
title: "Combinazioni di tasti",
|
title: "Combinazioni di tasti",
|
||||||
|
|||||||
@ -928,6 +928,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "キーボードショートカット",
|
keyboardShortcuts: "キーボードショートカット",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "エージェントを作成する",
|
||||||
|
editWorkspace: "ワークスペースの編集",
|
||||||
|
uploadDocument: "ドキュメントをアップロードする",
|
||||||
|
},
|
||||||
|
greeting: "今日はどのようにお手伝いできますか?",
|
||||||
},
|
},
|
||||||
"keyboard-shortcuts": {
|
"keyboard-shortcuts": {
|
||||||
title: "キーボードショートカット",
|
title: "キーボードショートカット",
|
||||||
|
|||||||
@ -204,6 +204,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "단축키 안내",
|
keyboardShortcuts: "단축키 안내",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "에이전트 생성",
|
||||||
|
editWorkspace: "워크스페이스 편집",
|
||||||
|
uploadDocument: "문서 업로드",
|
||||||
|
},
|
||||||
|
greeting: "오늘 어떻게 도와드릴까요?",
|
||||||
},
|
},
|
||||||
"new-workspace": {
|
"new-workspace": {
|
||||||
title: "새 워크스페이스",
|
title: "새 워크스페이스",
|
||||||
|
|||||||
@ -207,6 +207,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Taustiņu atvieglojumi",
|
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": {
|
"new-workspace": {
|
||||||
title: "Jauna darba telpa",
|
title: "Jauna darba telpa",
|
||||||
|
|||||||
@ -937,6 +937,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Sneltoetsen",
|
keyboardShortcuts: "Sneltoetsen",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "Maak een agent",
|
||||||
|
editWorkspace: "Werkruimte bewerken",
|
||||||
|
uploadDocument: "Upload een document",
|
||||||
|
},
|
||||||
|
greeting: "Hoe kan ik u vandaag helpen?",
|
||||||
},
|
},
|
||||||
"keyboard-shortcuts": {
|
"keyboard-shortcuts": {
|
||||||
title: "Sneltoetsen",
|
title: "Sneltoetsen",
|
||||||
|
|||||||
@ -209,6 +209,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Skróty klawiaturowe",
|
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": {
|
"new-workspace": {
|
||||||
title: "Nowy obszar roboczy",
|
title: "Nowy obszar roboczy",
|
||||||
|
|||||||
@ -205,6 +205,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Atalhos de Teclado",
|
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": {
|
"new-workspace": {
|
||||||
title: "Novo Workspace",
|
title: "Novo Workspace",
|
||||||
|
|||||||
@ -209,6 +209,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Scurtături de tastatură",
|
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": {
|
"new-workspace": {
|
||||||
title: "Spațiu de lucru nou",
|
title: "Spațiu de lucru nou",
|
||||||
|
|||||||
@ -946,6 +946,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Сочетания клавиш",
|
keyboardShortcuts: "Сочетания клавиш",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "Создать агента",
|
||||||
|
editWorkspace: "Редактировать рабочее пространство",
|
||||||
|
uploadDocument: "Загрузить документ",
|
||||||
|
},
|
||||||
|
greeting: "Чем я могу вам помочь сегодня?",
|
||||||
},
|
},
|
||||||
"keyboard-shortcuts": {
|
"keyboard-shortcuts": {
|
||||||
title: "Сочетания клавиш",
|
title: "Сочетания клавиш",
|
||||||
|
|||||||
@ -934,6 +934,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Klavye Kısayolları",
|
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": {
|
"keyboard-shortcuts": {
|
||||||
title: "Klavye Kısayolları",
|
title: "Klavye Kısayolları",
|
||||||
|
|||||||
@ -930,6 +930,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "Phím tắt",
|
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": {
|
"keyboard-shortcuts": {
|
||||||
title: "Phím tắt",
|
title: "Phím tắt",
|
||||||
|
|||||||
@ -199,6 +199,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "键盘快捷键",
|
keyboardShortcuts: "键盘快捷键",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "创建代理",
|
||||||
|
editWorkspace: "编辑工作区",
|
||||||
|
uploadDocument: "上传文件",
|
||||||
|
},
|
||||||
|
greeting: "今天我能帮您什么?",
|
||||||
},
|
},
|
||||||
"new-workspace": {
|
"new-workspace": {
|
||||||
title: "新工作区",
|
title: "新工作区",
|
||||||
|
|||||||
@ -874,6 +874,12 @@ const TRANSLATIONS = {
|
|||||||
},
|
},
|
||||||
keyboardShortcuts: "鍵盤快捷鍵",
|
keyboardShortcuts: "鍵盤快捷鍵",
|
||||||
},
|
},
|
||||||
|
quickActions: {
|
||||||
|
createAgent: "建立一個代理",
|
||||||
|
editWorkspace: "編輯工作區",
|
||||||
|
uploadDocument: "上傳文件",
|
||||||
|
},
|
||||||
|
greeting: "今天我能幫您什麼?",
|
||||||
},
|
},
|
||||||
"keyboard-shortcuts": {
|
"keyboard-shortcuts": {
|
||||||
title: "鍵盤快捷鍵",
|
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 React, { useState, useEffect, useRef, useContext } from "react";
|
||||||
import QuickLinks from "./QuickLinks";
|
import { useNavigate } from "react-router-dom";
|
||||||
import ExploreFeatures from "./ExploreFeatures";
|
|
||||||
import Updates from "./Updates";
|
|
||||||
import Resources from "./Resources";
|
|
||||||
import Checklist from "./Checklist";
|
|
||||||
import { isMobile } from "react-device-detect";
|
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() {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
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">
|
{isMobile && <SidebarMobileHeader />}
|
||||||
<div className="w-full max-w-[1200px] flex flex-col gap-y-[24px] p-4 pt-16 md:p-12 md:pt-11">
|
<DnDFileUploaderWrapper>
|
||||||
<Checklist />
|
<div className="flex flex-col h-full w-full items-center justify-center">
|
||||||
<QuickLinks />
|
<div className="flex flex-col items-center w-full max-w-[750px]">
|
||||||
<ExploreFeatures />
|
<h1 className="text-white text-xl md:text-2xl mb-11 text-center">
|
||||||
<Updates />
|
{t("main-page.greeting")}
|
||||||
<Resources />
|
</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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,10 +2,8 @@ import React from "react";
|
|||||||
import PasswordModal, { usePasswordModal } from "@/components/Modals/Password";
|
import PasswordModal, { usePasswordModal } from "@/components/Modals/Password";
|
||||||
import { FullScreenLoader } from "@/components/Preloader";
|
import { FullScreenLoader } from "@/components/Preloader";
|
||||||
import Home from "./Home";
|
import Home from "./Home";
|
||||||
import DefaultChatContainer from "@/components/DefaultChat";
|
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import Sidebar, { SidebarMobileHeader } from "@/components/Sidebar";
|
import Sidebar, { SidebarMobileHeader } from "@/components/Sidebar";
|
||||||
import { userFromStorage } from "@/utils/request";
|
|
||||||
|
|
||||||
export default function Main() {
|
export default function Main() {
|
||||||
const { loading, requiresAuth, mode } = usePasswordModal();
|
const { loading, requiresAuth, mode } = usePasswordModal();
|
||||||
@ -14,11 +12,10 @@ export default function Main() {
|
|||||||
if (requiresAuth !== false)
|
if (requiresAuth !== false)
|
||||||
return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;
|
return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;
|
||||||
|
|
||||||
const user = userFromStorage();
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
|
<div className="w-screen h-screen overflow-hidden bg-theme-bg-container flex">
|
||||||
{!isMobile ? <Sidebar /> : <SidebarMobileHeader />}
|
{!isMobile ? <Sidebar /> : <SidebarMobileHeader />}
|
||||||
{!!user && user?.role !== "admin" ? <DefaultChatContainer /> : <Home />}
|
<Home />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,8 +24,7 @@ export default function SuggestedChatMessages({ slug }) {
|
|||||||
|
|
||||||
const handleSaveSuggestedMessages = async () => {
|
const handleSaveSuggestedMessages = async () => {
|
||||||
const validMessages = suggestedMessages.filter(
|
const validMessages = suggestedMessages.filter(
|
||||||
(msg) =>
|
(msg) => msg?.message?.trim()?.length > 0
|
||||||
msg?.heading?.trim()?.length > 0 || msg?.message?.trim()?.length > 0
|
|
||||||
);
|
);
|
||||||
const { success, error } = await Workspace.setSuggestedMessages(
|
const { success, error } = await Workspace.setSuggestedMessages(
|
||||||
slug,
|
slug,
|
||||||
@ -35,6 +34,8 @@ export default function SuggestedChatMessages({ slug }) {
|
|||||||
showToast(`Failed to update welcome messages: ${error}`, "error");
|
showToast(`Failed to update welcome messages: ${error}`, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setSuggestedMessages(validMessages);
|
||||||
|
setEditingIndex(-1);
|
||||||
showToast("Successfully updated welcome messages.", "success");
|
showToast("Successfully updated welcome messages.", "success");
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
};
|
};
|
||||||
@ -46,8 +47,8 @@ export default function SuggestedChatMessages({ slug }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const defaultMessage = {
|
const defaultMessage = {
|
||||||
heading: t("general.message.heading"),
|
heading: "",
|
||||||
message: t("general.message.body"),
|
message: `${t("general.message.heading")} ${t("general.message.body")}`,
|
||||||
};
|
};
|
||||||
setNewMessage(defaultMessage);
|
setNewMessage(defaultMessage);
|
||||||
setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]);
|
setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]);
|
||||||
@ -64,7 +65,21 @@ export default function SuggestedChatMessages({ slug }) {
|
|||||||
const startEditing = (e, index) => {
|
const startEditing = (e, index) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setEditingIndex(index);
|
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) => {
|
const handleRemoveMessage = (index) => {
|
||||||
@ -134,26 +149,16 @@ export default function SuggestedChatMessages({ slug }) {
|
|||||||
editingIndex === index ? "border-sky-400" : ""
|
editingIndex === index ? "border-sky-400" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="font-semibold">{suggestion.heading}</p>
|
<p className="line-clamp-2 text-theme-text-primary">
|
||||||
<p>{suggestion.message}</p>
|
{suggestion?.heading ? `${suggestion.heading} ` : ""}
|
||||||
|
{suggestion?.message ?? ""}
|
||||||
|
</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{editingIndex >= 0 && (
|
{editingIndex >= 0 && (
|
||||||
<div className="flex flex-col gap-y-4 mr-2 mt-8">
|
<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">
|
<div className="w-1/2">
|
||||||
<label className="text-white text-sm font-semibold block mb-2">
|
<label className="text-white text-sm font-semibold block mb-2">
|
||||||
Message
|
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 SEEN_WATCH_ALERT = "anythingllm_watched_document_alert";
|
||||||
export const LAST_VISITED_WORKSPACE = "anythingllm_last_visited_workspace";
|
export const LAST_VISITED_WORKSPACE = "anythingllm_last_visited_workspace";
|
||||||
export const USER_PROMPT_INPUT_MAP = "anythingllm_user_prompt_input_map";
|
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";
|
export const APPEARANCE_SETTINGS = "anythingllm_appearance_settings";
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user