### What problem does this PR solve? Feat: Add the SelectWithSearch component #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| @@ -7,7 +7,7 @@ import { | |||
| } from '@/components/ui/form'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { NextLLMSelect } from './llm-select'; | |||
| import { NextLLMSelect } from './llm-select/next'; | |||
| export function LargeModelFormField() { | |||
| const form = useFormContext(); | |||
| @@ -1,12 +1,7 @@ | |||
| import { LlmModelType } from '@/constants/knowledge'; | |||
| import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks'; | |||
| import * as SelectPrimitive from '@radix-ui/react-select'; | |||
| import { Popover as AntPopover, Select as AntSelect } from 'antd'; | |||
| import { forwardRef, useState } from 'react'; | |||
| import LlmSettingItems from '../llm-setting-items'; | |||
| import { LlmSettingFieldItems } from '../llm-setting-items/next'; | |||
| import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; | |||
| import { Select, SelectTrigger, SelectValue } from '../ui/select'; | |||
| interface IProps { | |||
| id?: string; | |||
| @@ -16,7 +11,13 @@ interface IProps { | |||
| disabled?: boolean; | |||
| } | |||
| const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) => { | |||
| const LLMSelect = ({ | |||
| id, | |||
| value, | |||
| onInitialValue, | |||
| onChange, | |||
| disabled, | |||
| }: IProps) => { | |||
| const modelOptions = useComposeLlmOptionsByModelTypes([ | |||
| LlmModelType.Chat, | |||
| LlmModelType.Image2text, | |||
| @@ -31,11 +32,12 @@ const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) => | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| const content = ( | |||
| <div style={{ width: 400 }}> | |||
| <LlmSettingItems onChange={onChange} | |||
| <LlmSettingItems | |||
| onChange={onChange} | |||
| formItemLayout={{ labelCol: { span: 10 }, wrapperCol: { span: 14 } }} | |||
| ></LlmSettingItems> | |||
| </div> | |||
| @@ -63,43 +65,3 @@ const LLMSelect = ({ id, value, onInitialValue, onChange, disabled }: IProps) => | |||
| }; | |||
| export default LLMSelect; | |||
| export const NextLLMSelect = forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.Trigger>, | |||
| IProps | |||
| >(({ value, disabled }, ref) => { | |||
| const [isPopoverOpen, setIsPopoverOpen] = useState(false); | |||
| const modelOptions = useComposeLlmOptionsByModelTypes([ | |||
| LlmModelType.Chat, | |||
| LlmModelType.Image2text, | |||
| ]); | |||
| return ( | |||
| <Select disabled={disabled} value={value}> | |||
| <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> | |||
| <PopoverTrigger asChild> | |||
| <SelectTrigger | |||
| onClick={(e) => { | |||
| e.preventDefault(); | |||
| setIsPopoverOpen(true); | |||
| }} | |||
| ref={ref} | |||
| > | |||
| <SelectValue> | |||
| { | |||
| modelOptions | |||
| .flatMap((x) => x.options) | |||
| .find((x) => x.value === value)?.label | |||
| } | |||
| </SelectValue> | |||
| </SelectTrigger> | |||
| </PopoverTrigger> | |||
| <PopoverContent side={'left'}> | |||
| <LlmSettingFieldItems></LlmSettingFieldItems> | |||
| </PopoverContent> | |||
| </Popover> | |||
| </Select> | |||
| ); | |||
| }); | |||
| NextLLMSelect.displayName = 'LLMSelect'; | |||
| @@ -0,0 +1,55 @@ | |||
| import { LlmModelType } from '@/constants/knowledge'; | |||
| import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks'; | |||
| import * as SelectPrimitive from '@radix-ui/react-select'; | |||
| import { forwardRef, useState } from 'react'; | |||
| import { LlmSettingFieldItems } from '../llm-setting-items/next'; | |||
| import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; | |||
| import { Select, SelectTrigger, SelectValue } from '../ui/select'; | |||
| interface IProps { | |||
| id?: string; | |||
| value?: string; | |||
| onInitialValue?: (value: string, option: any) => void; | |||
| onChange?: (value: string, option: any) => void; | |||
| disabled?: boolean; | |||
| } | |||
| export const NextLLMSelect = forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.Trigger>, | |||
| IProps | |||
| >(({ value, disabled }, ref) => { | |||
| const [isPopoverOpen, setIsPopoverOpen] = useState(false); | |||
| const modelOptions = useComposeLlmOptionsByModelTypes([ | |||
| LlmModelType.Chat, | |||
| LlmModelType.Image2text, | |||
| ]); | |||
| return ( | |||
| <Select disabled={disabled} value={value}> | |||
| <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> | |||
| <PopoverTrigger asChild> | |||
| <SelectTrigger | |||
| onClick={(e) => { | |||
| e.preventDefault(); | |||
| setIsPopoverOpen(true); | |||
| }} | |||
| ref={ref} | |||
| > | |||
| <SelectValue> | |||
| { | |||
| modelOptions | |||
| .flatMap((x) => x.options) | |||
| .find((x) => x.value === value)?.label | |||
| } | |||
| </SelectValue> | |||
| </SelectTrigger> | |||
| </PopoverTrigger> | |||
| <PopoverContent side={'left'}> | |||
| <LlmSettingFieldItems></LlmSettingFieldItems> | |||
| </PopoverContent> | |||
| </Popover> | |||
| </Select> | |||
| ); | |||
| }); | |||
| NextLLMSelect.displayName = 'LLMSelect'; | |||
| @@ -4,6 +4,7 @@ import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks'; | |||
| import { camelCase } from 'lodash'; | |||
| import { useCallback } from 'react'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { | |||
| FormControl, | |||
| FormField, | |||
| @@ -11,7 +12,6 @@ import { | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '../ui/form'; | |||
| import { Input } from '../ui/input'; | |||
| import { | |||
| Select, | |||
| SelectContent, | |||
| @@ -21,86 +21,20 @@ import { | |||
| SelectTrigger, | |||
| SelectValue, | |||
| } from '../ui/select'; | |||
| import { FormSlider } from '../ui/slider'; | |||
| import { Switch } from '../ui/switch'; | |||
| interface SliderWithInputNumberFormFieldProps { | |||
| name: string; | |||
| label: string; | |||
| checkName: string; | |||
| max: number; | |||
| min?: number; | |||
| step?: number; | |||
| } | |||
| function SliderWithInputNumberFormField({ | |||
| name, | |||
| label, | |||
| checkName, | |||
| max, | |||
| min = 0, | |||
| step = 1, | |||
| }: SliderWithInputNumberFormFieldProps) { | |||
| const { control, watch } = useFormContext(); | |||
| const { t } = useTranslate('chat'); | |||
| const disabled = !watch(checkName); | |||
| return ( | |||
| <FormField | |||
| control={control} | |||
| name={name} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <div className="flex items-center justify-between"> | |||
| <FormLabel>{t(label)}</FormLabel> | |||
| <FormField | |||
| control={control} | |||
| name={checkName} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormControl> | |||
| <Switch | |||
| {...field} | |||
| checked={field.value} | |||
| onCheckedChange={field.onChange} | |||
| ></Switch> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </div> | |||
| <FormControl> | |||
| <div className="flex w-full items-center space-x-2"> | |||
| <FormSlider | |||
| {...field} | |||
| disabled={disabled} | |||
| max={max} | |||
| min={min} | |||
| step={step} | |||
| ></FormSlider> | |||
| <Input | |||
| type={'number'} | |||
| className="w-2/5" | |||
| {...field} | |||
| disabled={disabled} | |||
| max={max} | |||
| min={min} | |||
| step={step} | |||
| /> | |||
| </div> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| ); | |||
| } | |||
| import { SliderInputSwitchFormField } from './slider'; | |||
| interface LlmSettingFieldItemsProps { | |||
| prefix?: string; | |||
| } | |||
| export const LlmSettingSchema = { | |||
| llm_id: z.string(), | |||
| temperature: z.coerce.number(), | |||
| top_p: z.string(), | |||
| presence_penalty: z.coerce.number(), | |||
| frequency_penalty: z.coerce.number(), | |||
| }; | |||
| export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { | |||
| const form = useFormContext(); | |||
| const { t } = useTranslate('chat'); | |||
| @@ -122,7 +56,7 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { | |||
| ); | |||
| return ( | |||
| <div className="space-y-8"> | |||
| <div className="space-y-5"> | |||
| <FormField | |||
| control={form.control} | |||
| name={'llm_id'} | |||
| @@ -180,40 +114,40 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <SliderWithInputNumberFormField | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('temperature')} | |||
| checkName="temperatureEnabled" | |||
| label="temperature" | |||
| max={1} | |||
| step={0.01} | |||
| ></SliderWithInputNumberFormField> | |||
| <SliderWithInputNumberFormField | |||
| ></SliderInputSwitchFormField> | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('top_p')} | |||
| checkName="topPEnabled" | |||
| label="topP" | |||
| max={1} | |||
| step={0.01} | |||
| ></SliderWithInputNumberFormField> | |||
| <SliderWithInputNumberFormField | |||
| ></SliderInputSwitchFormField> | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('presence_penalty')} | |||
| checkName="presencePenaltyEnabled" | |||
| label="presencePenalty" | |||
| max={1} | |||
| step={0.01} | |||
| ></SliderWithInputNumberFormField> | |||
| <SliderWithInputNumberFormField | |||
| ></SliderInputSwitchFormField> | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('frequency_penalty')} | |||
| checkName="frequencyPenaltyEnabled" | |||
| label="frequencyPenalty" | |||
| max={1} | |||
| step={0.01} | |||
| ></SliderWithInputNumberFormField> | |||
| <SliderWithInputNumberFormField | |||
| ></SliderInputSwitchFormField> | |||
| <SliderInputSwitchFormField | |||
| name={getFieldWithPrefix('max_tokens')} | |||
| checkName="maxTokensEnabled" | |||
| label="maxTokens" | |||
| max={128000} | |||
| ></SliderWithInputNumberFormField> | |||
| ></SliderInputSwitchFormField> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,92 @@ | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { SingleFormSlider } from '../ui/dual-range-slider'; | |||
| import { | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '../ui/form'; | |||
| import { Input } from '../ui/input'; | |||
| import { Switch } from '../ui/switch'; | |||
| type SliderInputSwitchFormFieldProps = { | |||
| max?: number; | |||
| min?: number; | |||
| step?: number; | |||
| name: string; | |||
| label: string; | |||
| defaultValue?: number; | |||
| className?: string; | |||
| checkName: string; | |||
| }; | |||
| export function SliderInputSwitchFormField({ | |||
| max, | |||
| min, | |||
| step, | |||
| label, | |||
| name, | |||
| defaultValue, | |||
| className, | |||
| checkName, | |||
| }: SliderInputSwitchFormFieldProps) { | |||
| const form = useFormContext(); | |||
| const disabled = !form.watch(checkName); | |||
| const { t } = useTranslate('chat'); | |||
| return ( | |||
| <FormField | |||
| control={form.control} | |||
| name={name} | |||
| defaultValue={defaultValue} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel tooltip={t(`${label}Tip`)}>{t(label)}</FormLabel> | |||
| <div | |||
| className={cn('flex items-center gap-4 justify-between', className)} | |||
| > | |||
| <FormField | |||
| control={form.control} | |||
| name={checkName} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormControl> | |||
| <Switch | |||
| checked={field.value} | |||
| onCheckedChange={field.onChange} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormControl> | |||
| <SingleFormSlider | |||
| {...field} | |||
| max={max} | |||
| min={min} | |||
| step={step} | |||
| disabled={disabled} | |||
| ></SingleFormSlider> | |||
| </FormControl> | |||
| <FormControl> | |||
| <Input | |||
| disabled={disabled} | |||
| type={'number'} | |||
| className="h-7 w-20" | |||
| max={max} | |||
| min={min} | |||
| step={step} | |||
| {...field} | |||
| ></Input> | |||
| </FormControl> | |||
| </div> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,163 @@ | |||
| 'use client'; | |||
| import { CheckIcon, ChevronDownIcon } from 'lucide-react'; | |||
| import { Fragment, useCallback, useEffect, useId, useState } from 'react'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| Command, | |||
| CommandEmpty, | |||
| CommandGroup, | |||
| CommandInput, | |||
| CommandItem, | |||
| CommandList, | |||
| } from '@/components/ui/command'; | |||
| import { | |||
| Popover, | |||
| PopoverContent, | |||
| PopoverTrigger, | |||
| } from '@/components/ui/popover'; | |||
| import { RAGFlowSelectOptionType } from '../ui/select'; | |||
| const countries = [ | |||
| { | |||
| label: 'America', | |||
| options: [ | |||
| { value: 'United States', label: '🇺🇸' }, | |||
| { value: 'Canada', label: '🇨🇦' }, | |||
| { value: 'Mexico', label: '🇲🇽' }, | |||
| ], | |||
| }, | |||
| { | |||
| label: 'Africa', | |||
| options: [ | |||
| { value: 'South Africa', label: '🇿🇦' }, | |||
| { value: 'Nigeria', label: '🇳🇬' }, | |||
| { value: 'Morocco', label: '🇲🇦' }, | |||
| ], | |||
| }, | |||
| { | |||
| label: 'Asia', | |||
| options: [ | |||
| { value: 'China', label: '🇨🇳' }, | |||
| { value: 'Japan', label: '🇯🇵' }, | |||
| { value: 'India', label: '🇮🇳' }, | |||
| ], | |||
| }, | |||
| { | |||
| label: 'Europe', | |||
| options: [ | |||
| { value: 'United Kingdom', label: '🇬🇧' }, | |||
| { value: 'France', label: '🇫🇷' }, | |||
| { value: 'Germany', label: '🇩🇪' }, | |||
| ], | |||
| }, | |||
| { | |||
| label: 'Oceania', | |||
| options: [ | |||
| { value: 'Australia', label: '🇦🇺' }, | |||
| { value: 'New Zealand', label: '🇳🇿' }, | |||
| ], | |||
| }, | |||
| ]; | |||
| export type SelectWithSearchFlagOptionType = { | |||
| label: string; | |||
| options: RAGFlowSelectOptionType[]; | |||
| }; | |||
| export type SelectWithSearchFlagProps = { | |||
| options?: SelectWithSearchFlagOptionType[]; | |||
| value?: string; | |||
| onChange?(value: string): void; | |||
| }; | |||
| export function SelectWithSearch({ | |||
| value: val = '', | |||
| onChange, | |||
| options = countries, | |||
| }: SelectWithSearchFlagProps) { | |||
| const id = useId(); | |||
| const [open, setOpen] = useState<boolean>(false); | |||
| const [value, setValue] = useState<string>(''); | |||
| const handleSelect = useCallback( | |||
| (val: string) => { | |||
| setValue(val); | |||
| setOpen(false); | |||
| onChange?.(val); | |||
| }, | |||
| [onChange], | |||
| ); | |||
| useEffect(() => { | |||
| setValue(val); | |||
| }, [val]); | |||
| return ( | |||
| <Popover open={open} onOpenChange={setOpen}> | |||
| <PopoverTrigger asChild> | |||
| <Button | |||
| id={id} | |||
| variant="outline" | |||
| role="combobox" | |||
| aria-expanded={open} | |||
| className="bg-background hover:bg-background border-input w-full justify-between px-3 font-normal outline-offset-0 outline-none focus-visible:outline-[3px]" | |||
| > | |||
| {value ? ( | |||
| <span className="flex min-w-0 options-center gap-2"> | |||
| <span className="text-lg leading-none truncate"> | |||
| { | |||
| options | |||
| .map((group) => | |||
| group.options.find((item) => item.value === value), | |||
| ) | |||
| .filter(Boolean)[0]?.label | |||
| } | |||
| </span> | |||
| </span> | |||
| ) : ( | |||
| <span className="text-muted-foreground">Select value</span> | |||
| )} | |||
| <ChevronDownIcon | |||
| size={16} | |||
| className="text-muted-foreground/80 shrink-0" | |||
| aria-hidden="true" | |||
| /> | |||
| </Button> | |||
| </PopoverTrigger> | |||
| <PopoverContent | |||
| className="border-input w-full min-w-[var(--radix-popper-anchor-width)] p-0" | |||
| align="start" | |||
| > | |||
| <Command> | |||
| <CommandInput placeholder="Search ..." /> | |||
| <CommandList> | |||
| <CommandEmpty>No data found.</CommandEmpty> | |||
| {options.map((group) => ( | |||
| <Fragment key={group.label}> | |||
| <CommandGroup heading={group.label}> | |||
| {group.options.map((option) => ( | |||
| <CommandItem | |||
| key={option.value} | |||
| value={option.value} | |||
| onSelect={handleSelect} | |||
| > | |||
| <span className="text-lg leading-none"> | |||
| {option.label} | |||
| </span> | |||
| {option.value} | |||
| {value === option.value && ( | |||
| <CheckIcon size={16} className="ml-auto" /> | |||
| )} | |||
| </CommandItem> | |||
| ))} | |||
| </CommandGroup> | |||
| </Fragment> | |||
| ))} | |||
| </CommandList> | |||
| </Command> | |||
| </PopoverContent> | |||
| </Popover> | |||
| ); | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| import { LlmSettingSchema } from '@/components/llm-setting-items/next'; | |||
| import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| @@ -116,6 +117,7 @@ export function useFormConfigMap() { | |||
| component: CategorizeForm, | |||
| defaultValues: { message_history_window_size: 1 }, | |||
| schema: z.object({ | |||
| ...LlmSettingSchema, | |||
| message_history_window_size: z.number(), | |||
| items: z.array( | |||
| z.object({ | |||
| @@ -1,20 +1,45 @@ | |||
| import { LargeModelFormField } from '@/components/large-model-form-field'; | |||
| import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; | |||
| import { Form } from '@/components/ui/form'; | |||
| import { SelectWithSearch } from '@/components/originui/select-with-search'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { DynamicInputVariable } from '../components/next-dynamic-input-variable'; | |||
| import DynamicCategorize from './dynamic-categorize'; | |||
| const CategorizeForm = ({ form, node }: INextOperatorForm) => { | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| className="space-y-6 p-5 overflow-auto max-h-[76vh]" | |||
| className="space-y-6 p-5 " | |||
| onSubmit={(e) => { | |||
| e.preventDefault(); | |||
| }} | |||
| > | |||
| <DynamicInputVariable node={node}></DynamicInputVariable> | |||
| <FormField | |||
| control={form.control} | |||
| name="input" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel tooltip={t('chat.modelTip')}> | |||
| {t('chat.input')} | |||
| </FormLabel> | |||
| <FormControl> | |||
| <SelectWithSearch {...field}></SelectWithSearch> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <LargeModelFormField></LargeModelFormField> | |||
| <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> | |||
| <DynamicCategorize nodeId={node?.id}></DynamicCategorize> | |||
| @@ -1,4 +1,4 @@ | |||
| import { NextLLMSelect } from '@/components/llm-select'; | |||
| import { NextLLMSelect } from '@/components/llm-select/next'; | |||
| import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; | |||
| import { PromptEditor } from '@/components/prompt-editor'; | |||
| import { | |||
| @@ -1,4 +1,4 @@ | |||
| import { NextLLMSelect } from '@/components/llm-select'; | |||
| import { NextLLMSelect } from '@/components/llm-select/next'; | |||
| import { TopNFormField } from '@/components/top-n-item'; | |||
| import { | |||
| Form, | |||
| @@ -1,4 +1,4 @@ | |||
| import { NextLLMSelect } from '@/components/llm-select'; | |||
| import { NextLLMSelect } from '@/components/llm-select/next'; | |||
| import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; | |||
| import { | |||
| Form, | |||
| @@ -158,3 +158,26 @@ export const downloadJsonFile = async ( | |||
| const blob = new Blob([JSON.stringify(data)], { type: FileMimeType.Json }); | |||
| downloadFileFromBlob(blob, fileName); | |||
| }; | |||
| export function transformBase64ToFileWithPreview( | |||
| dataUrl: string, | |||
| filename: string = 'file', | |||
| ) { | |||
| const file = transformBase64ToFile(dataUrl, filename); | |||
| (file as any).preview = dataUrl; | |||
| return file; | |||
| } | |||
| export const getBase64FromFileList = async (fileList?: File[]) => { | |||
| if (Array.isArray(fileList) && fileList.length > 0) { | |||
| const file = fileList[0]; | |||
| if (file) { | |||
| const base64 = await transformFile2Base64(file); | |||
| return base64; | |||
| } | |||
| } | |||
| return ''; | |||
| }; | |||