- {props.isLoading
- ? '読み込み中...'
- : props.title
- ? props.title
- : '[タイトル未入力]'}
-
-
-
+
+
+ {!props.previewMode &&
}
+
+ {props.isLoading
+ ? '読み込み中...'
+ : props.title
+ ? props.title
+ : '[タイトル未入力]'}
+
+ {!props.previewMode && (
+
+
+
- {props.canEdit && (
- <>
-
-
- >
- )}
+ {props.canEdit && (
+ <>
+
+
+ >
+ )}
+
-
- )}
+ )}
+
{props.description ?? ''}
diff --git a/packages/web/src/hooks/useCaseBuilder/useMyUseCases.ts b/packages/web/src/hooks/useCaseBuilder/useMyUseCases.ts
index 714dbeae..92341fed 100644
--- a/packages/web/src/hooks/useCaseBuilder/useMyUseCases.ts
+++ b/packages/web/src/hooks/useCaseBuilder/useMyUseCases.ts
@@ -170,7 +170,7 @@ const useMyUseCases = () => {
if (index > -1 && myUseCases) {
mutateMyUseCases(
produce(myUseCases, (draft) => {
- draft[index].hasShared = !myUseCases[index].hasShared;
+ draft[index].isShared = !myUseCases[index].isShared;
}),
{
revalidate: false,
@@ -184,8 +184,8 @@ const useMyUseCases = () => {
if (indexRecentlyUsed > -1 && recentlyUsedUseCases) {
mutateRecentlyUsedUseCases(
produce(recentlyUsedUseCases, (draft) => {
- draft[indexRecentlyUsed].hasShared =
- !recentlyUsedUseCases[indexRecentlyUsed].hasShared;
+ draft[indexRecentlyUsed].isShared =
+ !recentlyUsedUseCases[indexRecentlyUsed].isShared;
}),
{
revalidate: false,
diff --git a/packages/web/src/hooks/useCaseBuilder/useUseCaseBuilderApi.ts b/packages/web/src/hooks/useCaseBuilder/useUseCaseBuilderApi.ts
index e07d3234..51e2c5eb 100644
--- a/packages/web/src/hooks/useCaseBuilder/useUseCaseBuilderApi.ts
+++ b/packages/web/src/hooks/useCaseBuilder/useUseCaseBuilderApi.ts
@@ -30,32 +30,32 @@ const useUseCaseBuilderApi = () => {
useCaseId ? `/usecases/${useCaseId}` : null
);
},
- createUseCase: (params: CreateUseCaseRequest) => {
+ createUseCase: async (params: CreateUseCaseRequest) => {
return http
.post
('/usecases', params)
.then((res) => {
return res.data;
});
},
- updateUseCase: (useCaseId: string, params: UpdateUseCaseRequest) => {
+ updateUseCase: async (useCaseId: string, params: UpdateUseCaseRequest) => {
return http.put(
`/usecases/${useCaseId}`,
params
);
},
- updateRecentUseUseCase: (useCaseId: string) => {
+ updateRecentUseUseCase: async (useCaseId: string) => {
return http.put(`/usecases/recent/${useCaseId}`, {});
},
- deleteUseCase: (useCaseId: string) => {
+ deleteUseCase: async (useCaseId: string) => {
return http.delete(`/usecases/${useCaseId}`);
},
- toggleFavorite: (useCaseId: string) => {
+ toggleFavorite: async (useCaseId: string) => {
return http.put(
`/usecases/${useCaseId}/favorite`,
{}
);
},
- toggleShared: (useCaseId: string) => {
+ toggleShared: async (useCaseId: string) => {
return http.put(`/usecases/${useCaseId}/shared`, {});
},
};
diff --git a/packages/web/src/hooks/useScroll.ts b/packages/web/src/hooks/useFollow.ts
similarity index 66%
rename from packages/web/src/hooks/useScroll.ts
rename to packages/web/src/hooks/useFollow.ts
index 4d444ecf..d954b9e1 100644
--- a/packages/web/src/hooks/useScroll.ts
+++ b/packages/web/src/hooks/useFollow.ts
@@ -1,35 +1,27 @@
-import { useRef, useEffect, useState, useCallback } from 'react';
-import useObserveScreen from './useObserveScreen';
+import { useRef, useEffect, useState } from 'react';
+import useScreen from './useScreen';
-const useScroll = () => {
- const { isAtBottom } = useObserveScreen();
+const useFollow = () => {
+ const { isAtBottom, scrollToBottom } = useScreen();
// スクロールされる要素が含まれる要素
// サイズが動的に変更されることが想定される
// チャットのページであればメッセージを wrap した要素
const scrollableContainer = useRef(null);
- // スクロール下部
- const scrolledAnchor = useRef(null);
-
// フォローするか否か
// ページ最下部まで到達している場合はフォローする
// そうでない場合 (手動で上にスクロールした場合) はフォローしないようにする
const [following, setFollowing] = useState(true);
- // フォロー中であれば最下部までスクロールする
- const scrollToBottom = useCallback(() => {
- if (scrolledAnchor.current && following) {
- scrolledAnchor.current.scrollIntoView();
- }
- }, [scrolledAnchor, following]);
-
// scrollableContainer のサイズ変更を監視
useEffect(() => {
if (scrollableContainer.current) {
const observer = new ResizeObserver(() => {
// 画面サイズ変更されたらフォローする
- scrollToBottom();
+ if (following) {
+ scrollToBottom();
+ }
});
observer.observe(scrollableContainer.current);
@@ -48,10 +40,8 @@ const useScroll = () => {
return {
setFollowing,
- scrollToBottom,
scrollableContainer,
- scrolledAnchor,
};
};
-export default useScroll;
+export default useFollow;
diff --git a/packages/web/src/hooks/useObserveScreen.ts b/packages/web/src/hooks/useObserveScreen.ts
deleted file mode 100644
index 14a40c93..00000000
--- a/packages/web/src/hooks/useObserveScreen.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-import { create } from 'zustand';
-
-const useObserveScreenStore = create<{
- isAtBottom: boolean;
- setIsAtBottom: (newIsAtBottom: boolean) => void;
-}>((set) => {
- const setIsAtBottom = (newIsAtBottom: boolean) => {
- set(() => {
- return {
- isAtBottom: newIsAtBottom,
- };
- });
- };
-
- return {
- isAtBottom: false,
- setIsAtBottom,
- };
-});
-
-const useObserveScreen = () => {
- const { isAtBottom, setIsAtBottom } = useObserveScreenStore();
- const handleScroll = (e: React.UIEvent) => {
- const div = e.target as HTMLDivElement;
- // 最下部に到達している時に isAtBottom を true に
- // 小数点が省略されることがあるため、1.0 の余裕を設ける
- if (div.clientHeight + div.scrollTop + 1.0 >= div.scrollHeight) {
- setIsAtBottom(true);
- } else {
- setIsAtBottom(false);
- }
- };
-
- return {
- handleScroll,
- isAtBottom,
- setIsAtBottom,
- };
-};
-
-export default useObserveScreen;
diff --git a/packages/web/src/hooks/useRag.ts b/packages/web/src/hooks/useRag.ts
index 2a798c94..be70a7b5 100644
--- a/packages/web/src/hooks/useRag.ts
+++ b/packages/web/src/hooks/useRag.ts
@@ -18,7 +18,9 @@ const uniqueKeyOfItem = (item: RetrieveResultItem): string => {
return `${uri}_${pageNumber}`;
};
-const arrangeItems = (items: RetrieveResultItem[]): RetrieveResultItem[] => {
+export const arrangeItems = (
+ items: RetrieveResultItem[]
+): RetrieveResultItem[] => {
const res: Record = {};
for (const item of items) {
@@ -91,7 +93,7 @@ const useRag = (id: string) => {
id: id,
});
- // Kendra から 参考ドキュメントを Retrieve してシステムコンテキストとして設定する
+ // Kendra から 参考ドキュメントを Retrieve してシステムプロンプトとして設定する
let items: RetrieveResultItem[] = [];
try {
const retrievedItems = await retrieve(query);
diff --git a/packages/web/src/hooks/useRagKnowledgeBase.ts b/packages/web/src/hooks/useRagKnowledgeBase.ts
index e144f9b2..69490726 100644
--- a/packages/web/src/hooks/useRagKnowledgeBase.ts
+++ b/packages/web/src/hooks/useRagKnowledgeBase.ts
@@ -5,6 +5,7 @@ import { getPrompter } from '../prompts';
import { RetrieveResultItem } from '@aws-sdk/client-kendra';
import { ShownMessage } from 'generative-ai-use-cases-jp';
import { cleanEncode } from '../utils/URLUtils';
+import { arrangeItems } from './useRag';
// s3:/// から https://s3..amazonaws.com// に変換する
const convertS3UriToUrl = (s3Uri: string, region: string): string => {
@@ -99,19 +100,30 @@ const useRagKnowledgeBase = (id: string) => {
retrievedItems.data.retrievalResults!.map((r, idx) => {
const sourceUri =
r.metadata?.['x-amz-bedrock-kb-source-uri']?.toString() ?? '';
+ const pageNumber =
+ r.metadata?.['x-amz-bedrock-kb-document-page-number'];
return {
Content: r.content?.text ?? '',
DocumentId: `${idx}`,
DocumentTitle: sourceUri.split('/').pop(),
DocumentURI: convertS3UriToUrl(sourceUri, modelRegion),
+ DocumentAttributes: pageNumber
+ ? [
+ {
+ Key: '_excerpt_page_number',
+ Value: { LongValue: Number(pageNumber) },
+ },
+ ]
+ : [],
};
});
+ const items = arrangeItems(retrievedItemsKendraFormat);
updateSystemContext(
prompter.ragPrompt({
promptType: 'SYSTEM_CONTEXT',
- referenceItems: retrievedItemsKendraFormat,
+ referenceItems: items,
})
);
@@ -129,13 +141,21 @@ const useRagKnowledgeBase = (id: string) => {
},
(message: string) => {
// 後処理:Footnote の付与
- const footnote = retrievedItemsKendraFormat
+ const footnote = items
.map((item, idx) => {
- const encodedURI = item.DocumentURI
- ? cleanEncode(item.DocumentURI)
- : '';
+ // 参考にしたページ番号がある場合は、アンカーリンクとして設定する
+ const _excerpt_page_number = item.DocumentAttributes?.find(
+ (attr) => attr.Key === '_excerpt_page_number'
+ )?.Value?.LongValue;
return message.includes(`[^${idx}]`)
- ? `[^${idx}]: [${item.DocumentTitle}](${encodedURI})`
+ ? `[^${idx}]: [${item.DocumentTitle}${
+ _excerpt_page_number
+ ? `(${_excerpt_page_number} ページ)`
+ : ''
+ }](
+ ${item.DocumentURI ? cleanEncode(item.DocumentURI) : ''}${
+ _excerpt_page_number ? `#page=${_excerpt_page_number}` : ''
+ })`
: '';
})
.filter((x) => x)
diff --git a/packages/web/src/hooks/useScreen.ts b/packages/web/src/hooks/useScreen.ts
new file mode 100644
index 00000000..bbd7d3a3
--- /dev/null
+++ b/packages/web/src/hooks/useScreen.ts
@@ -0,0 +1,157 @@
+import { useEffect, useRef, useCallback } from 'react';
+import { create } from 'zustand';
+
+const useScreenStore = create<{
+ isAtBottom: boolean;
+ isAtTop: boolean;
+ setIsAtBottom: (newIsAtBottom: boolean) => void;
+ setIsAtTop: (newIsAtTop: boolean) => void;
+ scrollTopAnchor: HTMLDivElement | null;
+ scrollBottomAnchor: HTMLDivElement | null;
+ setScrollTopAnchor: (newScrollTopAnchor: HTMLDivElement | null) => void;
+ setScrollBottomAnchor: (newScrollBottomAnchor: HTMLDivElement | null) => void;
+}>((set) => {
+ const setIsAtBottom = (newIsAtBottom: boolean) => {
+ set(() => {
+ return {
+ isAtBottom: newIsAtBottom,
+ };
+ });
+ };
+
+ const setIsAtTop = (newIsAtTop: boolean) => {
+ set(() => {
+ return {
+ isAtTop: newIsAtTop,
+ };
+ });
+ };
+
+ const setScrollTopAnchor = (newScrollTopAnchor: HTMLDivElement | null) => {
+ set(() => {
+ return {
+ scrollTopAnchor: newScrollTopAnchor,
+ };
+ });
+ };
+
+ const setScrollBottomAnchor = (
+ newScrollBottomAnchor: HTMLDivElement | null
+ ) => {
+ set(() => {
+ return {
+ scrollBottomAnchor: newScrollBottomAnchor,
+ };
+ });
+ };
+
+ return {
+ isAtBottom: false,
+ isAtTop: false,
+ setIsAtBottom,
+ setIsAtTop,
+ scrollTopAnchor: null,
+ scrollBottomAnchor: null,
+ setScrollTopAnchor,
+ setScrollBottomAnchor,
+ };
+});
+
+const useScreen = () => {
+ const {
+ isAtBottom,
+ isAtTop,
+ setIsAtBottom,
+ setIsAtTop,
+ scrollTopAnchor,
+ setScrollTopAnchor,
+ scrollBottomAnchor,
+ setScrollBottomAnchor,
+ } = useScreenStore();
+
+ const screen = useRef(null);
+
+ // スクリーンのサイズや位置が変わったことを通知する関数
+ // 初期時、スクロール時に呼ばれる
+ // チャットの要素読み込み完了時には自動で下までスクロールされるため、そこでも呼ばれる
+ const notifyScreen = useCallback(
+ (div: HTMLDivElement) => {
+ // 最下部に到達している時に isAtBottom を true に
+ // 小数点が省略されることがあるため、1.0 の余裕を設ける
+ if (div.clientHeight + div.scrollTop + 1.0 >= div.scrollHeight) {
+ setIsAtBottom(true);
+ } else {
+ setIsAtBottom(false);
+ }
+
+ // 最上部に到達している時に isAtTop を true に
+ // 小数点が省略されることがあるため、1.0 の余裕を設ける
+ if (div.scrollTop <= 1.0) {
+ setIsAtTop(true);
+ } else {
+ setIsAtTop(false);
+ }
+ },
+ [setIsAtBottom, setIsAtTop]
+ );
+
+ // screen (App.tsx に定義されている) が設定された際に scroll イベントの listener を設定する
+ useEffect(() => {
+ const current = screen.current;
+
+ if (!current) return;
+
+ const handleScrollInner = () => {
+ notifyScreen(current);
+ };
+
+ screen.current.addEventListener('scroll', handleScrollInner);
+ notifyScreen(current);
+
+ return () => {
+ current.removeEventListener('scroll', handleScrollInner);
+ };
+ }, [screen, notifyScreen]);
+
+ const scrollTopAnchorRef = useRef(null);
+ const scrollBottomAnchorRef = useRef(null);
+
+ useEffect(() => {
+ if (scrollTopAnchorRef.current) {
+ setScrollTopAnchor(scrollTopAnchorRef.current);
+ }
+ }, [scrollTopAnchorRef, setScrollTopAnchor]);
+
+ useEffect(() => {
+ if (scrollBottomAnchorRef.current) {
+ setScrollBottomAnchor(scrollBottomAnchorRef.current);
+ }
+ }, [scrollBottomAnchorRef, setScrollBottomAnchor]);
+
+ const scrollToBottom = useCallback(() => {
+ if (scrollBottomAnchor) {
+ scrollBottomAnchor.scrollIntoView();
+ }
+ }, [scrollBottomAnchor]);
+
+ const scrollToTop = useCallback(() => {
+ if (scrollTopAnchor) {
+ scrollTopAnchor.scrollIntoView();
+ }
+ }, [scrollTopAnchor]);
+
+ return {
+ screen,
+ notifyScreen,
+ isAtBottom,
+ isAtTop,
+ setIsAtBottom,
+ setIsAtTop,
+ scrollTopAnchorRef,
+ scrollBottomAnchorRef,
+ scrollToBottom,
+ scrollToTop,
+ };
+};
+
+export default useScreen;
diff --git a/packages/web/src/main.tsx b/packages/web/src/main.tsx
index 48f1fcbf..2792ff03 100644
--- a/packages/web/src/main.tsx
+++ b/packages/web/src/main.tsx
@@ -37,8 +37,9 @@ import { Authenticator } from '@aws-amplify/ui-react';
import UseCaseBuilderEditPage from './pages/useCaseBuilder/UseCaseBuilderEditPage.tsx';
import App from './App.tsx';
import UseCaseBuilderRoot from './UseCaseBuilderRoot.tsx';
-import UseCaseBuilderConsolePage from './pages/useCaseBuilder/UseCaseBuilderConsolePage.tsx';
import UseCaseBuilderExecutePage from './pages/useCaseBuilder/UseCaseBuilderExecutePage.tsx';
+import UseCaseBuilderSamplesPage from './pages/useCaseBuilder/UseCaseBuilderSamplesPage.tsx';
+import UseCaseBuilderMyUseCasePage from './pages/useCaseBuilder/UseCaseBuilderMyUseCasePage.tsx';
const ragEnabled: boolean = import.meta.env.VITE_APP_RAG_ENABLED === 'true';
const ragKnowledgeBaseEnabled: boolean =
@@ -171,7 +172,11 @@ export const ROUTE_INDEX_USE_CASE_BUILDER = '/use-case-builder';
const useCaseBuilderRoutes: RouteObject[] = [
{
path: ROUTE_INDEX_USE_CASE_BUILDER,
- element: ,
+ element: ,
+ },
+ {
+ path: `${ROUTE_INDEX_USE_CASE_BUILDER}/my-use-case`,
+ element: ,
},
{
path: `${ROUTE_INDEX_USE_CASE_BUILDER}/new`,
@@ -181,10 +186,6 @@ const useCaseBuilderRoutes: RouteObject[] = [
path: `${ROUTE_INDEX_USE_CASE_BUILDER}/edit/:useCaseId`,
element: ,
},
- {
- path: `${ROUTE_INDEX_USE_CASE_BUILDER}/chat/:chatId`,
- element: ,
- },
{
path: `${ROUTE_INDEX_USE_CASE_BUILDER}/execute/:useCaseId`,
element: ,
diff --git a/packages/web/src/pages/AgentChatPage.tsx b/packages/web/src/pages/AgentChatPage.tsx
index 261d3432..a28f3c05 100644
--- a/packages/web/src/pages/AgentChatPage.tsx
+++ b/packages/web/src/pages/AgentChatPage.tsx
@@ -5,7 +5,8 @@ import useChat from '../hooks/useChat';
import useChatList from '../hooks/useChatList';
import ChatMessage from '../components/ChatMessage';
import Select from '../components/Select';
-import useScroll from '../hooks/useScroll';
+import ScrollTopBottom from '../components/ScrollTopBottom';
+import useFollow from '../hooks/useFollow';
import { create } from 'zustand';
import BedrockIcon from '../assets/bedrock.svg?react';
import { AgentPageQueryParams } from '../@types/navigate';
@@ -76,7 +77,7 @@ const AgentChatPage: React.FC = () => {
postChat,
updateSystemContextByModel,
} = useChat(pathname, chatId);
- const { scrollableContainer, scrolledAnchor, setFollowing } = useScroll();
+ const { scrollableContainer, setFollowing } = useFollow();
const { getChatTitle } = useChatList();
const { agentNames: availableModels } = MODELS;
const modelId = getModelId();
@@ -215,7 +216,10 @@ const AgentChatPage: React.FC = () => {
))}
-
+
+
+
+
{
} = useChat(pathname, chatId);
const { createShareId, findShareId, deleteShareId } = useChatApi();
const { createSystemContext } = useSystemContextApi();
- const { scrollableContainer, scrolledAnchor, setFollowing } = useScroll();
+ const { scrollableContainer, setFollowing } = useFollow();
const { getChatTitle } = useChatList();
const { modelIds: availableModels } = MODELS;
const { data: share, mutate: reloadShare } = findShareId(chatId);
@@ -139,7 +140,7 @@ const ChatPage: React.FC = () => {
}, [modelId]);
useEffect(() => {
- // 会話履歴のページではモデルを変更してもシステムコンテキストを変更しない
+ // 会話履歴のページではモデルを変更してもシステムプロンプトを変更しない
if (!chatId) {
updateSystemContextByModel();
}
@@ -410,7 +411,7 @@ const ChatPage: React.FC = () => {
)}
@@ -425,17 +426,22 @@ const ChatPage: React.FC = () => {