Browse Source

Impl RightSidebar

Shun Miyazawa 1 year ago
parent
commit
bade360cb7

+ 261 - 0
apps/app/src/client/components/RightSidebar.tsx

@@ -0,0 +1,261 @@
+import {
+  type FC,
+  memo, useCallback, useState, useRef, useEffect,
+} from 'react';
+
+import withLoadingProps from 'next-dynamic-loading-props';
+import dynamic from 'next/dynamic';
+import SimpleBar from 'simplebar-react';
+import { useIsomorphicLayoutEffect } from 'usehooks-ts';
+
+import { ResizableAreaFallback } from '~/client/components/Sidebar/ResizableArea/ResizableAreaFallback';
+import type { ResizableAreaProps } from '~/client/components/Sidebar/ResizableArea/props';
+
+// Constants
+const RIGHT_SIDEBAR_MIN_WIDTH = 320;
+const RIGHT_SIDEBAR_DEFAULT_WIDTH = 700;
+
+// Import ResizableArea with loading fallback
+const ResizableArea = withLoadingProps<ResizableAreaProps>(useLoadingProps => dynamic(
+  () => import('~/client/components/Sidebar/ResizableArea').then(mod => mod.ResizableArea),
+  {
+    ssr: false,
+    loading: () => {
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const { children, ...rest } = useLoadingProps();
+      return <ResizableAreaFallback {...rest}>{children}</ResizableAreaFallback>;
+    },
+  },
+));
+
+// Assistant Info Component
+const AssistantInfo: FC = () => {
+  return (
+    <div className="p-3">
+      <h6>アシスタントへの指示</h6>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+      <p>あなたは生成 AI の専門家および、リサーチャーです。ナレッジベースの Wiki ツールである GROWI の AI 機能に関する情報...</p>
+
+
+      <h6 className="mt-4">参照するページ</h6>
+      <ul className="list-unstyled">
+        <li><a href="#">/Project/GROWI/新機能/GROWI AI/*</a></li>
+        <li><a href="#">/AI導入検討/調査</a></li>
+      </ul>
+    </div>
+  );
+};
+
+// Resizable Container
+type ResizableContainerProps = {
+  children: React.ReactNode;
+  onResize?: (width: number) => void;
+  initialWidth?: number;
+};
+
+const ResizableContainer: FC<ResizableContainerProps> = memo(({ children, onResize, initialWidth = RIGHT_SIDEBAR_DEFAULT_WIDTH }) => {
+  const [width, setWidth] = useState(initialWidth);
+
+  const handleResize = useCallback((newWidth: number) => {
+    setWidth(newWidth);
+    onResize?.(newWidth);
+  }, [onResize]);
+
+  return (
+    <div className="d-flex h-100">
+      <ResizableArea
+        className="flex-grow-1 transition-width"
+        width={width}
+        minWidth={RIGHT_SIDEBAR_MIN_WIDTH}
+        onResize={handleResize}
+      >
+        {children}
+      </ResizableArea>
+    </div>
+  );
+});
+
+// Right Sidebar Tab
+type RightSidebarTabProps = {
+  isOpen: boolean;
+  onClick: () => void;
+};
+
+const RightSidebarTab: FC<RightSidebarTabProps> = memo(({ isOpen, onClick }) => {
+  return (
+    <button
+      type="button"
+      className="position-fixed top-50 end-0 bg-white border-start border-top border-bottom px-2 py-3 translate-middle-y"
+      style={{
+        transform: 'translateX(-100%) translateY(-50%) rotate(-90deg)',
+        transformOrigin: '100% 50%',
+        borderRadius: '4px 4px 0 0',
+        zIndex: 1030,
+      }}
+      onClick={onClick}
+    >
+      <span className="d-flex align-items-center">
+        <span className="material-symbols-outlined me-2" style={{ transform: 'rotate(90deg)' }}>
+          {isOpen ? 'close' : 'robot'}
+        </span>
+        GROWI AI について
+      </span>
+    </button>
+  );
+});
+
+// Right Sidebar Head
+type RightSidebarHeadProps = {
+  onClose: () => void;
+};
+
+const RightSidebarHead: FC<RightSidebarHeadProps> = memo(({ onClose }) => {
+  return (
+    <div className="d-flex align-items-center p-3 border-bottom">
+      <span className="material-symbols-outlined me-2">robot</span>
+      <h6 className="mb-0 flex-grow-1">GROWI AI について</h6>
+      <button
+        type="button"
+        className="btn btn-link p-0 border-0"
+        onClick={onClose}
+      >
+        <span className="material-symbols-outlined">close</span>
+      </button>
+    </div>
+  );
+});
+
+// Main Right Sidebar Component
+export const RightSidebar: FC = memo((): JSX.Element => {
+  const [isOpen, setIsOpen] = useState(false);
+  const [isClient, setClient] = useState(false);
+  const [width, setWidth] = useState(RIGHT_SIDEBAR_DEFAULT_WIDTH);
+  const sidebarRef = useRef<HTMLDivElement>(null);
+  const sidebarScrollerRef = useRef<HTMLDivElement>(null);
+
+  useIsomorphicLayoutEffect(() => {
+    setClient(true);
+  }, []);
+
+  const handleToggle = useCallback(() => {
+    setIsOpen(prev => !prev);
+  }, []);
+
+  // Handle clicks outside of sidebar
+  useEffect(() => {
+    const handleClickOutside = (event: MouseEvent) => {
+      if (isOpen && sidebarRef.current && !sidebarRef.current.contains(event.target as Node)) {
+        setIsOpen(false);
+      }
+    };
+
+    document.addEventListener('mousedown', handleClickOutside);
+    return () => {
+      document.removeEventListener('mousedown', handleClickOutside);
+    };
+  }, [isOpen]);
+
+  if (!isClient) {
+    return <></>;
+  }
+
+  return (
+    <>
+      <RightSidebarTab
+        isOpen={isOpen}
+        onClick={handleToggle}
+      />
+      {isOpen && (
+        <div
+          ref={sidebarRef}
+          className="position-fixed top-0 end-0 h-100 border-start bg-white shadow-sm"
+          style={{ zIndex: 1500 }}
+          data-testid="grw-right-sidebar"
+        >
+          <ResizableContainer onResize={setWidth}>
+            <div className="h-100 d-flex flex-column">
+              <RightSidebarHead onClose={handleToggle} />
+              <div className="flex-grow-1" style={{ height: 'calc(100% - 56px)' }}>
+                <SimpleBar
+                  scrollableNodeProps={{ ref: sidebarScrollerRef }}
+                  style={{ height: '100%' }}
+                  className="h-100 position-relative"
+                  autoHide
+                >
+                  <AssistantInfo />
+                </SimpleBar>
+              </div>
+            </div>
+          </ResizableContainer>
+        </div>
+      )}
+    </>
+  );
+});
+
+// Usage example:
+// Usage example:
+// <RightSidebar />

+ 5 - 0
apps/app/src/components/Layout/BasicLayout.tsx

@@ -8,6 +8,9 @@ import { RawLayout } from './RawLayout';
 
 import styles from './BasicLayout.module.scss';
 
+const RightSidebar = dynamic(() => import('~/client/components/RightSidebar').then(mod => mod.RightSidebar), { ssr: false });
+
+
 const moduleClass = styles['grw-basic-layout'] ?? '';
 
 
@@ -59,6 +62,8 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
           <AlertSiteUrlUndefined />
           {children}
         </div>
+
+        <RightSidebar />
       </div>
 
       <GrowiNavbarBottom />