diff --git a/browser-extension/package-lock.json b/browser-extension/package-lock.json index 973f5e57..1a3304f9 100644 --- a/browser-extension/package-lock.json +++ b/browser-extension/package-lock.json @@ -7587,11 +7587,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 704a8c33..445efc40 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -42,7 +42,6 @@ export VITE_APP_AGENT_ENABLED= export VITE_APP_SELF_SIGN_UP_ENABLED= export VITE_APP_MODEL_REGION= export VITE_APP_MODEL_IDS= -export VITE_APP_MULTI_MODAL_MODEL_IDS= export VITE_APP_IMAGE_MODEL_IDS= export VITE_APP_ENDPOINT_NAMES= export VITE_APP_SAMLAUTH_ENABLED= @@ -67,7 +66,6 @@ export VITE_APP_AGENT_ENABLED=true export VITE_APP_SELF_SIGN_UP_ENABLED=true export VITE_APP_MODEL_REGION=us-west-2 export VITE_APP_MODEL_IDS=["anthropic.claude-instant-v1","anthropic.claude-v2"] -export VITE_APP_MULTI_MODAL_MODEL_IDS=["anthropic.claude-3-sonnet-20240229-v1:0"] export VITE_APP_IMAGE_MODEL_IDS=["stability.stable-diffusion-xl-v1","amazon.titan-image-generator-v1"] export VITE_APP_ENDPOINT_NAMES=[] export VITE_APP_SAMLAUTH_ENABLED=true diff --git a/package-lock.json b/package-lock.json index be48480f..e5157781 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7608,6 +7608,10 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@generative-ai-use-cases-jp/common": { + "resolved": "packages/common", + "link": true + }, "node_modules/@headlessui/react": { "version": "1.7.19", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", @@ -21230,6 +21234,10 @@ "typescript": "~5.4.5" } }, + "packages/common": { + "name": "@generative-ai-use-cases-jp/common", + "version": "1.0.0" + }, "packages/types": { "name": "@types/generative-ai-use-cases-jp", "dependencies": { diff --git a/packages/cdk/lib/construct/api.ts b/packages/cdk/lib/construct/api.ts index 58e73f1f..e08e7a31 100644 --- a/packages/cdk/lib/construct/api.ts +++ b/packages/cdk/lib/construct/api.ts @@ -21,6 +21,7 @@ import { HttpMethods, } from 'aws-cdk-lib/aws-s3'; import { Agent, AgentMap } from 'generative-ai-use-cases-jp'; +import { modelFeatureFlags } from '@generative-ai-use-cases-jp/common'; export interface BackendApiProps { userPool: UserPool; @@ -38,7 +39,6 @@ export class Api extends Construct { readonly optimizePromptFunction: NodejsFunction; readonly modelRegion: string; readonly modelIds: string[]; - readonly multiModalModelIds: string[]; readonly imageGenerationModelIds: string[]; readonly endpointNames: string[]; readonly agentNames: string[]; @@ -71,85 +71,7 @@ export class Api extends Construct { ]; // Validate Model Names - const supportedModelIds = [ - 'anthropic.claude-3-5-sonnet-20241022-v2:0', - 'anthropic.claude-3-5-haiku-20241022-v1:0', - 'anthropic.claude-3-5-sonnet-20240620-v1:0', - 'anthropic.claude-3-opus-20240229-v1:0', - 'anthropic.claude-3-sonnet-20240229-v1:0', - 'anthropic.claude-3-haiku-20240307-v1:0', - 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', - 'us.anthropic.claude-3-5-haiku-20241022-v1:0', - 'us.anthropic.claude-3-5-sonnet-20240620-v1:0', - 'us.anthropic.claude-3-opus-20240229-v1:0', - 'us.anthropic.claude-3-sonnet-20240229-v1:0', - 'us.anthropic.claude-3-haiku-20240307-v1:0', - 'eu.anthropic.claude-3-5-sonnet-20240620-v1:0', - 'eu.anthropic.claude-3-sonnet-20240229-v1:0', - 'eu.anthropic.claude-3-haiku-20240307-v1:0', - 'apac.anthropic.claude-3-5-sonnet-20240620-v1:0', - 'apac.anthropic.claude-3-sonnet-20240229-v1:0', - 'apac.anthropic.claude-3-haiku-20240307-v1:0', - 'anthropic.claude-v2:1', - 'anthropic.claude-v2', - 'anthropic.claude-instant-v1', - // Titan Express は日本語文字化けのため未対応 - // 'amazon.titan-text-express-v1', - 'amazon.titan-text-premier-v1:0', - 'stability.stable-diffusion-xl-v1', - 'stability.sd3-large-v1:0', - 'stability.stable-image-core-v1:0', - 'stability.stable-image-ultra-v1:0', - 'amazon.titan-image-generator-v2:0', - 'amazon.titan-image-generator-v1', - 'amazon.nova-canvas-v1:0', - 'meta.llama3-8b-instruct-v1:0', - 'meta.llama3-70b-instruct-v1:0', - 'meta.llama3-1-8b-instruct-v1:0', - 'meta.llama3-1-70b-instruct-v1:0', - 'meta.llama3-1-405b-instruct-v1:0', - 'us.meta.llama3-2-1b-instruct-v1:0', - 'us.meta.llama3-2-3b-instruct-v1:0', - 'us.meta.llama3-2-11b-instruct-v1:0', - 'us.meta.llama3-2-90b-instruct-v1:0', - 'mistral.mistral-7b-instruct-v0:2', - 'mistral.mixtral-8x7b-instruct-v0:1', - 'mistral.mistral-small-2402-v1:0', - 'mistral.mistral-large-2402-v1:0', - 'mistral.mistral-large-2407-v1:0', - 'cohere.command-r-v1:0', - 'cohere.command-r-plus-v1:0', - 'amazon.nova-pro-v1:0', - 'amazon.nova-lite-v1:0', - 'amazon.nova-micro-v1:0', - 'us.amazon.nova-pro-v1:0', - 'us.amazon.nova-lite-v1:0', - 'us.amazon.nova-micro-v1:0', - ]; - const multiModalModelIds = [ - 'anthropic.claude-3-5-sonnet-20241022-v2:0', - 'anthropic.claude-3-5-sonnet-20240620-v1:0', - 'anthropic.claude-3-opus-20240229-v1:0', - 'anthropic.claude-3-sonnet-20240229-v1:0', - 'anthropic.claude-3-haiku-20240307-v1:0', - 'us.anthropic.claude-3-5-sonnet-20241022-v2:0', - 'us.anthropic.claude-3-5-sonnet-20240620-v1:0', - 'us.anthropic.claude-3-opus-20240229-v1:0', - 'us.anthropic.claude-3-sonnet-20240229-v1:0', - 'us.anthropic.claude-3-haiku-20240307-v1:0', - 'eu.anthropic.claude-3-5-sonnet-20240620-v1:0', - 'eu.anthropic.claude-3-sonnet-20240229-v1:0', - 'eu.anthropic.claude-3-haiku-20240307-v1:0', - 'apac.anthropic.claude-3-5-sonnet-20240620-v1:0', - 'apac.anthropic.claude-3-sonnet-20240229-v1:0', - 'apac.anthropic.claude-3-haiku-20240307-v1:0', - 'us.meta.llama3-2-11b-instruct-v1:0', - 'us.meta.llama3-2-90b-instruct-v1:0', - 'amazon.nova-pro-v1:0', - 'amazon.nova-lite-v1:0', - 'us.amazon.nova-pro-v1:0', - 'us.amazon.nova-lite-v1:0', - ]; + const supportedModelIds = Object.keys(modelFeatureFlags); for (const modelId of modelIds) { if (!supportedModelIds.includes(modelId)) { throw new Error(`Unsupported Model Name: ${modelId}`); @@ -829,7 +751,6 @@ export class Api extends Construct { this.optimizePromptFunction = optimizePromptFunction; this.modelRegion = modelRegion; this.modelIds = modelIds; - this.multiModalModelIds = multiModalModelIds; this.imageGenerationModelIds = imageGenerationModelIds; this.endpointNames = endpointNames; this.agentNames = Object.keys(agentMap); diff --git a/packages/cdk/lib/construct/web.ts b/packages/cdk/lib/construct/web.ts index aca43e09..bb6a5101 100644 --- a/packages/cdk/lib/construct/web.ts +++ b/packages/cdk/lib/construct/web.ts @@ -28,7 +28,6 @@ export interface WebProps { webAclId?: string; modelRegion: string; modelIds: string[]; - multiModalModelIds: string[]; imageGenerationModelIds: string[]; endpointNames: string[]; samlAuthEnabled: boolean; @@ -178,9 +177,6 @@ export class Web extends Construct { VITE_APP_SELF_SIGN_UP_ENABLED: props.selfSignUpEnabled.toString(), VITE_APP_MODEL_REGION: props.modelRegion, VITE_APP_MODEL_IDS: JSON.stringify(props.modelIds), - VITE_APP_MULTI_MODAL_MODEL_IDS: JSON.stringify( - props.multiModalModelIds - ), VITE_APP_IMAGE_MODEL_IDS: JSON.stringify(props.imageGenerationModelIds), VITE_APP_ENDPOINT_NAMES: JSON.stringify(props.endpointNames), VITE_APP_SAMLAUTH_ENABLED: props.samlAuthEnabled.toString(), diff --git a/packages/cdk/lib/generative-ai-use-cases-stack.ts b/packages/cdk/lib/generative-ai-use-cases-stack.ts index 97cf606c..c36e9e13 100644 --- a/packages/cdk/lib/generative-ai-use-cases-stack.ts +++ b/packages/cdk/lib/generative-ai-use-cases-stack.ts @@ -158,7 +158,6 @@ export class GenerativeAiUseCasesStack extends Stack { webAclId: props.webAclId, modelRegion: api.modelRegion, modelIds: api.modelIds, - multiModalModelIds: api.multiModalModelIds, imageGenerationModelIds: api.imageGenerationModelIds, endpointNames: api.endpointNames, samlAuthEnabled, @@ -277,10 +276,6 @@ export class GenerativeAiUseCasesStack extends Stack { value: JSON.stringify(api.modelIds), }); - new CfnOutput(this, 'MultiModalModelIds', { - value: JSON.stringify(api.multiModalModelIds), - }); - new CfnOutput(this, 'ImageGenerateModelIds', { value: JSON.stringify(api.imageGenerationModelIds), }); diff --git a/packages/cdk/tsconfig.json b/packages/cdk/tsconfig.json index be82b593..ea143d04 100644 --- a/packages/cdk/tsconfig.json +++ b/packages/cdk/tsconfig.json @@ -21,7 +21,7 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "typeRoots": [ - "./node_modules/@types" + "../../node_modules/@types" ], "types": [ "node" diff --git a/packages/common/package.json b/packages/common/package.json new file mode 100644 index 00000000..43752db3 --- /dev/null +++ b/packages/common/package.json @@ -0,0 +1,5 @@ +{ + "name": "@generative-ai-use-cases-jp/common", + "private": true, + "main": "src/index.ts" +} \ No newline at end of file diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts new file mode 100644 index 00000000..9f8ccadd --- /dev/null +++ b/packages/common/src/index.ts @@ -0,0 +1 @@ +export * from './model'; diff --git a/packages/common/src/model.ts b/packages/common/src/model.ts new file mode 100644 index 00000000..d1e328c6 --- /dev/null +++ b/packages/common/src/model.ts @@ -0,0 +1,75 @@ +import { FeatureFlags } from 'generative-ai-use-cases-jp'; + +// Manage Model Feature +// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html +const MODEL_FEATURE: Record = { + TEXT_ONLY: { text: true, doc: false, image: false, video: false }, + TEXT_DOC: { text: true, doc: true, image: false, video: false }, + TEXT_DOC_IMAGE: { text: true, doc: true, image: true, video: false }, + TEXT_DOC_IMAGE_VIDEO: { text: true, doc: true, image: true, video: true }, + IMAGE_GEN: { image_gen: true }, + VIDEO_GEN: { video_gen: true }, +}; +export const modelFeatureFlags: Record = { + // Anthropic + 'anthropic.claude-3-5-sonnet-20241022-v2:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'anthropic.claude-3-5-haiku-20241022-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'anthropic.claude-3-5-sonnet-20240620-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'anthropic.claude-3-opus-20240229-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'anthropic.claude-3-sonnet-20240229-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'anthropic.claude-3-haiku-20240307-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'us.anthropic.claude-3-5-sonnet-20241022-v2:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'us.anthropic.claude-3-5-haiku-20241022-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'us.anthropic.claude-3-5-sonnet-20240620-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'us.anthropic.claude-3-opus-20240229-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'us.anthropic.claude-3-sonnet-20240229-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'us.anthropic.claude-3-haiku-20240307-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'eu.anthropic.claude-3-5-sonnet-20240620-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'eu.anthropic.claude-3-sonnet-20240229-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'eu.anthropic.claude-3-haiku-20240307-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'apac.anthropic.claude-3-5-sonnet-20240620-v1:0': + MODEL_FEATURE.TEXT_DOC_IMAGE, + 'apac.anthropic.claude-3-sonnet-20240229-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'apac.anthropic.claude-3-haiku-20240307-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'anthropic.claude-v2:1': MODEL_FEATURE.TEXT_DOC, + 'anthropic.claude-v2': MODEL_FEATURE.TEXT_DOC, + 'anthropic.claude-instant-v1': MODEL_FEATURE.TEXT_DOC, + // Amazon Titan + 'amazon.titan-text-express-v1': MODEL_FEATURE.TEXT_DOC, + 'amazon.titan-text-premier-v1:0': MODEL_FEATURE.TEXT_ONLY, + // Meta + 'meta.llama3-8b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC, + 'meta.llama3-70b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC, + 'meta.llama3-1-8b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC, + 'meta.llama3-1-70b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC, + 'meta.llama3-1-405b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC, + 'us.meta.llama3-2-1b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC, + 'us.meta.llama3-2-3b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC, + 'us.meta.llama3-2-11b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + 'us.meta.llama3-2-90b-instruct-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE, + // Mistral + 'mistral.mistral-7b-instruct-v0:2': MODEL_FEATURE.TEXT_DOC, + 'mistral.mixtral-8x7b-instruct-v0:1': MODEL_FEATURE.TEXT_DOC, + 'mistral.mistral-small-2402-v1:0': MODEL_FEATURE.TEXT_ONLY, + 'mistral.mistral-large-2402-v1:0': MODEL_FEATURE.TEXT_DOC, + 'mistral.mistral-large-2407-v1:0': MODEL_FEATURE.TEXT_DOC, + // Cohere + 'cohere.command-r-v1:0': MODEL_FEATURE.TEXT_DOC, + 'cohere.command-r-plus-v1:0': MODEL_FEATURE.TEXT_DOC, + // Amazon Nova + 'amazon.nova-pro-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE_VIDEO, + 'amazon.nova-lite-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE_VIDEO, + 'amazon.nova-micro-v1:0': MODEL_FEATURE.TEXT_ONLY, + 'us.amazon.nova-pro-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE_VIDEO, + 'us.amazon.nova-lite-v1:0': MODEL_FEATURE.TEXT_DOC_IMAGE_VIDEO, + 'us.amazon.nova-micro-v1:0': MODEL_FEATURE.TEXT_ONLY, + // Stability AI Image Gen + 'stability.stable-diffusion-xl-v1': MODEL_FEATURE.IMAGE_GEN, + 'stability.sd3-large-v1:0': MODEL_FEATURE.IMAGE_GEN, + 'stability.stable-image-core-v1:0': MODEL_FEATURE.IMAGE_GEN, + 'stability.stable-image-ultra-v1:0': MODEL_FEATURE.IMAGE_GEN, + // Amazon Image Gen + 'amazon.titan-image-generator-v2:0': MODEL_FEATURE.IMAGE_GEN, + 'amazon.titan-image-generator-v1': MODEL_FEATURE.IMAGE_GEN, + 'amazon.nova-canvas-v1:0': MODEL_FEATURE.IMAGE_GEN, +}; diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json new file mode 100644 index 00000000..ccd5c10e --- /dev/null +++ b/packages/common/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "rootDir": "src", + "composite": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 04d3f6f6..4d19be6a 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -10,3 +10,4 @@ export * from './utils'; export * from './systemContext'; export * from './useCaseBuilder'; export * from './protocolUseCaseBuilder'; +export * from './model'; diff --git a/packages/types/src/message.d.ts b/packages/types/src/message.d.ts index 5ce48455..571de00e 100644 --- a/packages/types/src/message.d.ts +++ b/packages/types/src/message.d.ts @@ -58,10 +58,15 @@ export type UploadedFileType = { s3Url?: string; uploading: boolean; deleting?: boolean; + errorMessages: string[]; }; export type FileLimit = { - accept: string[]; + accept: { + doc: string[]; + image: string[]; + video: string[]; + }; maxFileCount: number; maxFileSizeMB: number; maxImageFileCount: number; diff --git a/packages/types/src/model.d.ts b/packages/types/src/model.d.ts new file mode 100644 index 00000000..a3b08e3d --- /dev/null +++ b/packages/types/src/model.d.ts @@ -0,0 +1,9 @@ +// Feature Flag の型を定義 +export type FeatureFlags = { + text?: boolean; + doc?: boolean; + image?: boolean; + video?: boolean; + image_gen?: boolean; + video_gen?: boolean; +}; diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index b3aeb4bc..40df7900 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -36,8 +36,7 @@ const ragEnabled: boolean = import.meta.env.VITE_APP_RAG_ENABLED === 'true'; const ragKnowledgeBaseEnabled: boolean = import.meta.env.VITE_APP_RAG_KNOWLEDGE_BASE_ENABLED === 'true'; const agentEnabled: boolean = import.meta.env.VITE_APP_AGENT_ENABLED === 'true'; -const { multiModalModelIds } = MODELS; -const multiModalEnabled: boolean = multiModalModelIds.length > 0; +const { visionEnabled } = MODELS; const getPromptFlows = () => { try { return JSON.parse(import.meta.env.VITE_APP_PROMPT_FLOWS); @@ -137,7 +136,7 @@ const items: ItemProps[] = [ icon: , display: 'usecase' as const, }, - multiModalEnabled + visionEnabled ? { label: '映像分析', to: '/video', diff --git a/packages/web/src/components/FileCard.tsx b/packages/web/src/components/FileCard.tsx index bc7dce30..a1b90761 100644 --- a/packages/web/src/components/FileCard.tsx +++ b/packages/web/src/components/FileCard.tsx @@ -7,7 +7,9 @@ type Props = BaseProps & { filename?: string; url?: string; loading?: boolean; + deleting?: boolean; size: 's' | 'm'; + error?: boolean; onDelete?: () => void; }; @@ -16,7 +18,11 @@ const FileCard: React.FC = (props) => {
+ className={`${ + props.error ? 'border-red-500' : 'border-aws-squid-ink/50' + } max-w-36 break-all rounded border object-cover object-center p-1 ${ + props.size === 's' ? 'max-h-24' : 'max-h-32' + }`}> {props.url ? ( {props.filename} @@ -24,7 +30,7 @@ const FileCard: React.FC = (props) => { props.filename )}
- {props.loading && ( + {(props.loading || props.deleting) && (
diff --git a/packages/web/src/components/InputChatContent.tsx b/packages/web/src/components/InputChatContent.tsx index 88db067c..fd383ccf 100644 --- a/packages/web/src/components/InputChatContent.tsx +++ b/packages/web/src/components/InputChatContent.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import ButtonSend from './ButtonSend'; import Textarea from './Textarea'; import ZoomUpImage from './ZoomUpImage'; @@ -30,6 +30,7 @@ type Props = { disableMarginBottom?: boolean; fileUpload?: boolean; fileLimit?: FileLimit; + accept?: string[]; } & ( | { hideReset?: false; @@ -46,33 +47,43 @@ const InputChatContent: React.FC = (props) => { const { uploadedFiles, uploadFiles, + checkFiles, deleteUploadedFile, uploading, errorMessages, } = useFiles(); + // Model 変更等で accept が変更された際にエラーメッセージを表示 (自動でファイル削除は行わない) + useEffect(() => { + if (props.fileLimit && props.accept) { + checkFiles(props.fileLimit, props.accept); + } + }, [checkFiles, props.fileLimit, props.accept]); + const onChangeFiles = async (e: React.ChangeEvent) => { const files = e.target.files; - if (files) { + if (files && props.fileLimit && props.accept) { // ファイルを反映しアップロード - uploadFiles(Array.from(files), props?.fileLimit); + uploadFiles(Array.from(files), props.fileLimit, props.accept); } }; const deleteFile = useCallback( (fileUrl: string) => { - deleteUploadedFile(fileUrl); + if (props.fileLimit && props.accept) { + deleteUploadedFile(fileUrl, props.fileLimit, props.accept); + } }, - [deleteUploadedFile] + [deleteUploadedFile, props.fileLimit, props.accept] ); const handlePaste = async (pasteEvent: React.ClipboardEvent) => { const fileList = pasteEvent.clipboardData.items || []; const files = Array.from(fileList) .filter((file) => file.kind === 'file') .map((file) => file.getAsFile() as File); - if (files.length > 0) { + if (files.length > 0 && props.fileLimit && props.accept) { // ファイルをアップロード - uploadFiles(Array.from(files), props.fileLimit); + uploadFiles(Array.from(files), props.fileLimit, props.accept); // ファイルの場合ファイル名もペーストされるためデフォルトの挙動を止める pasteEvent.preventDefault(); } @@ -84,8 +95,13 @@ const InputChatContent: React.FC = (props) => { }, [chatLoading, props.loading]); const disabledSend = useMemo(() => { - return props.content === '' || props.disabled || uploading; - }, [props.content, props.disabled, uploading]); + return ( + props.content === '' || + props.disabled || + uploading || + errorMessages.length > 0 + ); + }, [props.content, props.disabled, uploading, errorMessages]); return (
= (props) => { props.disableMarginBottom ? '' : 'mb-7' }`}>
- {props.fileUpload && uploadedFiles.length > 0 && ( + {uploadedFiles.length > 0 && (
{uploadedFiles.map((uploadedFile, idx) => { if (uploadedFile.type === 'image') { @@ -111,7 +127,9 @@ const InputChatContent: React.FC = (props) => { key={idx} src={uploadedFile.base64EncodedData} loading={uploadedFile.uploading} + deleting={uploadedFile.deleting} size="s" + error={uploadedFile.errorMessages.length > 0} onDelete={() => { deleteFile(uploadedFile.s3Url ?? ''); }} @@ -123,7 +141,9 @@ const InputChatContent: React.FC = (props) => { key={idx} src={uploadedFile.base64EncodedData} loading={uploadedFile.uploading} + deleting={uploadedFile.deleting} size="s" + error={uploadedFile.errorMessages.length > 0} onDelete={() => { deleteFile(uploadedFile.s3Url ?? ''); }} @@ -135,7 +155,9 @@ const InputChatContent: React.FC = (props) => { key={idx} filename={uploadedFile.name} loading={uploadedFile.uploading} + deleting={uploadedFile.deleting} size="s" + error={uploadedFile.errorMessages.length > 0} onDelete={() => { deleteFile(uploadedFile.s3Url ?? ''); }} @@ -172,7 +194,7 @@ const InputChatContent: React.FC = (props) => { hidden onChange={onChangeFiles} type="file" - accept={props.fileLimit?.accept?.join(',')} + accept={props.accept?.join(',')} multiple value={[]} /> diff --git a/packages/web/src/components/ZoomUpImage.tsx b/packages/web/src/components/ZoomUpImage.tsx index bcace9ff..6c7f3d33 100644 --- a/packages/web/src/components/ZoomUpImage.tsx +++ b/packages/web/src/components/ZoomUpImage.tsx @@ -6,7 +6,9 @@ import { PiSpinnerGap, PiX } from 'react-icons/pi'; type Props = BaseProps & { src?: string; loading?: boolean; + deleting?: boolean; size: 's' | 'm'; + error?: boolean; onDelete?: () => void; }; @@ -17,13 +19,17 @@ const ZoomUpImage: React.FC = (props) => {
{ setZoom(true); }} /> - {props.loading && ( + {(props.loading || props.deleting) && (
diff --git a/packages/web/src/components/ZoomUpVideo.tsx b/packages/web/src/components/ZoomUpVideo.tsx index 455c3f7e..3113180d 100644 --- a/packages/web/src/components/ZoomUpVideo.tsx +++ b/packages/web/src/components/ZoomUpVideo.tsx @@ -6,7 +6,9 @@ import { PiSpinnerGap, PiX } from 'react-icons/pi'; type Props = BaseProps & { src?: string; loading?: boolean; + deleting?: boolean; size: 's' | 'm'; + error?: boolean; onDelete?: () => void; }; @@ -17,14 +19,18 @@ const ZoomUpVideo: React.FC = (props) => {
diff --git a/packages/web/src/pages/ChatPage.tsx b/packages/web/src/pages/ChatPage.tsx index 59b4e5f1..c613438d 100644 --- a/packages/web/src/pages/ChatPage.tsx +++ b/packages/web/src/pages/ChatPage.tsx @@ -27,26 +27,22 @@ import useFiles from '../hooks/useFiles'; import { FileLimit, SystemContext } from 'generative-ai-use-cases-jp'; const fileLimit: FileLimit = { - accept: [ - '.csv', - '.doc', - '.docx', - '.html', - '.md', - '.pdf', - '.txt', - '.xls', - '.xlsx', - '.gif', - '.jpg', - '.jpeg', - '.png', - '.webp', - '.mkv', - '.mov', - '.mp4', - '.webm', - ], + 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, @@ -161,9 +157,18 @@ const ChatPage: React.FC = () => { } }, [chatId, getChatTitle]); - const fileUpload = useMemo(() => { - return MODELS.multiModalModelIds.includes(modelId); + 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]); + const fileUpload = useMemo(() => { + return accept.length > 0; + }, [accept]); useEffect(() => { const _modelId = !modelId ? availableModels[0] : modelId; @@ -354,7 +359,7 @@ const ChatPage: React.FC = () => { setIsOver(false); if (event.dataTransfer.files) { // ファイルを反映しアップロード - uploadFiles(Array.from(event.dataTransfer.files), fileLimit); + uploadFiles(Array.from(event.dataTransfer.files), fileLimit, accept); } }; @@ -498,6 +503,7 @@ const ChatPage: React.FC = () => { onReset={onReset} fileUpload={fileUpload} fileLimit={fileLimit} + accept={accept} />
diff --git a/packages/web/src/pages/LandingPage.tsx b/packages/web/src/pages/LandingPage.tsx index 80e37217..29b0b4ad 100644 --- a/packages/web/src/pages/LandingPage.tsx +++ b/packages/web/src/pages/LandingPage.tsx @@ -39,8 +39,7 @@ const ragEnabled: boolean = import.meta.env.VITE_APP_RAG_ENABLED === 'true'; const ragKnowledgeBaseEnabled: boolean = import.meta.env.VITE_APP_RAG_KNOWLEDGE_BASE_ENABLED === 'true'; const agentEnabled: boolean = import.meta.env.VITE_APP_AGENT_ENABLED === 'true'; -const { multiModalModelIds } = MODELS; -const multiModalEnabled: boolean = multiModalModelIds.length > 0; +const { visionEnabled } = MODELS; const getPromptFlows = () => { try { return JSON.parse(import.meta.env.VITE_APP_PROMPT_FLOWS); @@ -360,7 +359,7 @@ const LandingPage: React.FC = () => { icon={} description="画像生成 AI は、テキストや画像を元に新しい画像を生成できます。アイデアを即座に可視化することができ、デザイン作業などの効率化を期待できます。こちらの機能では、プロンプトの作成を LLM に支援してもらうことができます。" /> - {multiModalEnabled && ( + {visionEnabled && ( { clear: clearChat, } = useChat(pathname); const { setTypingTextInput, typingTextOutput } = useTyping(loading); - const { modelIds: availableModels } = MODELS; - const availableMultiModalModels = useMemo(() => { - return availableModels.filter((modelId) => - MODELS.multiModalModelIds.includes(modelId) - ); - }, [availableModels]); + const { visionModelIds } = MODELS; const modelId = getModelId(); const prompter = useMemo(() => { return getPrompter(modelId); }, [modelId]); useEffect(() => { - const _modelId = !modelId ? availableMultiModalModels[0] : modelId; + const _modelId = !modelId ? visionModelIds[0] : modelId; if (search !== '') { const params = queryString.parse(search) as VideoAnalyzerPageQueryParams; setContent(params.content); setModelId( - availableMultiModalModels.includes(params.modelId ?? '') + visionModelIds.includes(params.modelId ?? '') ? params.modelId! : _modelId ); @@ -101,7 +96,7 @@ const VideoAnalyzerPage: React.FC = () => { setModelId(_modelId); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setContent, modelId, availableMultiModalModels, search]); + }, [setContent, modelId, visionModelIds, search]); useEffect(() => { setTypingTextInput(analysis); @@ -183,6 +178,7 @@ const VideoAnalyzerPage: React.FC = () => { s3Url: baseUrl, base64EncodedData: imageBase64, uploading: false, + errorMessages: [], }, ]; @@ -293,7 +289,7 @@ const VideoAnalyzerPage: React.FC = () => {