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: Implement AiAssistantTree #9617

Open
wants to merge 23 commits into
base: feat/growi-ai-next
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
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const AiAssistantManagementHome = (props: Props): JSX.Element => {
<ShareScopeWarningModal
isOpen={isShareScopeWarningModalOpen}
closeModal={() => setIsShareScopeWarningModalOpen(false)}
onSubmit={createAiAssistantHandler}
onSubmit={onCreateAiAssistant}
yuki-takei marked this conversation as resolved.
Show resolved Hide resolved
/>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import loggerFactory from '~/utils/logger';

import type { SelectedPage } from '../../../../interfaces/selected-page';
import { createAiAssistant } from '../../../services/ai-assistant';
import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode } from '../../../stores/ai-assistant';
import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode, useSWRxAiAssistants } from '../../../stores/ai-assistant';

import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
import { AiAssistantManagementEditPages } from './AiAssistantManagementEditPages';
Expand All @@ -36,6 +36,7 @@ const convertToGrantedGroups = (selectedGroups: PopulatedGrantedGroup[]): IGrant
const AiAssistantManagementModalSubstance = (): JSX.Element => {
// Hooks
const { t } = useTranslation();
const { mutate: mutateAiAssistants } = useSWRxAiAssistants();
const { data: aiAssistantManagementModalData, close: closeAiAssistantManagementModal } = useAiAssistantManagementModal();

const pageMode = aiAssistantManagementModalData?.pageMode ?? AiAssistantManagementModalPageMode.HOME;
Expand Down Expand Up @@ -83,13 +84,15 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
});

toastSuccess('アシスタントを作成しました');
mutateAiAssistants();
closeAiAssistantManagementModal();
}
catch (err) {
toastError('アシスタントの作成に失敗しました');
logger.error(err);
}
}, [
mutateAiAssistants,
closeAiAssistantManagementModal,
description,
instruction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import dynamic from 'next/dynamic';
import { useTranslation } from 'react-i18next';

import ItemsTreeContentSkeleton from '~/client/components/ItemsTree/ItemsTreeContentSkeleton';
import { useIsGuestUser } from '~/stores-universal/context';

const AiAssistantContent = dynamic(() => import('./AiAssistantSubstance').then(mod => mod.AiAssistantContent), { ssr: false });

export const AiAssistant = (): JSX.Element => {
const { t } = useTranslation();
const { data: isGuestUser } = useIsGuestUser();

return (
<div className="px-3">
Expand All @@ -17,9 +19,19 @@ export const AiAssistant = (): JSX.Element => {
{t('Knowledge Assistant')}
</h3>
</div>
<Suspense fallback={<ItemsTreeContentSkeleton />}>
<AiAssistantContent />
</Suspense>

{ isGuestUser
? (
<h4 className="fs-6">
{ t('Not available for guest') }
</h4>
)
: (
<Suspense fallback={<ItemsTreeContentSkeleton />}>
<AiAssistantContent />
</Suspense>
)
}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
@use '@growi/core-styles/scss/bootstrap/init' as bs;

.grw-ai-assistant-substance :global {
.grw-ai-assistant-substance-header {
font-size: 14px;
}

.grw-ai-assistant-item-container {
.list-group-item {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

色については PageTreeItem.module.scss を参考に、別ブロック (// == Colors) に分けて

height: 40px;
padding-left: 4px;

.grw-ai-assistant-triangle-btn {
border: 0;
transition: transform 0.2s ease-out;
transform: rotate(0deg);

&.grw-ai-assistant-open {
transform: rotate(90deg);
}
}

.grw-ai-assistant-title-anchor {
width: 100%;
overflow: hidden;
font-size: 14px;
}


.grw-ai-assistant-actions {
transition: opacity 0.2s ease-out;

.btn-link {
&:hover {
color: var(--bs-gray-800) !important;
}
}
}

&:hover {
.grw-ai-assistant-actions {
opacity: 1 !important;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
import React from 'react';

import { useAiAssistantManagementModal } from '../../../stores/ai-assistant';
import { useAiAssistantManagementModal, useSWRxAiAssistants } from '../../../stores/ai-assistant';

import { AiAssistantTree } from './AiAssistantTree';

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

const moduleClass = styles['grw-ai-assistant-substance'] ?? '';

export const AiAssistantContent = (): JSX.Element => {
const { open } = useAiAssistantManagementModal();
const { data: aiAssistants, mutate: mutateAiAssistants } = useSWRxAiAssistants();

return (
<div>
<button type="button" className="btn btn-primary" onClick={open}>
アシスタントを追加する
{/* TODO i18n */}
<div className={moduleClass}>
<button
type="button"
className="btn btn-outline-secondary px-3 d-flex align-items-center mb-4"
onClick={open}
>
<span className="material-symbols-outlined fs-5 me-2">add</span>
<span className="fw-normal">アシスタントを追加する</span>
</button>

<div className="d-flex flex-column gap-4">
<div>
<h3 className="fw-bold grw-ai-assistant-substance-header">
マイアシスタント
</h3>
{aiAssistants?.myAiAssistants != null && aiAssistants.myAiAssistants.length !== 0 && (
<AiAssistantTree
onDeleted={mutateAiAssistants}
aiAssistants={aiAssistants.myAiAssistants}
/>
)}
</div>

<div>
<h3 className="fw-bold grw-ai-assistant-substance-header">
チームアシスタント
</h3>
{aiAssistants?.teamAiAssistants != null && aiAssistants.teamAiAssistants.length !== 0 && (
<AiAssistantTree
aiAssistants={aiAssistants.teamAiAssistants}
/>
)}
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import React, { useCallback, useState } from 'react';

import { getIdStringForRef } from '@growi/core';

import { toastError, toastSuccess } from '~/client/util/toastr';
import { useCurrentUser } from '~/stores-universal/context';

import type { AiAssistantAccessScope } from '../../../../interfaces/ai-assistant';
import { AiAssistantShareScope, type AiAssistantHasId } from '../../../../interfaces/ai-assistant';
import { deleteAiAssistant } from '../../../services/ai-assistant';


type Thread = {
_id: string;
name: string;
}

const dummyThreads: Thread[] = [
{ _id: '1', name: 'thread1' },
{ _id: '2', name: 'thread2' },
{ _id: '3', name: 'thread3' },
];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


type ThreadItemProps = {
thread: Thread;
};

const ThreadItem: React.FC<ThreadItemProps> = ({
thread,
}) => {

const deleteThreadHandler = useCallback(() => {
// TODO: https://redmine.weseek.co.jp/issues/161490
}, []);

const openChatHandler = useCallback(() => {
// TODO: https://redmine.weseek.co.jp/issues/159530
}, []);

return (
<li
role="button"
className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1 ps-5"
onClick={openChatHandler}
>
<div>
<span className="material-symbols-outlined fs-5">chat</span>
</div>

<div className="grw-ai-assistant-title-anchor ps-1">
<p className="text-truncate m-auto">{thread.name}</p>
</div>

<div className="grw-ai-assistant-actions opacity-0 d-flex justify-content-center ">
<button
type="button"
className="btn btn-link text-secondary p-0"
onClick={deleteThreadHandler}
>
<span className="material-symbols-outlined fs-5">delete</span>
</button>
</div>
</li>
);
};


const getShareScopeIcon = (shareScope: AiAssistantShareScope, accessScope: AiAssistantAccessScope): string => {
const determinedSharedScope = shareScope === AiAssistantShareScope.SAME_AS_ACCESS_SCOPE ? accessScope : shareScope;
switch (determinedSharedScope) {
case AiAssistantShareScope.OWNER:
return 'lock';
case AiAssistantShareScope.GROUPS:
return 'account_tree';
case AiAssistantShareScope.PUBLIC_ONLY:
return 'group';
}
};

type AiAssistantItemProps = {
currentUserId?: string;
aiAssistant: AiAssistantHasId;
threads: Thread[];
onDeleted?: () => void;
};

const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
currentUserId,
aiAssistant,
threads,
onDeleted,
}) => {
const [isThreadsOpened, setIsThreadsOpened] = useState(false);

const openChatHandler = useCallback(() => {
// TODO: https://redmine.weseek.co.jp/issues/159530
}, []);

const openThreadsHandler = useCallback(() => {
setIsThreadsOpened(toggle => !toggle);
}, []);

const deleteAiAssistantHandler = useCallback(async() => {
try {
await deleteAiAssistant(aiAssistant._id);
onDeleted?.();
toastSuccess('アシスタントを削除しました');
}
catch (err) {
toastError('アシスタントの削除に失敗しました');
}
}, [aiAssistant._id, onDeleted]);
Copy link
Member Author

@miya miya Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ゴミ箱ボタンをクリックした時に AiAssistant を削除できるようにしました
  • Figma にはデザインがありませんでしたが、削除する時に最終確認モーダルを表示しても良いと思いました
    • 必要があれば後続タスクでやろうと思います


const isOperable = currentUserId != null && getIdStringForRef(aiAssistant.owner) === currentUserId;

return (
<div className="grw-ai-assistant-item-container">
<li
onClick={openChatHandler}
role="button"
className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1"
>
<div className="d-flex justify-content-center">
<button
type="button"
onClick={openThreadsHandler}
className={`grw-ai-assistant-triangle-btn btn px-0 ${isThreadsOpened ? 'grw-ai-assistant-open' : ''}`}
>
<div className="d-flex justify-content-center">
<span className="material-symbols-outlined fs-5">arrow_right</span>
</div>
</button>
</div>

<div className="d-flex justify-content-center">
<span className="material-symbols-outlined fs-5">{getShareScopeIcon(aiAssistant.shareScope, aiAssistant.accessScope)}</span>
</div>

<div className="grw-ai-assistant-title-anchor ps-1">
<p className="text-truncate m-auto">{aiAssistant.name}</p>
</div>

{ isOperable && (
<div className="grw-ai-assistant-actions opacity-0 d-flex justify-content-center ">
<button
type="button"
className="btn btn-link text-secondary p-0 ms-2"
>
<span className="material-symbols-outlined fs-5">edit</span>
</button>
<button
type="button"
className="btn btn-link text-secondary p-0"
onClick={deleteAiAssistantHandler}
>
<span className="material-symbols-outlined fs-5">delete</span>
</button>
</div>
)}
</li>

{isThreadsOpened && threads.length > 0 && (
<div className="grw-ai-assistant-item-children">
{threads.map(thread => (
<ThreadItem
key={thread._id}
thread={thread}
/>
))}
</div>
)}
</div>
);
};

type AiAssistantTreeProps = {
aiAssistants: AiAssistantHasId[];
onDeleted?: () => void;
};

export const AiAssistantTree: React.FC<AiAssistantTreeProps> = ({ aiAssistants, onDeleted }) => {
const { data: currentUser } = useCurrentUser();
return (
<ul className="list-group">
{aiAssistants.map(assistant => (
<AiAssistantItem
key={assistant._id}
currentUserId={currentUser?._id}
aiAssistant={assistant}
threads={dummyThreads}
onDeleted={onDeleted}
/>
))}
</ul>
);
};
Loading
Loading