diff --git a/APIKeyManager.tsx b/APIKeyManager.tsx new file mode 100644 index 0000000000000000000000000000000000000000..92263363fac08188fe40a3e4f4aca722c05d4b9f --- /dev/null +++ b/APIKeyManager.tsx @@ -0,0 +1,169 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { IconButton } from '~/components/ui/IconButton'; +import type { ProviderInfo } from '~/types/model'; +import Cookies from 'js-cookie'; + +interface APIKeyManagerProps { + provider: ProviderInfo; + apiKey: string; + setApiKey: (key: string) => void; + getApiKeyLink?: string; + labelForGetApiKey?: string; +} + +// cache which stores whether the provider's API key is set via environment variable +const providerEnvKeyStatusCache: Record = {}; + +const apiKeyMemoizeCache: { [k: string]: Record } = {}; + +export function getApiKeysFromCookies() { + const storedApiKeys = Cookies.get('apiKeys'); + let parsedKeys: Record = {}; + + if (storedApiKeys) { + parsedKeys = apiKeyMemoizeCache[storedApiKeys]; + + if (!parsedKeys) { + parsedKeys = apiKeyMemoizeCache[storedApiKeys] = JSON.parse(storedApiKeys); + } + } + + return parsedKeys; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const APIKeyManager: React.FC = ({ provider, apiKey, setApiKey }) => { + const [isEditing, setIsEditing] = useState(false); + const [tempKey, setTempKey] = useState(apiKey); + const [isEnvKeySet, setIsEnvKeySet] = useState(false); + + // Reset states and load saved key when provider changes + useEffect(() => { + // Load saved API key from cookies for this provider + const savedKeys = getApiKeysFromCookies(); + const savedKey = savedKeys[provider.name] || ''; + + setTempKey(savedKey); + setApiKey(savedKey); + setIsEditing(false); + }, [provider.name]); + + const checkEnvApiKey = useCallback(async () => { + // Check cache first + if (providerEnvKeyStatusCache[provider.name] !== undefined) { + setIsEnvKeySet(providerEnvKeyStatusCache[provider.name]); + return; + } + + try { + const response = await fetch(`/api/check-env-key?provider=${encodeURIComponent(provider.name)}`); + const data = await response.json(); + const isSet = (data as { isSet: boolean }).isSet; + + // Cache the result + providerEnvKeyStatusCache[provider.name] = isSet; + setIsEnvKeySet(isSet); + } catch (error) { + console.error('Failed to check environment API key:', error); + setIsEnvKeySet(false); + } + }, [provider.name]); + + useEffect(() => { + checkEnvApiKey(); + }, [checkEnvApiKey]); + + const handleSave = () => { + // Save to parent state + setApiKey(tempKey); + + // Save to cookies + const currentKeys = getApiKeysFromCookies(); + const newKeys = { ...currentKeys, [provider.name]: tempKey }; + Cookies.set('apiKeys', JSON.stringify(newKeys)); + + setIsEditing(false); + }; + + return ( +
+
+
+ {provider?.name} API Key: + {!isEditing && ( +
+ {apiKey ? ( + <> +
+ Set via UI + + ) : isEnvKeySet ? ( + <> +
+ Set via environment variable + + ) : ( + <> +
+ Not Set (Please set via UI or ENV_VAR) + + )} +
+ )} +
+
+ +
+ {isEditing ? ( +
+ setTempKey(e.target.value)} + className="w-[300px] px-3 py-1.5 text-sm rounded border border-bolt-elements-borderColor + bg-bolt-elements-prompt-background text-bolt-elements-textPrimary + focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus" + /> + +
+ + setIsEditing(false)} + title="Cancel" + className="bg-red-500/10 hover:bg-red-500/20 text-red-500" + > +
+ +
+ ) : ( + <> + { + setIsEditing(true)} + title="Edit API Key" + className="bg-blue-500/10 hover:bg-blue-500/20 text-blue-500" + > +
+ + } + {provider?.getApiKeyLink && !apiKey && ( + window.open(provider?.getApiKeyLink)} + title="Get API Key" + className="bg-purple-500/10 hover:bg-purple-500/20 text-purple-500 flex items-center gap-2" + > + {provider?.labelForGetApiKey || 'Get API Key'} +
+ + )} + + )} +
+
+ ); +}; diff --git a/Artifact.tsx b/Artifact.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5f0c99106df922b915de3bf5a64d49d94c2a0864 --- /dev/null +++ b/Artifact.tsx @@ -0,0 +1,263 @@ +import { useStore } from '@nanostores/react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { computed } from 'nanostores'; +import { memo, useEffect, useRef, useState } from 'react'; +import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki'; +import type { ActionState } from '~/lib/runtime/action-runner'; +import { workbenchStore } from '~/lib/stores/workbench'; +import { classNames } from '~/utils/classNames'; +import { cubicEasingFn } from '~/utils/easings'; +import { WORK_DIR } from '~/utils/constants'; + +const highlighterOptions = { + langs: ['shell'], + themes: ['light-plus', 'dark-plus'], +}; + +const shellHighlighter: HighlighterGeneric = + import.meta.hot?.data.shellHighlighter ?? (await createHighlighter(highlighterOptions)); + +if (import.meta.hot) { + import.meta.hot.data.shellHighlighter = shellHighlighter; +} + +interface ArtifactProps { + messageId: string; +} + +export const Artifact = memo(({ messageId }: ArtifactProps) => { + const userToggledActions = useRef(false); + const [showActions, setShowActions] = useState(false); + const [allActionFinished, setAllActionFinished] = useState(false); + + const artifacts = useStore(workbenchStore.artifacts); + const artifact = artifacts[messageId]; + + const actions = useStore( + computed(artifact.runner.actions, (actions) => { + return Object.values(actions); + }), + ); + + const toggleActions = () => { + userToggledActions.current = true; + setShowActions(!showActions); + }; + + useEffect(() => { + if (actions.length && !showActions && !userToggledActions.current) { + setShowActions(true); + } + + if (actions.length !== 0 && artifact.type === 'bundled') { + const finished = !actions.find((action) => action.status !== 'complete'); + + if (allActionFinished !== finished) { + setAllActionFinished(finished); + } + } + }, [actions]); + + return ( +
+
+ +
+ + {actions.length && artifact.type !== 'bundled' && ( + +
+
+
+
+ )} +
+
+ + {artifact.type !== 'bundled' && showActions && actions.length > 0 && ( + +
+ +
+ +
+ + )} + +
+ ); +}); + +interface ShellCodeBlockProps { + classsName?: string; + code: string; +} + +function ShellCodeBlock({ classsName, code }: ShellCodeBlockProps) { + return ( +
+ ); +} + +interface ActionListProps { + actions: ActionState[]; +} + +const actionVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0 }, +}; + +function openArtifactInWorkbench(filePath: any) { + if (workbenchStore.currentView.get() !== 'code') { + workbenchStore.currentView.set('code'); + } + + workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`); +} + +const ActionList = memo(({ actions }: ActionListProps) => { + return ( + +
    + {actions.map((action, index) => { + const { status, type, content } = action; + const isLast = index === actions.length - 1; + + return ( + +
    +
    + {status === 'running' ? ( + <> + {type !== 'start' ? ( +
    + ) : ( +
    + )} + + ) : status === 'pending' ? ( +
    + ) : status === 'complete' ? ( +
    + ) : status === 'failed' || status === 'aborted' ? ( +
    + ) : null} +
    + {type === 'file' ? ( +
    + Create{' '} + openArtifactInWorkbench(action.filePath)} + > + {action.filePath} + +
    + ) : type === 'shell' ? ( +
    + Run command +
    + ) : type === 'start' ? ( + { + e.preventDefault(); + workbenchStore.currentView.set('preview'); + }} + className="flex items-center w-full min-h-[28px]" + > + Start Application + + ) : null} +
    + {(type === 'shell' || type === 'start') && ( + + )} +
    + ); + })} +
+
+ ); +}); + +function getIconColor(status: ActionState['status']) { + switch (status) { + case 'pending': { + return 'text-bolt-elements-textTertiary'; + } + case 'running': { + return 'text-bolt-elements-loader-progress'; + } + case 'complete': { + return 'text-bolt-elements-icon-success'; + } + case 'aborted': { + return 'text-bolt-elements-textSecondary'; + } + case 'failed': { + return 'text-bolt-elements-icon-error'; + } + default: { + return undefined; + } + } +} diff --git a/AssistantMessage.tsx b/AssistantMessage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1e3ed2d98b5b4e5afe525bf4f99979993f93011b --- /dev/null +++ b/AssistantMessage.tsx @@ -0,0 +1,113 @@ +import { memo } from 'react'; +import { Markdown } from './Markdown'; +import type { JSONValue } from 'ai'; +import Popover from '~/components/ui/Popover'; +import { workbenchStore } from '~/lib/stores/workbench'; +import { WORK_DIR } from '~/utils/constants'; + +interface AssistantMessageProps { + content: string; + annotations?: JSONValue[]; +} + +function openArtifactInWorkbench(filePath: string) { + filePath = normalizedFilePath(filePath); + + if (workbenchStore.currentView.get() !== 'code') { + workbenchStore.currentView.set('code'); + } + + workbenchStore.setSelectedFile(`${WORK_DIR}/${filePath}`); +} + +function normalizedFilePath(path: string) { + let normalizedPath = path; + + if (normalizedPath.startsWith(WORK_DIR)) { + normalizedPath = path.replace(WORK_DIR, ''); + } + + if (normalizedPath.startsWith('/')) { + normalizedPath = normalizedPath.slice(1); + } + + return normalizedPath; +} + +export const AssistantMessage = memo(({ content, annotations }: AssistantMessageProps) => { + const filteredAnnotations = (annotations?.filter( + (annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'), + ) || []) as { type: string; value: any } & { [key: string]: any }[]; + + let chatSummary: string | undefined = undefined; + + if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) { + chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary; + } + + let codeContext: string[] | undefined = undefined; + + if (filteredAnnotations.find((annotation) => annotation.type === 'codeContext')) { + codeContext = filteredAnnotations.find((annotation) => annotation.type === 'codeContext')?.files; + } + + const usage: { + completionTokens: number; + promptTokens: number; + totalTokens: number; + } = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value; + + return ( +
+ <> +
+ {(codeContext || chatSummary) && ( + }> + {chatSummary && ( +
+
+

Summary

+
+ {chatSummary} +
+
+ {codeContext && ( +
+

Context

+
+ {codeContext.map((x) => { + const normalized = normalizedFilePath(x); + return ( + <> + { + e.preventDefault(); + e.stopPropagation(); + openArtifactInWorkbench(normalized); + }} + > + {normalized} + + + ); + })} +
+
+ )} +
+ )} +
+
+ )} + {usage && ( +
+ Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) +
+ )} +
+ + {content} +
+ ); +}); diff --git a/AvatarDropdown.tsx b/AvatarDropdown.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6adfd31d3cb51f850d981d5efe8110cc0d0f223b --- /dev/null +++ b/AvatarDropdown.tsx @@ -0,0 +1,158 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { motion } from 'framer-motion'; +import { useStore } from '@nanostores/react'; +import { classNames } from '~/utils/classNames'; +import { profileStore } from '~/lib/stores/profile'; +import type { TabType, Profile } from './types'; + +const BetaLabel = () => ( + + BETA + +); + +interface AvatarDropdownProps { + onSelectTab: (tab: TabType) => void; +} + +export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => { + const profile = useStore(profileStore) as Profile; + + return ( + + + + {profile?.avatar ? ( + {profile?.username + ) : ( +
+
+
+ )} + + + + + +
+
+ {profile?.avatar ? ( + {profile?.username + ) : ( +
+ ? +
+ )} +
+
+
+ {profile?.username || 'Guest User'} +
+ {profile?.bio &&
{profile.bio}
} +
+
+ + onSelectTab('profile')} + > +
+ Edit Profile + + + onSelectTab('settings')} + > +
+ Settings + + +
+ + onSelectTab('task-manager')} + > +
+ Task Manager + + + + onSelectTab('service-status')} + > +
+ Service Status + + + + + + ); +}; diff --git a/BaseChat.module.scss b/BaseChat.module.scss new file mode 100644 index 0000000000000000000000000000000000000000..4908e34e05d90eb08c7da48946631d5a68ee7e2e --- /dev/null +++ b/BaseChat.module.scss @@ -0,0 +1,47 @@ +.BaseChat { + &[data-chat-visible='false'] { + --workbench-inner-width: 100%; + --workbench-left: 0; + + .Chat { + --at-apply: bolt-ease-cubic-bezier; + transition-property: transform, opacity; + transition-duration: 0.3s; + will-change: transform, opacity; + transform: translateX(-50%); + opacity: 0; + } + } +} + +.Chat { + opacity: 1; +} + +.PromptEffectContainer { + --prompt-container-offset: 50px; + --prompt-line-stroke-width: 1px; + position: absolute; + pointer-events: none; + inset: calc(var(--prompt-container-offset) / -2); + width: calc(100% + var(--prompt-container-offset)); + height: calc(100% + var(--prompt-container-offset)); +} + +.PromptEffectLine { + width: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width)); + height: calc(100% - var(--prompt-container-offset) + var(--prompt-line-stroke-width)); + x: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2); + y: calc(var(--prompt-container-offset) / 2 - var(--prompt-line-stroke-width) / 2); + rx: calc(8px - var(--prompt-line-stroke-width)); + fill: transparent; + stroke-width: var(--prompt-line-stroke-width); + stroke: url(#line-gradient); + stroke-dasharray: 35px 65px; + stroke-dashoffset: 10; +} + +.PromptShine { + fill: url(#shine-gradient); + mix-blend-mode: overlay; +} diff --git a/BaseChat.tsx b/BaseChat.tsx new file mode 100644 index 0000000000000000000000000000000000000000..12929b10fb420f35bf12ae39ba3797757abd2463 --- /dev/null +++ b/BaseChat.tsx @@ -0,0 +1,630 @@ +/* + * @ts-nocheck + * Preventing TS checks with files presented in the video for a better presentation. + */ +import type { JSONValue, Message } from 'ai'; +import React, { type RefCallback, useEffect, useState } from 'react'; +import { ClientOnly } from 'remix-utils/client-only'; +import { Menu } from '~/components/sidebar/Menu.client'; +import { IconButton } from '~/components/ui/IconButton'; +import { Workbench } from '~/components/workbench/Workbench.client'; +import { classNames } from '~/utils/classNames'; +import { PROVIDER_LIST } from '~/utils/constants'; +import { Messages } from './Messages.client'; +import { SendButton } from './SendButton.client'; +import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager'; +import Cookies from 'js-cookie'; +import * as Tooltip from '@radix-ui/react-tooltip'; + +import styles from './BaseChat.module.scss'; +import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton'; +import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons'; +import { ExamplePrompts } from '~/components/chat/ExamplePrompts'; +import GitCloneButton from './GitCloneButton'; + +import FilePreview from './FilePreview'; +import { ModelSelector } from '~/components/chat/ModelSelector'; +import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition'; +import type { ProviderInfo } from '~/types/model'; +import { ScreenshotStateManager } from './ScreenshotStateManager'; +import { toast } from 'react-toastify'; +import StarterTemplates from './StarterTemplates'; +import type { ActionAlert } from '~/types/actions'; +import ChatAlert from './ChatAlert'; +import type { ModelInfo } from '~/lib/modules/llm/types'; +import ProgressCompilation from './ProgressCompilation'; +import type { ProgressAnnotation } from '~/types/context'; +import type { ActionRunner } from '~/lib/runtime/action-runner'; +import { LOCAL_PROVIDERS } from '~/lib/stores/settings'; + +const TEXTAREA_MIN_HEIGHT = 76; + +interface BaseChatProps { + textareaRef?: React.RefObject | undefined; + messageRef?: RefCallback | undefined; + scrollRef?: RefCallback | undefined; + showChat?: boolean; + chatStarted?: boolean; + isStreaming?: boolean; + onStreamingChange?: (streaming: boolean) => void; + messages?: Message[]; + description?: string; + enhancingPrompt?: boolean; + promptEnhanced?: boolean; + input?: string; + model?: string; + setModel?: (model: string) => void; + provider?: ProviderInfo; + setProvider?: (provider: ProviderInfo) => void; + providerList?: ProviderInfo[]; + handleStop?: () => void; + sendMessage?: (event: React.UIEvent, messageInput?: string) => void; + handleInputChange?: (event: React.ChangeEvent) => void; + enhancePrompt?: () => void; + importChat?: (description: string, messages: Message[]) => Promise; + exportChat?: () => void; + uploadedFiles?: File[]; + setUploadedFiles?: (files: File[]) => void; + imageDataList?: string[]; + setImageDataList?: (dataList: string[]) => void; + actionAlert?: ActionAlert; + clearAlert?: () => void; + data?: JSONValue[] | undefined; + actionRunner?: ActionRunner; +} + +export const BaseChat = React.forwardRef( + ( + { + textareaRef, + messageRef, + scrollRef, + showChat = true, + chatStarted = false, + isStreaming = false, + onStreamingChange, + model, + setModel, + provider, + setProvider, + providerList, + input = '', + enhancingPrompt, + handleInputChange, + + // promptEnhanced, + enhancePrompt, + sendMessage, + handleStop, + importChat, + exportChat, + uploadedFiles = [], + setUploadedFiles, + imageDataList = [], + setImageDataList, + messages, + actionAlert, + clearAlert, + data, + actionRunner, + }, + ref, + ) => { + const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; + const [apiKeys, setApiKeys] = useState>(getApiKeysFromCookies()); + const [modelList, setModelList] = useState([]); + const [isModelSettingsCollapsed, setIsModelSettingsCollapsed] = useState(false); + const [isListening, setIsListening] = useState(false); + const [recognition, setRecognition] = useState(null); + const [transcript, setTranscript] = useState(''); + const [isModelLoading, setIsModelLoading] = useState('all'); + const [progressAnnotations, setProgressAnnotations] = useState([]); + useEffect(() => { + if (data) { + const progressList = data.filter( + (x) => typeof x === 'object' && (x as any).type === 'progress', + ) as ProgressAnnotation[]; + setProgressAnnotations(progressList); + } + }, [data]); + useEffect(() => { + console.log(transcript); + }, [transcript]); + + useEffect(() => { + onStreamingChange?.(isStreaming); + }, [isStreaming, onStreamingChange]); + + useEffect(() => { + if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + recognition.continuous = true; + recognition.interimResults = true; + + recognition.onresult = (event) => { + const transcript = Array.from(event.results) + .map((result) => result[0]) + .map((result) => result.transcript) + .join(''); + + setTranscript(transcript); + + if (handleInputChange) { + const syntheticEvent = { + target: { value: transcript }, + } as React.ChangeEvent; + handleInputChange(syntheticEvent); + } + }; + + recognition.onerror = (event) => { + console.error('Speech recognition error:', event.error); + setIsListening(false); + }; + + setRecognition(recognition); + } + }, []); + + useEffect(() => { + if (typeof window !== 'undefined') { + let parsedApiKeys: Record | undefined = {}; + + try { + parsedApiKeys = getApiKeysFromCookies(); + setApiKeys(parsedApiKeys); + } catch (error) { + console.error('Error loading API keys from cookies:', error); + Cookies.remove('apiKeys'); + } + + setIsModelLoading('all'); + fetch('/api/models') + .then((response) => response.json()) + .then((data) => { + const typedData = data as { modelList: ModelInfo[] }; + setModelList(typedData.modelList); + }) + .catch((error) => { + console.error('Error fetching model list:', error); + }) + .finally(() => { + setIsModelLoading(undefined); + }); + } + }, [providerList, provider]); + + const onApiKeysChange = async (providerName: string, apiKey: string) => { + const newApiKeys = { ...apiKeys, [providerName]: apiKey }; + setApiKeys(newApiKeys); + Cookies.set('apiKeys', JSON.stringify(newApiKeys)); + + setIsModelLoading(providerName); + + let providerModels: ModelInfo[] = []; + + try { + const response = await fetch(`/api/models/${encodeURIComponent(providerName)}`); + const data = await response.json(); + providerModels = (data as { modelList: ModelInfo[] }).modelList; + } catch (error) { + console.error('Error loading dynamic models for:', providerName, error); + } + + // Only update models for the specific provider + setModelList((prevModels) => { + const otherModels = prevModels.filter((model) => model.provider !== providerName); + return [...otherModels, ...providerModels]; + }); + setIsModelLoading(undefined); + }; + + const startListening = () => { + if (recognition) { + recognition.start(); + setIsListening(true); + } + }; + + const stopListening = () => { + if (recognition) { + recognition.stop(); + setIsListening(false); + } + }; + + const handleSendMessage = (event: React.UIEvent, messageInput?: string) => { + if (sendMessage) { + sendMessage(event, messageInput); + + if (recognition) { + recognition.abort(); // Stop current recognition + setTranscript(''); // Clear transcript + setIsListening(false); + + // Clear the input by triggering handleInputChange with empty value + if (handleInputChange) { + const syntheticEvent = { + target: { value: '' }, + } as React.ChangeEvent; + handleInputChange(syntheticEvent); + } + } + } + }; + + const handleFileUpload = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + + if (file) { + const reader = new FileReader(); + + reader.onload = (e) => { + const base64Image = e.target?.result as string; + setUploadedFiles?.([...uploadedFiles, file]); + setImageDataList?.([...imageDataList, base64Image]); + }; + reader.readAsDataURL(file); + } + }; + + input.click(); + }; + + const handlePaste = async (e: React.ClipboardEvent) => { + const items = e.clipboardData?.items; + + if (!items) { + return; + } + + for (const item of items) { + if (item.type.startsWith('image/')) { + e.preventDefault(); + + const file = item.getAsFile(); + + if (file) { + const reader = new FileReader(); + + reader.onload = (e) => { + const base64Image = e.target?.result as string; + setUploadedFiles?.([...uploadedFiles, file]); + setImageDataList?.([...imageDataList, base64Image]); + }; + reader.readAsDataURL(file); + } + + break; + } + } + }; + + const baseChat = ( +
+ {() => } +
+
+ {!chatStarted && ( +
+

+ Where ideas begin +

+

+ Bring ideas to life in seconds or get help on existing projects. +

+
+ )} +
+ + {() => { + return chatStarted ? ( + + ) : null; + }} + +
+
+ {actionAlert && ( + clearAlert?.()} + postMessage={(message) => { + sendMessage?.({} as any, message); + clearAlert?.(); + }} + /> + )} +
+ {progressAnnotations && } +
+ + + + + + + + + + + + + + + + + + +
+ + {() => ( +
+ + {(providerList || []).length > 0 && provider && !LOCAL_PROVIDERS.includes(provider.name) && ( + { + onApiKeysChange(provider.name, key); + }} + /> + )} +
+ )} +
+
+ { + setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index)); + setImageDataList?.(imageDataList.filter((_, i) => i !== index)); + }} + /> + + {() => ( + + )} + +
+