Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: GROWI AI Next #9492

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/app/public/static/locales/en_US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@
"Page Tree": "Page Tree",
"Bookmarks": "Bookmarks",
"In-App Notification": "Notifications",
"AI Assistant": "AI Assistant",
"Knowledge Assistant": "Knowledge Assistant",
"original_path": "Original path",
"new_path": "New path",
"duplicated_path": "Duplicated path",
Expand Down
2 changes: 2 additions & 0 deletions apps/app/public/static/locales/fr_FR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@
"Page Tree": "Arbre",
"Bookmarks": "Favoris",
"In-App Notification": "Notifications",
"AI Assistant": "Assistant IA",
"Knowledge Assistant": "Assistant de Connaissance",
"original_path": "Chemin originel",
"new_path": "Nouveau chemin",
"duplicated_path": "Chemin dupliqué",
Expand Down
2 changes: 2 additions & 0 deletions apps/app/public/static/locales/ja_JP/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@
"Page Tree": "ページツリー",
"Bookmarks": "ブックマーク",
"In-App Notification": "通知",
"AI Assistant": "AI アシスタント",
"Knowledge Assistant": "ナレッジアシスタント",
"original_path": "元のパス",
"new_path": "新しいパス",
"duplicated_path": "重複したパス",
Expand Down
2 changes: 2 additions & 0 deletions apps/app/public/static/locales/zh_CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@
"Page Tree": "页面树",
"Bookmarks": "书签",
"In-App Notification": "通知",
"AI Assistant": "AI助手",
"Knowledge Assistant": "知识助手",
"original_path": "Original path",
"new_path": "New path",
"duplicated_path": "Duplicated path",
Expand Down
25 changes: 19 additions & 6 deletions apps/app/src/client/components/PageHeader/PagePathHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
useState, useCallback, memo,
} from 'react';

import nodePath from 'path';

import type { IPagePopulatedToShowRevision } from '@growi/core';
import { DevidedPagePath } from '@growi/core/dist/models';
import { normalizePath } from '@growi/core/dist/utils/path-utils';
Expand All @@ -11,13 +13,13 @@ import { debounce } from 'throttle-debounce';

import type { InputValidationResult } from '~/client/util/use-input-validator';
import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
import type { IPageForItem } from '~/interfaces/page';
import LinkedPagePath from '~/models/linked-page-path';
import { usePageSelectModal } from '~/stores/modal';

import { PagePathHierarchicalLink } from '../../../components/Common/PagePathHierarchicalLink';
import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '../Common/SubmittableInput';
import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
import { PageSelectModal } from '../PageSelectModal/PageSelectModal';

import styles from './PagePathHeader.module.scss';

Expand Down Expand Up @@ -45,8 +47,7 @@ export const PagePathHeader = memo((props: Props): JSX.Element => {
const [isRenameInputShown, setRenameInputShown] = useState(false);
const [isHover, setHover] = useState(false);

const { data: PageSelectModalData, open: openPageSelectModal } = usePageSelectModal();
const isOpened = PageSelectModalData?.isOpened ?? false;
const { open: openPageSelectModal } = usePageSelectModal();

const [validationResult, setValidationResult] = useState<InputValidationResult>();

Expand All @@ -61,6 +62,20 @@ export const PagePathHeader = memo((props: Props): JSX.Element => {

const pagePathRenameHandler = usePagePathRenameHandler(currentPage);

const onClickOpenPageSelectModalButton = useCallback(() => {
const onSelected = (page: IPageForItem): void => {
if (page == null || page.path == null) {
return;
}

const currentPageTitle = nodePath.basename(currentPage?.path ?? '') || '/';
const newPagePath = nodePath.resolve(page.path, currentPageTitle);

pagePathRenameHandler(newPagePath);
};

openPageSelectModal({ onSelected });
}, [currentPage?.path, openPageSelectModal, pagePathRenameHandler]);

const rename = useCallback((inputText) => {
const pathToRename = normalizePath(`${inputText}/${dPagePath.latter}`);
Expand Down Expand Up @@ -144,13 +159,11 @@ export const PagePathHeader = memo((props: Props): JSX.Element => {
<button
type="button"
className="btn btn-outline-neutral-secondary d-flex align-items-center justify-content-center"
onClick={openPageSelectModal}
onClick={onClickOpenPageSelectModalButton}
>
<span className="material-symbols-outlined fs-6">account_tree</span>
</button>
</div>

{isOpened && <PageSelectModal />}
</div>
);
});
50 changes: 26 additions & 24 deletions apps/app/src/client/components/PageSelectModal/PageSelectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,16 @@ import { useSWRxCurrentPage } from '~/stores/page';

import { ItemsTree } from '../ItemsTree';
import ItemsTreeContentSkeleton from '../ItemsTree/ItemsTreeContentSkeleton';
import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';

import { TreeItemForModal } from './TreeItemForModal';


export const PageSelectModal: FC = () => {
const PageSelectModalSubstance: FC = () => {
const {
data: PageSelectModalData,
close: closeModal,
} = usePageSelectModal();

const isOpened = PageSelectModalData?.isOpened ?? false;

const [clickedParentPagePath, setClickedParentPagePath] = useState<string | null>(null);
const [clickedParentPage, setClickedParentPage] = useState<IPageForItem | null>(null);

const { t } = useTranslation();

Expand All @@ -41,50 +37,41 @@ export const PageSelectModal: FC = () => {
const { data: targetAndAncestorsData } = useTargetAndAncestors();
const { data: currentPage } = useSWRxCurrentPage();

const pagePathRenameHandler = usePagePathRenameHandler(currentPage);

const onClickTreeItem = useCallback((page: IPageForItem) => {
const parentPagePath = page.path;

if (parentPagePath == null) {
return <></>;
return;
}

setClickedParentPagePath(parentPagePath);
setClickedParentPage(page);
}, []);

const onClickCancel = useCallback(() => {
setClickedParentPagePath(null);
setClickedParentPage(null);
closeModal();
}, [closeModal]);

const onClickDone = useCallback(() => {
if (clickedParentPagePath != null) {
const currentPageTitle = nodePath.basename(currentPage?.path ?? '') || '/';
const newPagePath = nodePath.resolve(clickedParentPagePath, currentPageTitle);

pagePathRenameHandler(newPagePath);
if (clickedParentPage != null) {
PageSelectModalData?.opts?.onSelected?.(clickedParentPage);
}

closeModal();
}, [clickedParentPagePath, closeModal, currentPage?.path, pagePathRenameHandler]);
}, [PageSelectModalData?.opts, clickedParentPage, closeModal]);

const parentPagePath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage?.path ?? ''));

const targetPathOrId = clickedParentPagePath || parentPagePath;
const targetPathOrId = clickedParentPage?.path || parentPagePath;

const targetPath = clickedParentPagePath || parentPagePath;
const targetPath = clickedParentPage?.path || parentPagePath;

if (isGuestUser == null) {
return <></>;
}

return (
<Modal
isOpen={isOpened}
toggle={closeModal}
centered
>
<>
<ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
<ModalBody className="p-0">
<Suspense fallback={<ItemsTreeContentSkeleton />}>
Expand All @@ -107,6 +94,21 @@ export const PageSelectModal: FC = () => {
<Button color="secondary" onClick={onClickCancel}>{t('Cancel')}</Button>
<Button color="primary" onClick={onClickDone}>{t('Done')}</Button>
</ModalFooter>
</>
);
};

export const PageSelectModal = (): JSX.Element => {
const { data: pageSelectModalData, close: closePageSelectModal } = usePageSelectModal();
const isOpen = pageSelectModalData?.isOpened ?? false;

if (!isOpen) {
return <></>;
}

return (
<Modal isOpen={isOpen} toggle={closePageSelectModal} centered>
<PageSelectModalSubstance />
</Modal>
);
};
3 changes: 3 additions & 0 deletions apps/app/src/client/components/Sidebar/SidebarContents.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { memo, useMemo } from 'react';

import { AiAssistant } from '~/features/openai/client/components/AiAssistant/Sidebar/AiAssistant';
import { SidebarContentsType } from '~/interfaces/ui';
import { useCollapsedContentsOpened, useCurrentSidebarContents, useSidebarMode } from '~/stores/ui';

Expand Down Expand Up @@ -32,6 +33,8 @@ export const SidebarContents = memo(() => {
return Bookmarks;
case SidebarContentsType.NOTIFICATION:
return InAppNotification;
case SidebarContentsType.AI_ASSISTANT:
return AiAssistant;
default:
return PageTree;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ export type PrimaryItemProps = {
label: string,
iconName: string,
sidebarMode: SidebarMode,
isCustomIcon?: boolean,
badgeContents?: number,
onHover?: (contents: SidebarContentsType) => void,
onClick?: () => void,
}

export const PrimaryItem = (props: PrimaryItemProps): JSX.Element => {
const {
contents, label, iconName, sidebarMode, badgeContents,
contents, label, iconName, sidebarMode, badgeContents, isCustomIcon,
onClick, onHover,
} = props;

Expand Down Expand Up @@ -80,7 +81,10 @@ export const PrimaryItem = (props: PrimaryItemProps): JSX.Element => {
{ badgeContents != null && (
<span className="position-absolute badge rounded-pill bg-primary">{badgeContents}</span>
)}
<span className="material-symbols-outlined">{iconName}</span>
{ isCustomIcon
? (<span className="growi-custom-icons fs-4 align-middle">{iconName}</span>)
: (<span className="material-symbols-outlined">{iconName}</span>)
}
</div>
</button>
{
Expand Down
12 changes: 12 additions & 0 deletions apps/app/src/client/components/Sidebar/SidebarNav/PrimaryItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { memo } from 'react';
import dynamic from 'next/dynamic';

import { SidebarContentsType } from '~/interfaces/ui';
import { useIsAiEnabled } from '~/stores-universal/context';
import { useSidebarMode } from '~/stores/ui';

import { PrimaryItem } from './PrimaryItem';
Expand All @@ -22,6 +23,7 @@ export const PrimaryItems = memo((props: Props) => {
const { onItemHover } = props;

const { data: sidebarMode } = useSidebarMode();
const { data: isAiEnabled } = useIsAiEnabled();

if (sidebarMode == null) {
return <></>;
Expand All @@ -35,6 +37,16 @@ export const PrimaryItems = memo((props: Props) => {
<PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmarks" onHover={onItemHover} />
<PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TAG} label="Tags" iconName="local_offer" onHover={onItemHover} />
<PrimaryItemForNotification sidebarMode={sidebarMode} onHover={onItemHover} />
{isAiEnabled && (
<PrimaryItem
sidebarMode={sidebarMode}
contents={SidebarContentsType.AI_ASSISTANT}
label="AI Assistant"
iconName="ai_assistant"
isCustomIcon
onHover={onItemHover}
/>
)}
</div>
);
});
7 changes: 7 additions & 0 deletions apps/app/src/components/Layout/BasicLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ const DeleteBookmarkFolderModal = dynamic(
);
const SearchModal = dynamic(() => import('../../features/search/client/components/SearchModal'), { ssr: false });
const AiChatModal = dynamic(() => import('~/features/openai/chat/components/AiChatModal').then(mod => mod.AiChatModal), { ssr: false });
const AiAssistantManegementModal = dynamic(
() => import('~/features/openai/client/components/AiAssistant/AiAssistantManegementModal')
.then(mod => mod.AiAssistantManegementModal), { ssr: false },
);
const PageSelectModal = dynamic(() => import('~/client/components/PageSelectModal/PageSelectModal').then(mod => mod.PageSelectModal), { ssr: false });

type Props = {
children?: ReactNode
Expand Down Expand Up @@ -66,8 +71,10 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
<DeleteAttachmentModal />
<DeleteBookmarkFolderModal />
<PutbackPageModal />
<PageSelectModal />
<SearchModal />
<AiChatModal />
<AiAssistantManegementModal />

<PagePresentationModal />
<HotkeysManager />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@use '@growi/core-styles/scss/variables/growi-official-colors';

// == Colors
.grw-ai-assistant-manegement :global {
.growi-ai-assistant-icon {
color: growi-official-colors.$growi-ai-purple;
}
}
Loading
Loading