diff --git a/packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.ts b/packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.ts index c3473d96..bea34fc6 100644 --- a/packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.ts +++ b/packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.ts @@ -157,6 +157,7 @@ export const createUseCase = async ( promptTemplate: content.promptTemplate, inputExamples: content.inputExamples, fixedModelId: content.fixedModelId, + fileUpload: content.fileUpload, isShared: false, }; @@ -258,13 +259,14 @@ export const updateUseCase = async ( dataType: useCaseInTable.dataType, }, UpdateExpression: - 'set title = :title, promptTemplate = :promptTemplate, description = :description, inputExamples = :inputExamples, fixedModelId = :fixedModelId', + 'set title = :title, promptTemplate = :promptTemplate, description = :description, inputExamples = :inputExamples, fixedModelId = :fixedModelId, fileUpload = :fileUpload', ExpressionAttributeValues: { ':title': content.title, ':promptTemplate': content.promptTemplate, ':description': content.description ?? '', ':inputExamples': content.inputExamples ?? [], ':fixedModelId': content.fixedModelId ?? '', + ':fileUpload': !!content.fileUpload, }, }) ); diff --git a/packages/types/src/useCaseBuilder.d.ts b/packages/types/src/useCaseBuilder.d.ts index e304f602..a0f536d6 100644 --- a/packages/types/src/useCaseBuilder.d.ts +++ b/packages/types/src/useCaseBuilder.d.ts @@ -19,6 +19,7 @@ export type UseCaseContent = { promptTemplate: string; inputExamples?: UseCaseInputExample[]; fixedModelId?: string; + fileUpload?: boolean; }; // Table に記録されている内容 diff --git a/packages/web/src/components/useCaseBuilder/UseCaseBuilderView.tsx b/packages/web/src/components/useCaseBuilder/UseCaseBuilderView.tsx index d76a093f..2e840229 100644 --- a/packages/web/src/components/useCaseBuilder/UseCaseBuilderView.tsx +++ b/packages/web/src/components/useCaseBuilder/UseCaseBuilderView.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useState, + useRef, +} from 'react'; import Select from '../Select'; import Button from '../Button'; import useChat from '../../hooks/useChat'; @@ -15,7 +21,7 @@ import ButtonShare from './ButtonShare'; import ButtonUseCaseEdit from './ButtonUseCaseEdit'; import Skeleton from '../Skeleton'; import useMyUseCases from '../../hooks/useCaseBuilder/useMyUseCases'; -import { UseCaseInputExample } from 'generative-ai-use-cases-jp'; +import { UseCaseInputExample, FileLimit } from 'generative-ai-use-cases-jp'; import { NOLABEL, extractPlaceholdersFromPromptTemplate, @@ -25,11 +31,43 @@ import { } from '../../utils/UseCaseBuilderUtils'; import useRagKnowledgeBaseApi from '../../hooks/useRagKnowledgeBaseApi'; import useRagApi from '../../hooks/useRagApi'; +import useFiles from '../../hooks/useFiles'; +import ZoomUpImage from '../ZoomUpImage'; +import ZoomUpVideo from '../ZoomUpVideo'; +import FileCard from '../FileCard'; +import { PiPaperclip, PiSpinnerGap } from 'react-icons/pi'; const ragEnabled: boolean = import.meta.env.VITE_APP_RAG_ENABLED === 'true'; const ragKnowledgeBaseEnabled: boolean = import.meta.env.VITE_APP_RAG_KNOWLEDGE_BASE_ENABLED === 'true'; +// pages/ChatPage.tsx に合わせている +// 差分が生まれた場合は更新する +const fileLimit: FileLimit = { + accept: { + doc: [ + '.csv', + '.doc', + '.docx', + '.html', + '.md', + '.pdf', + '.txt', + '.xls', + '.xlsx', + '.gif', + ], + image: ['.jpg', '.jpeg', '.png', '.webp'], + video: ['.mkv', '.mov', '.mp4', '.webm'], + }, + maxFileCount: 5, + maxFileSizeMB: 4.5, + maxImageFileCount: 20, + maxImageFileSizeMB: 3.75, + maxVideoFileCount: 1, + maxVideoFileSizeMB: 25, // 25 MB for base64 input (TODO: up to 1 GB through S3) +}; + type Props = { modelId?: string; title: string; @@ -37,6 +75,7 @@ type Props = { description?: string; inputExamples?: UseCaseInputExample[]; fixedModelId: string; + fileUpload: boolean; isLoading?: boolean; } & ( | { @@ -118,6 +157,16 @@ const UseCaseBuilderView: React.FC = (props) => { const { updateRecentUseUseCase } = useMyUseCases(); const { retrieve: retrieveKendra } = useRagApi(); const { retrieve: retrieveKnowledgeBase } = useRagKnowledgeBaseApi(); + const { + uploadedFiles, + uploadFiles, + checkFiles, + deleteUploadedFile, + uploading, + errorMessages: fileErrorMessages, + clear: clearFiles, + } = useFiles(); + const [errorMessages, setErrorMessages] = useState([]); const placeholders = useMemo(() => { @@ -207,8 +256,10 @@ const UseCaseBuilderView: React.FC = (props) => { } } + tmpErrorMessages.push(...fileErrorMessages); + setErrorMessages(tmpErrorMessages); - }, [setErrorMessages, items, textFormItems]); + }, [setErrorMessages, items, textFormItems, fileErrorMessages]); const onClickExec = useCallback(async () => { if (loading) return; @@ -259,7 +310,14 @@ const UseCaseBuilderView: React.FC = (props) => { } } - postChat(prompt, true); + postChat( + prompt, + true, + undefined, + undefined, + undefined, + uploadedFiles.length > 0 ? uploadedFiles : undefined + ); if (!props.previewMode) { updateRecentUseUseCase(props.useCaseId); } @@ -275,13 +333,15 @@ const UseCaseBuilderView: React.FC = (props) => { retrieveKendra, retrieveKnowledgeBase, setText, + uploadedFiles, ]); // リセット const onClickClear = useCallback(() => { clear(textFormUniqueLabels); clearChat(); - }, [clear, clearChat, textFormUniqueLabels]); + clearFiles(); + }, [clear, clearChat, clearFiles, textFormUniqueLabels]); const disabledExec = useMemo(() => { if (props.isLoading || loading) { @@ -304,6 +364,42 @@ const UseCaseBuilderView: React.FC = (props) => { [setValue] ); + const accept = useMemo(() => { + if (!modelId) return []; + const feature = MODELS.modelFeatureFlags[modelId]; + return [ + ...(feature.doc ? fileLimit.accept.doc : []), + ...(feature.image ? fileLimit.accept.image : []), + ...(feature.video ? fileLimit.accept.video : []), + ]; + }, [modelId]); + + useEffect(() => { + checkFiles(fileLimit, accept); + }, [accept, checkFiles]); + + const fileInput = useRef(null); + + const onChangeFile = async (e: React.ChangeEvent) => { + const files = e.target.files; + if (files) { + uploadFiles(Array.from(files), fileLimit, accept); + + if (fileInput.current) { + fileInput.current.value = ''; + } + } + }; + + const deleteFile = useCallback( + (fileUrl: string) => { + if (fileLimit && accept) { + deleteUploadedFile(fileUrl, fileLimit, accept); + } + }, + [deleteUploadedFile, accept] + ); + return (
@@ -389,6 +485,79 @@ const UseCaseBuilderView: React.FC = (props) => {
))}
+ + {props.fileUpload && ( +
+ + + {uploadedFiles.length > 0 && ( +
+ {uploadedFiles.map((uploadedFile, idx) => { + if (uploadedFile.type === 'image') { + return ( + 0} + onDelete={() => { + deleteFile(uploadedFile.s3Url ?? ''); + }} + /> + ); + } else if (uploadedFile.type === 'video') { + return ( + 0} + onDelete={() => { + deleteFile(uploadedFile.s3Url ?? ''); + }} + /> + ); + } else { + return ( + 0} + onDelete={() => { + deleteFile(uploadedFile.s3Url ?? ''); + }} + /> + ); + } + })} +
+ )} +
+ )} )}
diff --git a/packages/web/src/hooks/useCaseBuilder/useMyUseCases.ts b/packages/web/src/hooks/useCaseBuilder/useMyUseCases.ts index 85abec87..b07e98d3 100644 --- a/packages/web/src/hooks/useCaseBuilder/useMyUseCases.ts +++ b/packages/web/src/hooks/useCaseBuilder/useMyUseCases.ts @@ -51,6 +51,7 @@ const useMyUseCases = () => { description?: string; inputExamples?: UseCaseInputExample[]; fixedModelId?: string; + fileUpload?: boolean; }) => { return createUseCase(params).finally(() => { mutateMyUseCases(); @@ -63,6 +64,7 @@ const useMyUseCases = () => { description?: string; inputExamples?: UseCaseInputExample[]; fixedModelId?: string; + fileUpload?: boolean; }) => { // 一覧の更新 const index = findIndex(params.useCaseId); @@ -83,6 +85,7 @@ const useMyUseCases = () => { description: params.description, inputExamples: params.inputExamples, fixedModelId: params.fixedModelId, + fileUpload: params.fileUpload, }).finally(() => { mutateMyUseCases(); mutateFavoriteUseCases(); diff --git a/packages/web/src/pages/useCaseBuilder/UseCaseBuilderEditPage.tsx b/packages/web/src/pages/useCaseBuilder/UseCaseBuilderEditPage.tsx index c3b7f4e8..221876f6 100644 --- a/packages/web/src/pages/useCaseBuilder/UseCaseBuilderEditPage.tsx +++ b/packages/web/src/pages/useCaseBuilder/UseCaseBuilderEditPage.tsx @@ -44,6 +44,8 @@ type StateType = { setInputExamples: (inputExamples: UseCaseInputExample[]) => void; fixedModelId: string; setFixedModelId: (m: string) => void; + fileUpload: boolean; + setFileUpload: (u: boolean) => void; clear: () => void; }; @@ -54,6 +56,7 @@ const useUseCaseBuilderEditPageState = create((set, get) => { promptTemplate: '', inputExamples: [], fixedModelId: '', + fileUpload: false, }; return { ...INIT_STATE, @@ -109,6 +112,11 @@ const useUseCaseBuilderEditPageState = create((set, get) => { fixedModelId: m, })); }, + setFileUpload: (u: boolean) => { + set(() => ({ + fileUpload: u, + })); + }, clear: () => { set(INIT_STATE); }, @@ -136,6 +144,8 @@ const UseCaseBuilderEditPage: React.FC = () => { removeInputExample, fixedModelId, setFixedModelId, + fileUpload, + setFileUpload, clear, } = useUseCaseBuilderEditPageState(); @@ -171,6 +181,7 @@ const UseCaseBuilderEditPage: React.FC = () => { setDescription(useCase?.description ?? ''); setInputExamples(useCase?.inputExamples ?? []); setFixedModelId(useCase?.fixedModelId ?? ''); + setFileUpload(!!useCase?.fileUpload); // サンプル集から遷移した場合(RouterのStateから設定) } else if (state) { @@ -179,6 +190,7 @@ const UseCaseBuilderEditPage: React.FC = () => { setDescription(state.description ?? ''); setInputExamples(state.inputExamples ?? []); setFixedModelId(state.fixedModelId ?? ''); + setFileUpload(!!state.fileUpload); } else { clear(); } @@ -222,9 +234,9 @@ const UseCaseBuilderEditPage: React.FC = () => { title.replace(/[  ]/g, '') !== '' && // eslint-disable-next-line no-irregular-whitespace promptTemplate.replace(/[  ]/g, '') !== '' && - placeholders.length > 0 + (placeholders.length > 0 || fileUpload) ); - }, [placeholders.length, promptTemplate, title]); + }, [placeholders.length, promptTemplate, title, fileUpload]); const onClickRegister = useCallback(() => { setIsPosting(true); @@ -237,6 +249,7 @@ const UseCaseBuilderEditPage: React.FC = () => { description: description === '' ? undefined : description, inputExamples, fixedModelId, + fileUpload, }) .then(() => { // DB変更直後は更新ボタンをDisabledにする @@ -252,6 +265,7 @@ const UseCaseBuilderEditPage: React.FC = () => { description: description === '' ? undefined : description, inputExamples, fixedModelId, + fileUpload, }) .then(async (res) => { setUseCaseId(res.useCaseId); @@ -279,6 +293,7 @@ const UseCaseBuilderEditPage: React.FC = () => { updateUseCase, useCaseId, fixedModelId, + fileUpload, ]); const onClickDelete = useCallback(() => { @@ -456,7 +471,6 @@ const UseCaseBuilderEditPage: React.FC = () => {
{
+ +
+ { + setFileUpload(!fileUpload); + setIsDisabledUpdate(false); + }} + /> + +
+ 添付可能なファイルはモデルによって異なります +
+
+
{isUpdate ? ( @@ -560,6 +594,7 @@ const UseCaseBuilderEditPage: React.FC = () => { description={description} inputExamples={inputExamples} fixedModelId={fixedModelId} + fileUpload={fileUpload} previewMode /> ) : ( diff --git a/packages/web/src/pages/useCaseBuilder/UseCaseBuilderExecutePage.tsx b/packages/web/src/pages/useCaseBuilder/UseCaseBuilderExecutePage.tsx index 8641593c..0a8d4a9b 100644 --- a/packages/web/src/pages/useCaseBuilder/UseCaseBuilderExecutePage.tsx +++ b/packages/web/src/pages/useCaseBuilder/UseCaseBuilderExecutePage.tsx @@ -109,6 +109,7 @@ const UseCaseBuilderExecutePage: React.FC = () => { description={useCase?.description} inputExamples={useCase?.inputExamples} fixedModelId={useCase?.fixedModelId ?? ''} + fileUpload={!!useCase?.fileUpload} isShared={useCase?.isShared ?? false} isFavorite={useCase?.isFavorite ?? false} useCaseId={useCaseId ?? ''}