### What problem does this PR solve? Refactor Datasets UI #3221. ### Type of change - [X] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| import { FormLabel } from '@/components/ui/form'; | |||||
| import { MultiSelect } from '@/components/ui/multi-select'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| const Languages = [ | |||||
| 'English', | |||||
| 'Chinese', | |||||
| 'Spanish', | |||||
| 'French', | |||||
| 'German', | |||||
| 'Japanese', | |||||
| 'Korean', | |||||
| ]; | |||||
| const options = Languages.map((x) => ({ label: x, value: x })); | |||||
| type CrossLanguageItemProps = { | |||||
| name?: string | Array<string>; | |||||
| onChange: (arg: string[]) => void; | |||||
| }; | |||||
| export const CrossLanguageItem = ({ | |||||
| name = ['prompt_config', 'cross_languages'], | |||||
| onChange = () => {}, | |||||
| }: CrossLanguageItemProps) => { | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <div> | |||||
| <div className="pb-2"> | |||||
| <FormLabel tooltip={t('chat.crossLanguageTip')}> | |||||
| {t('chat.crossLanguage')} | |||||
| </FormLabel> | |||||
| </div> | |||||
| <MultiSelect | |||||
| options={options} | |||||
| onValueChange={(val) => { | |||||
| onChange(val); | |||||
| }} | |||||
| // defaultValue={field.value} | |||||
| placeholder={t('fileManager.pleaseSelect')} | |||||
| maxCount={100} | |||||
| // {...field} | |||||
| modalPopover | |||||
| /> | |||||
| </div> | |||||
| ); | |||||
| }; | 
| <FormField | <FormField | ||||
| control={form.control} | control={form.control} | ||||
| name={'parser_config.delimiter'} | name={'parser_config.delimiter'} | ||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={t('knowledgeDetails.delimiterTip')}> | |||||
| {t('knowledgeDetails.delimiter')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <DelimiterInput {...field}></DelimiterInput> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| render={({ field }) => { | |||||
| if (typeof field.value === 'undefined') { | |||||
| // default value set | |||||
| form.setValue('parser_config.delimiter', '\n'); | |||||
| } | |||||
| return ( | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={t('knowledgeDetails.delimiterTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('knowledgeDetails.delimiter')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <DelimiterInput {...field}></DelimiterInput> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| ); | |||||
| }} | |||||
| /> | /> | ||||
| ); | ); | ||||
| } | } | 
| display: flex; | display: flex; | ||||
| gap: 8px; | gap: 8px; | ||||
| flex-wrap: wrap; | flex-wrap: wrap; | ||||
| width: 100%; | |||||
| // width: 100%; | |||||
| margin-bottom: 8px; | margin-bottom: 8px; | ||||
| } | } | ||||
| .tag { | .tag { | ||||
| max-width: 100%; | max-width: 100%; | ||||
| margin: 0; | margin: 0; | ||||
| padding: 2px 20px 2px 4px; | |||||
| padding: 2px 20px 0px 4px; | |||||
| height: 26px; | |||||
| font-size: 14px; | font-size: 14px; | ||||
| .textEllipsis(); | .textEllipsis(); | ||||
| position: relative; | position: relative; | 
| }; | }; | ||||
| return ( | return ( | ||||
| <div> | |||||
| <div className="flex gap-[8px] items-start"> | |||||
| {Array.isArray(tagChild) && tagChild.length > 0 && ( | {Array.isArray(tagChild) && tagChild.length > 0 && ( | ||||
| <TweenOneGroup | <TweenOneGroup | ||||
| className={styles.tweenGroup} | className={styles.tweenGroup} | ||||
| </TweenOneGroup> | </TweenOneGroup> | ||||
| )} | )} | ||||
| {inputVisible ? ( | {inputVisible ? ( | ||||
| <Input | |||||
| ref={inputRef} | |||||
| type="text" | |||||
| size="small" | |||||
| value={inputValue} | |||||
| onChange={handleInputChange} | |||||
| onBlur={handleInputConfirm} | |||||
| onPressEnter={handleInputConfirm} | |||||
| /> | |||||
| <div className="w-[180px] mb-[8px]"> | |||||
| <Input | |||||
| ref={inputRef} | |||||
| type="text" | |||||
| size="small" | |||||
| value={inputValue} | |||||
| onChange={handleInputChange} | |||||
| onBlur={handleInputConfirm} | |||||
| onPressEnter={handleInputConfirm} | |||||
| /> | |||||
| </div> | |||||
| ) : ( | ) : ( | ||||
| <Tag onClick={showInput} style={tagPlusStyle}> | |||||
| <PlusOutlined /> | |||||
| </Tag> | |||||
| <div className="mb-[8px]"> | |||||
| <Tag onClick={showInput} style={tagPlusStyle}> | |||||
| <PlusOutlined /> | |||||
| </Tag> | |||||
| </div> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| ); | ); | 
| control={form.control} | control={form.control} | ||||
| name={name} | name={name} | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel>{t('entityTypes')}</FormLabel> | |||||
| <FormControl> | |||||
| <EditTag {...field}></EditTag> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| <span className="text-red-600">*</span> {t('entityTypes')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <EditTag {...field}></EditTag> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | 
| <FormField | <FormField | ||||
| control={form.control} | control={form.control} | ||||
| name="parser_config.html4excel" | name="parser_config.html4excel" | ||||
| render={({ field }) => ( | |||||
| <FormItem defaultChecked={false}> | |||||
| <FormLabel tooltip={t('html4excelTip')}>{t('html4excel')}</FormLabel> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| render={({ field }) => { | |||||
| if (typeof field.value === 'undefined') { | |||||
| // default value set | |||||
| form.setValue('parser_config.html4excel', false); | |||||
| } | |||||
| return ( | |||||
| <FormItem defaultChecked={false} className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={t('html4excelTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('html4excel')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| ); | |||||
| }} | |||||
| /> | /> | ||||
| ); | ); | ||||
| } | } | 
| <FormField | <FormField | ||||
| control={form.control} | control={form.control} | ||||
| name="parser_config.layout_recognize" | name="parser_config.layout_recognize" | ||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={t('layoutRecognizeTip')}> | |||||
| {t('layoutRecognize')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <RAGFlowSelect {...field} options={options}></RAGFlowSelect> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| render={({ field }) => { | |||||
| if (typeof field.value === 'undefined') { | |||||
| // default value set | |||||
| form.setValue( | |||||
| 'parser_config.layout_recognize', | |||||
| form.formState.defaultValues?.parser_config?.layout_recognize ?? | |||||
| 'DeepDOC', | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={t('layoutRecognizeTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('layoutRecognize')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <RAGFlowSelect {...field} options={options}></RAGFlowSelect> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| ); | |||||
| }} | |||||
| /> | /> | ||||
| ); | ); | ||||
| } | } | 
| import * as React from 'react'; | |||||
| import { cn } from '@/lib/utils'; | |||||
| function Input({ className, type, ...props }: React.ComponentProps<'input'>) { | |||||
| return ( | |||||
| <input | |||||
| type={type} | |||||
| data-slot="input" | |||||
| className={cn( | |||||
| 'border-input file:text-foreground placeholder:text-muted-foreground/70 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50', | |||||
| 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', | |||||
| 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', | |||||
| type === 'search' && | |||||
| '[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none', | |||||
| type === 'file' && | |||||
| 'text-muted-foreground/70 file:border-input file:text-foreground p-0 pr-3 italic file:me-3 file:h-full file:border-0 file:border-r file:border-solid file:bg-transparent file:px-3 file:text-sm file:font-medium file:not-italic', | |||||
| className, | |||||
| )} | |||||
| {...props} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| export { Input }; | 
| tooltip={t('pageRankTip')} | tooltip={t('pageRankTip')} | ||||
| defaultValue={0} | defaultValue={0} | ||||
| max={100} | max={100} | ||||
| min={1} | |||||
| min={0} | |||||
| ></SliderInputFormField> | ></SliderInputFormField> | ||||
| ); | ); | ||||
| } | } | 
| control={form.control} | control={form.control} | ||||
| name="parser_config.graphrag.use_graphrag" | name="parser_config.graphrag.use_graphrag" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem defaultChecked={false}> | |||||
| <FormLabel tooltip={t('useGraphRagTip')}> | |||||
| {t('useGraphRag')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem defaultChecked={false} className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={t('useGraphRagTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('useGraphRag')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | ||||
| control={form.control} | control={form.control} | ||||
| name="parser_config.graphrag.method" | name="parser_config.graphrag.method" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel | |||||
| tooltip={renderWideTooltip( | |||||
| <div | |||||
| dangerouslySetInnerHTML={{ | |||||
| __html: t('graphRagMethodTip'), | |||||
| }} | |||||
| ></div>, | |||||
| )} | |||||
| > | |||||
| {t('graphRagMethod')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <RAGFlowSelect | |||||
| {...field} | |||||
| options={methodOptions} | |||||
| ></RAGFlowSelect> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| tooltip={renderWideTooltip( | |||||
| <div | |||||
| dangerouslySetInnerHTML={{ | |||||
| __html: t('graphRagMethodTip'), | |||||
| }} | |||||
| ></div>, | |||||
| )} | |||||
| > | |||||
| {t('graphRagMethod')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <RAGFlowSelect | |||||
| {...field} | |||||
| options={methodOptions} | |||||
| ></RAGFlowSelect> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | ||||
| control={form.control} | control={form.control} | ||||
| name="parser_config.graphrag.resolution" | name="parser_config.graphrag.resolution" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel tooltip={renderWideTooltip('resolutionTip')}> | |||||
| {t('resolution')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={renderWideTooltip('resolutionTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('resolution')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | ||||
| control={form.control} | control={form.control} | ||||
| name="parser_config.graphrag.community" | name="parser_config.graphrag.community" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel tooltip={renderWideTooltip('communityTip')}> | |||||
| {t('community')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={renderWideTooltip('communityTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('community')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | 
| import { DocumentParserType } from '@/constants/knowledge'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import random from 'lodash/random'; | |||||
| import { Plus } from 'lucide-react'; | |||||
| import { useCallback } from 'react'; | |||||
| import { useFormContext, useWatch } from 'react-hook-form'; | |||||
| import { SliderInputFormField } from '../slider-input-form-field'; | |||||
| import { Button } from '../ui/button'; | |||||
| import { | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from '../ui/form'; | |||||
| import { Input } from '../ui/input'; | |||||
| import { Switch } from '../ui/switch'; | |||||
| import { Textarea } from '../ui/textarea'; | |||||
| export const excludedParseMethods = [ | |||||
| DocumentParserType.Table, | |||||
| DocumentParserType.Resume, | |||||
| DocumentParserType.One, | |||||
| DocumentParserType.Picture, | |||||
| DocumentParserType.KnowledgeGraph, | |||||
| DocumentParserType.Qa, | |||||
| DocumentParserType.Tag, | |||||
| ]; | |||||
| export const showRaptorParseConfiguration = ( | |||||
| parserId: DocumentParserType | undefined, | |||||
| ) => { | |||||
| return !excludedParseMethods.some((x) => x === parserId); | |||||
| }; | |||||
| export const excludedTagParseMethods = [ | |||||
| DocumentParserType.Table, | |||||
| DocumentParserType.KnowledgeGraph, | |||||
| DocumentParserType.Tag, | |||||
| ]; | |||||
| export const showTagItems = (parserId: DocumentParserType) => { | |||||
| return !excludedTagParseMethods.includes(parserId); | |||||
| }; | |||||
| const UseRaptorField = 'parser_config.raptor.use_raptor'; | |||||
| const RandomSeedField = 'parser_config.raptor.random_seed'; | |||||
| // The three types "table", "resume" and "one" do not display this configuration. | |||||
| const RaptorFormFields = () => { | |||||
| const form = useFormContext(); | |||||
| const { t } = useTranslate('knowledgeConfiguration'); | |||||
| const useRaptor = useWatch({ name: UseRaptorField }); | |||||
| const handleGenerate = useCallback(() => { | |||||
| form.setValue(RandomSeedField, random(10000)); | |||||
| }, [form]); | |||||
| return ( | |||||
| <> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={UseRaptorField} | |||||
| render={({ field }) => ( | |||||
| <FormItem defaultChecked={false}> | |||||
| <FormLabel tooltip={t('useRaptorTip')}>{t('useRaptor')}</FormLabel> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| {useRaptor && ( | |||||
| <div className="space-y-3"> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={'parser_config.raptor.prompt'} | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={t('promptTip')}>{t('prompt')}</FormLabel> | |||||
| <FormControl> | |||||
| <Textarea {...field} rows={8} /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <SliderInputFormField | |||||
| name={'parser_config.raptor.max_token'} | |||||
| label={t('maxToken')} | |||||
| tooltip={t('maxTokenTip')} | |||||
| defaultValue={256} | |||||
| max={2048} | |||||
| min={0} | |||||
| ></SliderInputFormField> | |||||
| <SliderInputFormField | |||||
| name={'parser_config.raptor.threshold'} | |||||
| label={t('threshold')} | |||||
| tooltip={t('thresholdTip')} | |||||
| defaultValue={0.1} | |||||
| step={0.01} | |||||
| max={1} | |||||
| min={0} | |||||
| ></SliderInputFormField> | |||||
| <SliderInputFormField | |||||
| name={'parser_config.raptor.max_cluster'} | |||||
| label={t('maxCluster')} | |||||
| tooltip={t('maxClusterTip')} | |||||
| defaultValue={64} | |||||
| max={1024} | |||||
| min={1} | |||||
| ></SliderInputFormField> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={'parser_config.raptor.random_seed'} | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>{t('randomSeed')}</FormLabel> | |||||
| <FormControl defaultValue={0}> | |||||
| <div className="flex gap-4"> | |||||
| <Input {...field} /> | |||||
| <Button | |||||
| size={'sm'} | |||||
| onClick={handleGenerate} | |||||
| type={'button'} | |||||
| > | |||||
| <Plus /> | |||||
| </Button> | |||||
| </div> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| </div> | |||||
| )} | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default RaptorFormFields; | 
| <FormField | <FormField | ||||
| control={form.control} | control={form.control} | ||||
| name={UseRaptorField} | name={UseRaptorField} | ||||
| render={({ field }) => ( | |||||
| <FormItem defaultChecked={false}> | |||||
| <FormLabel tooltip={t('useRaptorTip')}>{t('useRaptor')}</FormLabel> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| render={({ field }) => { | |||||
| if (typeof field.value === 'undefined') { | |||||
| // default value set | |||||
| form.setValue('parser_config.raptor.use_raptor', false); | |||||
| } | |||||
| return ( | |||||
| <FormItem | |||||
| defaultChecked={false} | |||||
| className="items-center space-y-0 " | |||||
| > | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={t('useRaptorTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('useRaptor')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| ></Switch> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| ); | |||||
| }} | |||||
| /> | /> | ||||
| {useRaptor && ( | {useRaptor && ( | ||||
| <div className="space-y-3"> | <div className="space-y-3"> | ||||
| control={form.control} | control={form.control} | ||||
| name={'parser_config.raptor.prompt'} | name={'parser_config.raptor.prompt'} | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel tooltip={t('promptTip')}>{t('prompt')}</FormLabel> | |||||
| <FormControl> | |||||
| <Textarea {...field} rows={8} /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-start"> | |||||
| <FormLabel | |||||
| tooltip={t('promptTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('prompt')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <Textarea {...field} rows={8} /> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | ||||
| control={form.control} | control={form.control} | ||||
| name={'parser_config.raptor.random_seed'} | name={'parser_config.raptor.random_seed'} | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel>{t('randomSeed')}</FormLabel> | |||||
| <FormControl defaultValue={0}> | |||||
| <div className="flex gap-4"> | |||||
| <Input {...field} /> | |||||
| <Button | |||||
| size={'sm'} | |||||
| onClick={handleGenerate} | |||||
| type={'button'} | |||||
| > | |||||
| <Plus /> | |||||
| </Button> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| {t('randomSeed')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl defaultValue={0}> | |||||
| <div className="flex gap-4"> | |||||
| <Input {...field} /> | |||||
| <Button | |||||
| size={'sm'} | |||||
| onClick={handleGenerate} | |||||
| type={'button'} | |||||
| > | |||||
| <Plus /> | |||||
| </Button> | |||||
| </div> | |||||
| </FormControl> | |||||
| </div> | </div> | ||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | 
| import { Input } from '@/components/originui/input'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import { EyeIcon, EyeOffIcon } from 'lucide-react'; | |||||
| import { ChangeEvent, LegacyRef, forwardRef, useId, useState } from 'react'; | |||||
| type PropType = { | |||||
| name: string; | |||||
| value: string; | |||||
| onBlur: () => void; | |||||
| onChange: (event: ChangeEvent<HTMLInputElement>) => void; | |||||
| }; | |||||
| function PasswordInput( | |||||
| props: PropType, | |||||
| ref: LegacyRef<HTMLInputElement> | undefined, | |||||
| ) { | |||||
| const id = useId(); | |||||
| const [isVisible, setIsVisible] = useState<boolean>(false); | |||||
| const toggleVisibility = () => setIsVisible((prevState) => !prevState); | |||||
| const { t } = useTranslate('setting'); | |||||
| return ( | |||||
| <div className="*:not-first:mt-2 w-full"> | |||||
| {/* <Label htmlFor={id}>Show/hide password input</Label> */} | |||||
| <div className="relative"> | |||||
| <Input | |||||
| autoComplete="off" | |||||
| inputMode="numeric" | |||||
| id={id} | |||||
| className="pe-9" | |||||
| placeholder="" | |||||
| type={isVisible ? 'text' : 'password'} | |||||
| value={props.value} | |||||
| onBlur={props.onBlur} | |||||
| onChange={(ev) => props.onChange(ev)} | |||||
| /> | |||||
| <button | |||||
| className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50" | |||||
| type="button" | |||||
| onClick={toggleVisibility} | |||||
| aria-label={isVisible ? 'Hide password' : 'Show password'} | |||||
| aria-pressed={isVisible} | |||||
| aria-controls="password" | |||||
| > | |||||
| {isVisible ? ( | |||||
| <EyeOffIcon size={16} aria-hidden="true" /> | |||||
| ) : ( | |||||
| <EyeIcon size={16} aria-hidden="true" /> | |||||
| )} | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default forwardRef(PasswordInput); | 
| <FormField | <FormField | ||||
| control={form.control} | control={form.control} | ||||
| name={name} | name={name} | ||||
| defaultValue={defaultValue} | |||||
| defaultValue={defaultValue || 0} | |||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel tooltip={tooltip}>{label}</FormLabel> | |||||
| <div | |||||
| className={cn( | |||||
| 'flex items-center gap-14 justify-between', | |||||
| className, | |||||
| )} | |||||
| > | |||||
| <FormControl> | |||||
| <SingleFormSlider | |||||
| {...field} | |||||
| max={max} | |||||
| min={min} | |||||
| step={step} | |||||
| // defaultValue={ | |||||
| // typeof defaultValue === 'number' ? [defaultValue] : undefined | |||||
| // } | |||||
| ></SingleFormSlider> | |||||
| </FormControl> | |||||
| <FormControl> | |||||
| <Input | |||||
| type={'number'} | |||||
| className="h-7 w-20" | |||||
| max={max} | |||||
| min={min} | |||||
| step={step} | |||||
| {...field} | |||||
| // defaultValue={defaultValue} | |||||
| ></Input> | |||||
| </FormControl> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={tooltip} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {label} | |||||
| </FormLabel> | |||||
| <div | |||||
| className={cn( | |||||
| 'flex items-center gap-14 justify-between', | |||||
| 'w-3/4', | |||||
| className, | |||||
| )} | |||||
| > | |||||
| <FormControl> | |||||
| <SingleFormSlider | |||||
| {...field} | |||||
| max={max} | |||||
| min={min} | |||||
| step={step} | |||||
| // defaultValue={ | |||||
| // typeof defaultValue === 'number' ? [defaultValue] : undefined | |||||
| // } | |||||
| ></SingleFormSlider> | |||||
| </FormControl> | |||||
| <FormControl> | |||||
| <Input | |||||
| type={'number'} | |||||
| className="h-7 w-20" | |||||
| max={max} | |||||
| min={min} | |||||
| step={step} | |||||
| {...field} | |||||
| onChange={(ev) => { | |||||
| const value = ev.target.value; | |||||
| field.onChange(value === '' ? 0 : Number(value)); // convert to number | |||||
| }} | |||||
| // defaultValue={defaultValue} | |||||
| ></Input> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | </div> | ||||
| <FormMessage /> | <FormMessage /> | ||||
| </FormItem> | </FormItem> | 
| import * as TabsPrimitive from '@radix-ui/react-tabs'; | |||||
| import * as React from 'react'; | |||||
| import { cn } from '@/lib/utils'; | |||||
| function Tabs({ | |||||
| className, | |||||
| ...props | |||||
| }: React.ComponentProps<typeof TabsPrimitive.Root>) { | |||||
| return ( | |||||
| <TabsPrimitive.Root | |||||
| data-slot="tabs" | |||||
| className={cn('flex flex-col gap-2', className)} | |||||
| {...props} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| function TabsList({ | |||||
| className, | |||||
| ...props | |||||
| }: React.ComponentProps<typeof TabsPrimitive.List>) { | |||||
| return ( | |||||
| <TabsPrimitive.List | |||||
| data-slot="tabs-list" | |||||
| className={cn( | |||||
| 'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]', | |||||
| className, | |||||
| )} | |||||
| {...props} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| function TabsTrigger({ | |||||
| className, | |||||
| ...props | |||||
| }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { | |||||
| return ( | |||||
| <TabsPrimitive.Trigger | |||||
| data-slot="tabs-trigger" | |||||
| className={cn( | |||||
| "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", | |||||
| className, | |||||
| )} | |||||
| {...props} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| function TabsContent({ | |||||
| className, | |||||
| ...props | |||||
| }: React.ComponentProps<typeof TabsPrimitive.Content>) { | |||||
| return ( | |||||
| <TabsPrimitive.Content | |||||
| data-slot="tabs-content" | |||||
| className={cn('flex-1 outline-none', className)} | |||||
| {...props} | |||||
| /> | |||||
| ); | |||||
| } | |||||
| export { Tabs, TabsContent, TabsList, TabsTrigger }; | 
| return { data, loading, saveKnowledgeConfiguration: mutateAsync }; | return { data, loading, saveKnowledgeConfiguration: mutateAsync }; | ||||
| }; | }; | ||||
| export const useFetchKnowledgeBaseConfiguration = () => { | |||||
| export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => { | |||||
| const { id } = useParams(); | const { id } = useParams(); | ||||
| let queryKey: (KnowledgeApiAction | number)[] = [ | |||||
| KnowledgeApiAction.FetchKnowledgeDetail, | |||||
| ]; | |||||
| if (typeof refreshCount === 'number') { | |||||
| queryKey = [KnowledgeApiAction.FetchKnowledgeDetail, refreshCount]; | |||||
| } | |||||
| const { data, isFetching: loading } = useQuery<IKnowledge>({ | const { data, isFetching: loading } = useQuery<IKnowledge>({ | ||||
| queryKey: [KnowledgeApiAction.FetchKnowledgeDetail], | |||||
| queryKey, | |||||
| initialData: {} as IKnowledge, | initialData: {} as IKnowledge, | ||||
| gcTime: 0, | gcTime: 0, | ||||
| queryFn: async () => { | queryFn: async () => { | 
| nickname: string; | nickname: string; | ||||
| password: string; | password: string; | ||||
| status: string; | status: string; | ||||
| timezone: string; | |||||
| update_date: string; | update_date: string; | ||||
| update_time: number; | update_time: number; | ||||
| } | } | 
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'Profil', | profile: 'Profil', | ||||
| avatar: 'Avatar', | |||||
| profileDescription: | profileDescription: | ||||
| 'Aktualisieren Sie hier Ihr Foto und Ihre persönlichen Daten.', | 'Aktualisieren Sie hier Ihr Foto und Ihre persönlichen Daten.', | ||||
| maxTokens: 'Maximale Tokens', | maxTokens: 'Maximale Tokens', | 
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'Profile', | profile: 'Profile', | ||||
| avatar: 'Avatar', | |||||
| profileDescription: 'Update your photo and personal details here.', | profileDescription: 'Update your photo and personal details here.', | ||||
| maxTokens: 'Max Tokens', | maxTokens: 'Max Tokens', | ||||
| maxTokensMessage: 'Max Tokens is required', | maxTokensMessage: 'Max Tokens is required', | 
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'Perfil', | profile: 'Perfil', | ||||
| avatar: 'Avatar', | |||||
| profileDescription: 'Actualiza tu foto y tus datos personales aquí.', | profileDescription: 'Actualiza tu foto y tus datos personales aquí.', | ||||
| maxTokens: 'Máximo de tokens', | maxTokens: 'Máximo de tokens', | ||||
| maxTokensMessage: 'El máximo de tokens es obligatorio', | maxTokensMessage: 'El máximo de tokens es obligatorio', | 
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'Profil', | profile: 'Profil', | ||||
| avatar: 'Avatar', | |||||
| profileDescription: 'Perbarui foto dan detail pribadi Anda di sini.', | profileDescription: 'Perbarui foto dan detail pribadi Anda di sini.', | ||||
| maxTokens: 'Token Maksimum', | maxTokens: 'Token Maksimum', | ||||
| maxTokensMessage: 'Token Maksimum diperlukan', | maxTokensMessage: 'Token Maksimum diperlukan', | 
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'プロファイル', | profile: 'プロファイル', | ||||
| avatar: 'アバター', | |||||
| profileDescription: 'ここで写真と個人情報を更新してください。', | profileDescription: 'ここで写真と個人情報を更新してください。', | ||||
| maxTokens: '最大トークン数', | maxTokens: '最大トークン数', | ||||
| maxTokensMessage: '最大トークン数は必須です', | maxTokensMessage: '最大トークン数は必須です', | 
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'Perfil', | profile: 'Perfil', | ||||
| avatar: 'Avatar', | |||||
| profileDescription: 'Atualize sua foto e detalhes pessoais aqui.', | profileDescription: 'Atualize sua foto e detalhes pessoais aqui.', | ||||
| maxTokens: 'Máximo de Tokens', | maxTokens: 'Máximo de Tokens', | ||||
| maxTokensMessage: 'Máximo de Tokens é obrigatório', | maxTokensMessage: 'Máximo de Tokens é obrigatório', | 
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'Hồ sơ', | profile: 'Hồ sơ', | ||||
| avatar: 'Avatar', | |||||
| profileDescription: 'Cập nhật ảnh và thông tin cá nhân của bạn tại đây.', | profileDescription: 'Cập nhật ảnh và thông tin cá nhân của bạn tại đây.', | ||||
| maxTokens: 'Token tối đa', | maxTokens: 'Token tối đa', | ||||
| maxTokensMessage: 'Token tối đa là bắt buộc', | maxTokensMessage: 'Token tối đa là bắt buộc', | 
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: '概述', | profile: '概述', | ||||
| avatar: '头像', | |||||
| profileDescription: '在此更新您的照片和個人詳細信息。', | profileDescription: '在此更新您的照片和個人詳細信息。', | ||||
| maxTokens: '最大token數', | maxTokens: '最大token數', | ||||
| maxTokensMessage: '最大token數是必填項', | maxTokensMessage: '最大token數是必填項', | 
| cancel: '取消', | cancel: '取消', | ||||
| methodTitle: '分块方法说明', | methodTitle: '分块方法说明', | ||||
| methodExamples: '示例', | methodExamples: '示例', | ||||
| methodExamplesDescription: '为帮助您更好地理解,我们提供了相关截图供您参考。', | |||||
| methodExamplesDescription: | |||||
| '为帮助您更好地理解,我们提供了相关截图供您参考。', | |||||
| dialogueExamplesTitle: '对话示例', | dialogueExamplesTitle: '对话示例', | ||||
| methodEmpty: '这将显示知识库类别的可视化解释', | methodEmpty: '这将显示知识库类别的可视化解释', | ||||
| book: `<p>支持的文件格式为<b>DOCX</b>、<b>PDF</b>、<b>TXT</b>。</p><p> | book: `<p>支持的文件格式为<b>DOCX</b>、<b>PDF</b>、<b>TXT</b>。</p><p> | ||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: '概要', | profile: '概要', | ||||
| avatar: '头像', | |||||
| profileDescription: '在此更新您的照片和个人详细信息。', | profileDescription: '在此更新您的照片和个人详细信息。', | ||||
| maxTokens: '最大token数', | maxTokens: '最大token数', | ||||
| maxTokensMessage: '最大token数是必填项', | maxTokensMessage: '最大token数是必填项', | 
| import { PageHeader } from '@/components/page-header'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { Segmented, SegmentedValue } from '@/components/ui/segmented'; | |||||
| import { | |||||
| QueryStringMap, | |||||
| useNavigatePage, | |||||
| } from '@/hooks/logic-hooks/navigate-hooks'; | |||||
| import { Routes } from '@/routes'; | |||||
| import { EllipsisVertical, Save } from 'lucide-react'; | |||||
| import { useMemo } from 'react'; | |||||
| import { Outlet, useLocation } from 'umi'; | |||||
| export default function ChunkPage() { | |||||
| const { navigateToDataset, getQueryString, navigateToChunk } = | |||||
| useNavigatePage(); | |||||
| const location = useLocation(); | |||||
| const options = useMemo(() => { | |||||
| return [ | |||||
| { | |||||
| label: 'Parsed results', | |||||
| value: Routes.ParsedResult, | |||||
| }, | |||||
| { | |||||
| label: 'Chunk result', | |||||
| value: Routes.ChunkResult, | |||||
| }, | |||||
| { | |||||
| label: 'Result view', | |||||
| value: Routes.ResultView, | |||||
| }, | |||||
| ]; | |||||
| }, []); | |||||
| const path = useMemo(() => { | |||||
| return location.pathname.split('/').slice(0, 3).join('/'); | |||||
| }, [location.pathname]); | |||||
| return ( | |||||
| <section> | |||||
| <PageHeader | |||||
| title="Editing block" | |||||
| back={navigateToDataset( | |||||
| getQueryString(QueryStringMap.KnowledgeId) as string, | |||||
| )} | |||||
| > | |||||
| <div> | |||||
| <Segmented | |||||
| options={options} | |||||
| value={path} | |||||
| onChange={navigateToChunk as (val: SegmentedValue) => void} | |||||
| className="bg-colors-background-inverse-standard text-colors-text-neutral-standard" | |||||
| ></Segmented> | |||||
| </div> | |||||
| <div className="flex items-center gap-2"> | |||||
| <Button variant={'icon'} size={'icon'}> | |||||
| <EllipsisVertical /> | |||||
| </Button> | |||||
| <Button variant={'tertiary'} size={'sm'}> | |||||
| <Save /> | |||||
| Save | |||||
| </Button> | |||||
| </div> | |||||
| </PageHeader> | |||||
| <Outlet /> | |||||
| </section> | |||||
| ); | |||||
| } | 
| import ParsedResultPanel from '../parsed-result-panel'; | |||||
| export default function ParsedResult() { | |||||
| return ( | |||||
| <section className="flex"> | |||||
| <div className="flex-1"></div> | |||||
| <ParsedResultPanel></ParsedResultPanel> | |||||
| </section> | |||||
| ); | |||||
| } | 
| .image { | |||||
| width: 100px !important; | |||||
| object-fit: contain; | |||||
| } | |||||
| .imagePreview { | |||||
| max-width: 50vw; | |||||
| max-height: 50vh; | |||||
| object-fit: contain; | |||||
| } | |||||
| .content { | |||||
| flex: 1; | |||||
| .chunkText; | |||||
| } | |||||
| .contentEllipsis { | |||||
| .multipleLineEllipsis(3); | |||||
| } | |||||
| .contentText { | |||||
| word-break: break-all !important; | |||||
| } | |||||
| .chunkCard { | |||||
| width: 100%; | |||||
| } | |||||
| .cardSelected { | |||||
| background-color: @selectedBackgroundColor; | |||||
| } | |||||
| .cardSelectedDark { | |||||
| background-color: #ffffff2f; | |||||
| } | 
| import Image from '@/components/image'; | |||||
| import { IChunk } from '@/interfaces/database/knowledge'; | |||||
| import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import DOMPurify from 'dompurify'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { ChunkTextMode } from '../../constant'; | |||||
| import styles from './index.less'; | |||||
| interface IProps { | |||||
| item: IChunk; | |||||
| checked: boolean; | |||||
| switchChunk: (available?: number, chunkIds?: string[]) => void; | |||||
| editChunk: (chunkId: string) => void; | |||||
| handleCheckboxClick: (chunkId: string, checked: boolean) => void; | |||||
| selected: boolean; | |||||
| clickChunkCard: (chunkId: string) => void; | |||||
| textMode: ChunkTextMode; | |||||
| } | |||||
| const ChunkCard = ({ | |||||
| item, | |||||
| checked, | |||||
| handleCheckboxClick, | |||||
| editChunk, | |||||
| switchChunk, | |||||
| selected, | |||||
| clickChunkCard, | |||||
| textMode, | |||||
| }: IProps) => { | |||||
| const available = Number(item.available_int); | |||||
| const [enabled, setEnabled] = useState(false); | |||||
| const { theme } = useTheme(); | |||||
| const onChange = (checked: boolean) => { | |||||
| setEnabled(checked); | |||||
| switchChunk(available === 0 ? 1 : 0, [item.chunk_id]); | |||||
| }; | |||||
| const handleCheck: CheckboxProps['onChange'] = (e) => { | |||||
| handleCheckboxClick(item.chunk_id, e.target.checked); | |||||
| }; | |||||
| const handleContentDoubleClick = () => { | |||||
| editChunk(item.chunk_id); | |||||
| }; | |||||
| const handleContentClick = () => { | |||||
| clickChunkCard(item.chunk_id); | |||||
| }; | |||||
| useEffect(() => { | |||||
| setEnabled(available === 1); | |||||
| }, [available]); | |||||
| return ( | |||||
| <Card | |||||
| className={classNames(styles.chunkCard, { | |||||
| [`${theme === 'dark' ? styles.cardSelectedDark : styles.cardSelected}`]: | |||||
| selected, | |||||
| })} | |||||
| > | |||||
| <Flex gap={'middle'} justify={'space-between'}> | |||||
| <Checkbox onChange={handleCheck} checked={checked}></Checkbox> | |||||
| {item.image_id && ( | |||||
| <Popover | |||||
| placement="right" | |||||
| content={ | |||||
| <Image id={item.image_id} className={styles.imagePreview}></Image> | |||||
| } | |||||
| > | |||||
| <Image id={item.image_id} className={styles.image}></Image> | |||||
| </Popover> | |||||
| )} | |||||
| <section | |||||
| onDoubleClick={handleContentDoubleClick} | |||||
| onClick={handleContentClick} | |||||
| className={styles.content} | |||||
| > | |||||
| <div | |||||
| dangerouslySetInnerHTML={{ | |||||
| __html: DOMPurify.sanitize(item.content_with_weight), | |||||
| }} | |||||
| className={classNames(styles.contentText, { | |||||
| [styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse, | |||||
| })} | |||||
| ></div> | |||||
| </section> | |||||
| <div> | |||||
| <Switch checked={enabled} onChange={onChange} /> | |||||
| </div> | |||||
| </Flex> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default ChunkCard; | 
| import EditTag from '@/components/edit-tag'; | |||||
| import { useFetchChunk } from '@/hooks/chunk-hooks'; | |||||
| import { IModalProps } from '@/interfaces/common'; | |||||
| import { IChunk } from '@/interfaces/database/knowledge'; | |||||
| import { DeleteOutlined } from '@ant-design/icons'; | |||||
| import { Divider, Form, Input, Modal, Space, Switch } from 'antd'; | |||||
| import React, { useCallback, useEffect, useState } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { useDeleteChunkByIds } from '../../hooks'; | |||||
| import { | |||||
| transformTagFeaturesArrayToObject, | |||||
| transformTagFeaturesObjectToArray, | |||||
| } from '../../utils'; | |||||
| import { TagFeatureItem } from './tag-feature-item'; | |||||
| type FieldType = Pick< | |||||
| IChunk, | |||||
| 'content_with_weight' | 'tag_kwd' | 'question_kwd' | 'important_kwd' | |||||
| >; | |||||
| interface kFProps { | |||||
| doc_id: string; | |||||
| chunkId: string | undefined; | |||||
| parserId: string; | |||||
| } | |||||
| const ChunkCreatingModal: React.FC<IModalProps<any> & kFProps> = ({ | |||||
| doc_id, | |||||
| chunkId, | |||||
| hideModal, | |||||
| onOk, | |||||
| loading, | |||||
| parserId, | |||||
| }) => { | |||||
| const [form] = Form.useForm(); | |||||
| const [checked, setChecked] = useState(false); | |||||
| const { removeChunk } = useDeleteChunkByIds(); | |||||
| const { data } = useFetchChunk(chunkId); | |||||
| const { t } = useTranslation(); | |||||
| const isTagParser = parserId === 'tag'; | |||||
| const handleOk = useCallback(async () => { | |||||
| try { | |||||
| const values = await form.validateFields(); | |||||
| console.log('🚀 ~ handleOk ~ values:', values); | |||||
| onOk?.({ | |||||
| ...values, | |||||
| tag_feas: transformTagFeaturesArrayToObject(values.tag_feas), | |||||
| available_int: checked ? 1 : 0, // available_int | |||||
| }); | |||||
| } catch (errorInfo) { | |||||
| console.log('Failed:', errorInfo); | |||||
| } | |||||
| }, [checked, form, onOk]); | |||||
| const handleRemove = useCallback(() => { | |||||
| if (chunkId) { | |||||
| return removeChunk([chunkId], doc_id); | |||||
| } | |||||
| }, [chunkId, doc_id, removeChunk]); | |||||
| const handleCheck = useCallback(() => { | |||||
| setChecked(!checked); | |||||
| }, [checked]); | |||||
| useEffect(() => { | |||||
| if (data?.code === 0) { | |||||
| const { available_int, tag_feas } = data.data; | |||||
| form.setFieldsValue({ | |||||
| ...(data.data || {}), | |||||
| tag_feas: transformTagFeaturesObjectToArray(tag_feas), | |||||
| }); | |||||
| setChecked(available_int !== 0); | |||||
| } | |||||
| }, [data, form, chunkId]); | |||||
| return ( | |||||
| <Modal | |||||
| title={`${chunkId ? t('common.edit') : t('common.create')} ${t('chunk.chunk')}`} | |||||
| open={true} | |||||
| onOk={handleOk} | |||||
| onCancel={hideModal} | |||||
| okButtonProps={{ loading }} | |||||
| destroyOnClose | |||||
| > | |||||
| <Form form={form} autoComplete="off" layout={'vertical'}> | |||||
| <Form.Item<FieldType> | |||||
| label={t('chunk.chunk')} | |||||
| name="content_with_weight" | |||||
| rules={[{ required: true, message: t('chunk.chunkMessage') }]} | |||||
| > | |||||
| <Input.TextArea autoSize={{ minRows: 4, maxRows: 10 }} /> | |||||
| </Form.Item> | |||||
| <Form.Item<FieldType> label={t('chunk.keyword')} name="important_kwd"> | |||||
| <EditTag></EditTag> | |||||
| </Form.Item> | |||||
| <Form.Item<FieldType> | |||||
| label={t('chunk.question')} | |||||
| name="question_kwd" | |||||
| tooltip={t('chunk.questionTip')} | |||||
| > | |||||
| <EditTag></EditTag> | |||||
| </Form.Item> | |||||
| {isTagParser && ( | |||||
| <Form.Item<FieldType> | |||||
| label={t('knowledgeConfiguration.tagName')} | |||||
| name="tag_kwd" | |||||
| > | |||||
| <EditTag></EditTag> | |||||
| </Form.Item> | |||||
| )} | |||||
| {!isTagParser && <TagFeatureItem></TagFeatureItem>} | |||||
| </Form> | |||||
| {chunkId && ( | |||||
| <section> | |||||
| <Divider></Divider> | |||||
| <Space size={'large'}> | |||||
| <Switch | |||||
| checkedChildren={t('chunk.enabled')} | |||||
| unCheckedChildren={t('chunk.disabled')} | |||||
| onChange={handleCheck} | |||||
| checked={checked} | |||||
| /> | |||||
| <span onClick={handleRemove}> | |||||
| <DeleteOutlined /> {t('common.delete')} | |||||
| </span> | |||||
| </Space> | |||||
| </section> | |||||
| )} | |||||
| </Modal> | |||||
| ); | |||||
| }; | |||||
| export default ChunkCreatingModal; | 
| import { | |||||
| useFetchKnowledgeBaseConfiguration, | |||||
| useFetchTagListByKnowledgeIds, | |||||
| } from '@/hooks/knowledge-hooks'; | |||||
| import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||||
| import { Button, Form, InputNumber, Select } from 'antd'; | |||||
| import { useCallback, useEffect, useMemo } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { FormListItem } from '../../utils'; | |||||
| const FieldKey = 'tag_feas'; | |||||
| export const TagFeatureItem = () => { | |||||
| const form = Form.useFormInstance(); | |||||
| const { t } = useTranslation(); | |||||
| const { data: knowledgeConfiguration } = useFetchKnowledgeBaseConfiguration(); | |||||
| const { setKnowledgeIds, list } = useFetchTagListByKnowledgeIds(); | |||||
| const tagKnowledgeIds = useMemo(() => { | |||||
| return knowledgeConfiguration?.parser_config?.tag_kb_ids ?? []; | |||||
| }, [knowledgeConfiguration?.parser_config?.tag_kb_ids]); | |||||
| const options = useMemo(() => { | |||||
| return list.map((x) => ({ | |||||
| value: x[0], | |||||
| label: x[0], | |||||
| })); | |||||
| }, [list]); | |||||
| const filterOptions = useCallback( | |||||
| (index: number) => { | |||||
| const tags: FormListItem[] = form.getFieldValue(FieldKey) ?? []; | |||||
| // Exclude it's own current data | |||||
| const list = tags | |||||
| .filter((x, idx) => x && index !== idx) | |||||
| .map((x) => x.tag); | |||||
| // Exclude the selected data from other options from one's own options. | |||||
| return options.filter((x) => !list.some((y) => x.value === y)); | |||||
| }, | |||||
| [form, options], | |||||
| ); | |||||
| useEffect(() => { | |||||
| setKnowledgeIds(tagKnowledgeIds); | |||||
| }, [setKnowledgeIds, tagKnowledgeIds]); | |||||
| return ( | |||||
| <Form.Item label={t('knowledgeConfiguration.tags')}> | |||||
| <Form.List name={FieldKey} initialValue={[]}> | |||||
| {(fields, { add, remove }) => ( | |||||
| <> | |||||
| {fields.map(({ key, name, ...restField }) => ( | |||||
| <div key={key} className="flex gap-3 items-center"> | |||||
| <div className="flex flex-1 gap-8"> | |||||
| <Form.Item | |||||
| {...restField} | |||||
| name={[name, 'tag']} | |||||
| rules={[ | |||||
| { required: true, message: t('common.pleaseSelect') }, | |||||
| ]} | |||||
| className="w-2/3" | |||||
| > | |||||
| <Select | |||||
| showSearch | |||||
| placeholder={t('knowledgeConfiguration.tagName')} | |||||
| options={filterOptions(name)} | |||||
| /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| {...restField} | |||||
| name={[name, 'frequency']} | |||||
| rules={[ | |||||
| { required: true, message: t('common.pleaseInput') }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber | |||||
| placeholder={t('knowledgeConfiguration.frequency')} | |||||
| max={10} | |||||
| min={0} | |||||
| /> | |||||
| </Form.Item> | |||||
| </div> | |||||
| <MinusCircleOutlined | |||||
| onClick={() => remove(name)} | |||||
| className="mb-6" | |||||
| /> | |||||
| </div> | |||||
| ))} | |||||
| <Form.Item> | |||||
| <Button | |||||
| type="dashed" | |||||
| onClick={() => add()} | |||||
| block | |||||
| icon={<PlusOutlined />} | |||||
| > | |||||
| {t('knowledgeConfiguration.addTag')} | |||||
| </Button> | |||||
| </Form.Item> | |||||
| </> | |||||
| )} | |||||
| </Form.List> | |||||
| </Form.Item> | |||||
| ); | |||||
| }; | 
| import { ReactComponent as FilterIcon } from '@/assets/filter.svg'; | |||||
| import { KnowledgeRouteKey } from '@/constants/knowledge'; | |||||
| import { IChunkListResult, useSelectChunkList } from '@/hooks/chunk-hooks'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import { useKnowledgeBaseId } from '@/hooks/knowledge-hooks'; | |||||
| import { | |||||
| ArrowLeftOutlined, | |||||
| CheckCircleOutlined, | |||||
| CloseCircleOutlined, | |||||
| DeleteOutlined, | |||||
| DownOutlined, | |||||
| FilePdfOutlined, | |||||
| PlusOutlined, | |||||
| SearchOutlined, | |||||
| } from '@ant-design/icons'; | |||||
| import { | |||||
| Button, | |||||
| Checkbox, | |||||
| Flex, | |||||
| Input, | |||||
| Menu, | |||||
| MenuProps, | |||||
| Popover, | |||||
| Radio, | |||||
| RadioChangeEvent, | |||||
| Segmented, | |||||
| SegmentedProps, | |||||
| Space, | |||||
| Typography, | |||||
| } from 'antd'; | |||||
| import { useCallback, useMemo, useState } from 'react'; | |||||
| import { Link } from 'umi'; | |||||
| import { ChunkTextMode } from '../../constant'; | |||||
| const { Text } = Typography; | |||||
| interface IProps | |||||
| extends Pick< | |||||
| IChunkListResult, | |||||
| 'searchString' | 'handleInputChange' | 'available' | 'handleSetAvailable' | |||||
| > { | |||||
| checked: boolean; | |||||
| selectAllChunk: (checked: boolean) => void; | |||||
| createChunk: () => void; | |||||
| removeChunk: () => void; | |||||
| switchChunk: (available: number) => void; | |||||
| changeChunkTextMode(mode: ChunkTextMode): void; | |||||
| } | |||||
| const ChunkToolBar = ({ | |||||
| selectAllChunk, | |||||
| checked, | |||||
| createChunk, | |||||
| removeChunk, | |||||
| switchChunk, | |||||
| changeChunkTextMode, | |||||
| available, | |||||
| handleSetAvailable, | |||||
| searchString, | |||||
| handleInputChange, | |||||
| }: IProps) => { | |||||
| const data = useSelectChunkList(); | |||||
| const documentInfo = data?.documentInfo; | |||||
| const knowledgeBaseId = useKnowledgeBaseId(); | |||||
| const [isShowSearchBox, setIsShowSearchBox] = useState(false); | |||||
| const { t } = useTranslate('chunk'); | |||||
| const handleSelectAllCheck = useCallback( | |||||
| (e: any) => { | |||||
| selectAllChunk(e.target.checked); | |||||
| }, | |||||
| [selectAllChunk], | |||||
| ); | |||||
| const handleSearchIconClick = () => { | |||||
| setIsShowSearchBox(true); | |||||
| }; | |||||
| const handleSearchBlur = () => { | |||||
| if (!searchString?.trim()) { | |||||
| setIsShowSearchBox(false); | |||||
| } | |||||
| }; | |||||
| const handleDelete = useCallback(() => { | |||||
| removeChunk(); | |||||
| }, [removeChunk]); | |||||
| const handleEnabledClick = useCallback(() => { | |||||
| switchChunk(1); | |||||
| }, [switchChunk]); | |||||
| const handleDisabledClick = useCallback(() => { | |||||
| switchChunk(0); | |||||
| }, [switchChunk]); | |||||
| const items: MenuProps['items'] = useMemo(() => { | |||||
| return [ | |||||
| { | |||||
| key: '1', | |||||
| label: ( | |||||
| <> | |||||
| <Checkbox onChange={handleSelectAllCheck} checked={checked}> | |||||
| <b>{t('selectAll')}</b> | |||||
| </Checkbox> | |||||
| </> | |||||
| ), | |||||
| }, | |||||
| { type: 'divider' }, | |||||
| { | |||||
| key: '2', | |||||
| label: ( | |||||
| <Space onClick={handleEnabledClick}> | |||||
| <CheckCircleOutlined /> | |||||
| <b>{t('enabledSelected')}</b> | |||||
| </Space> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| key: '3', | |||||
| label: ( | |||||
| <Space onClick={handleDisabledClick}> | |||||
| <CloseCircleOutlined /> | |||||
| <b>{t('disabledSelected')}</b> | |||||
| </Space> | |||||
| ), | |||||
| }, | |||||
| { type: 'divider' }, | |||||
| { | |||||
| key: '4', | |||||
| label: ( | |||||
| <Space onClick={handleDelete}> | |||||
| <DeleteOutlined /> | |||||
| <b>{t('deleteSelected')}</b> | |||||
| </Space> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| }, [ | |||||
| checked, | |||||
| handleSelectAllCheck, | |||||
| handleDelete, | |||||
| handleEnabledClick, | |||||
| handleDisabledClick, | |||||
| t, | |||||
| ]); | |||||
| const content = ( | |||||
| <Menu style={{ width: 200 }} items={items} selectable={false} /> | |||||
| ); | |||||
| const handleFilterChange = (e: RadioChangeEvent) => { | |||||
| selectAllChunk(false); | |||||
| handleSetAvailable(e.target.value); | |||||
| }; | |||||
| const filterContent = ( | |||||
| <Radio.Group onChange={handleFilterChange} value={available}> | |||||
| <Space direction="vertical"> | |||||
| <Radio value={undefined}>{t('all')}</Radio> | |||||
| <Radio value={1}>{t('enabled')}</Radio> | |||||
| <Radio value={0}>{t('disabled')}</Radio> | |||||
| </Space> | |||||
| </Radio.Group> | |||||
| ); | |||||
| return ( | |||||
| <Flex justify="space-between" align="center"> | |||||
| <Space size={'middle'}> | |||||
| <Link | |||||
| to={`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`} | |||||
| > | |||||
| <ArrowLeftOutlined /> | |||||
| </Link> | |||||
| <FilePdfOutlined /> | |||||
| <Text ellipsis={{ tooltip: documentInfo?.name }} style={{ width: 150 }}> | |||||
| {documentInfo?.name} | |||||
| </Text> | |||||
| </Space> | |||||
| <Space> | |||||
| <Segmented | |||||
| options={[ | |||||
| { label: t(ChunkTextMode.Full), value: ChunkTextMode.Full }, | |||||
| { label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse }, | |||||
| ]} | |||||
| onChange={changeChunkTextMode as SegmentedProps['onChange']} | |||||
| /> | |||||
| <Popover content={content} placement="bottom" arrow={false}> | |||||
| <Button> | |||||
| {t('bulk')} | |||||
| <DownOutlined /> | |||||
| </Button> | |||||
| </Popover> | |||||
| {isShowSearchBox ? ( | |||||
| <Input | |||||
| size="middle" | |||||
| placeholder={t('search')} | |||||
| prefix={<SearchOutlined />} | |||||
| allowClear | |||||
| onChange={handleInputChange} | |||||
| onBlur={handleSearchBlur} | |||||
| value={searchString} | |||||
| /> | |||||
| ) : ( | |||||
| <Button icon={<SearchOutlined />} onClick={handleSearchIconClick} /> | |||||
| )} | |||||
| <Popover content={filterContent} placement="bottom" arrow={false}> | |||||
| <Button icon={<FilterIcon />} /> | |||||
| </Popover> | |||||
| <Button | |||||
| icon={<PlusOutlined />} | |||||
| type="primary" | |||||
| onClick={() => createChunk()} | |||||
| /> | |||||
| </Space> | |||||
| </Flex> | |||||
| ); | |||||
| }; | |||||
| export default ChunkToolBar; | 
| import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; | |||||
| import { api_host } from '@/utils/api'; | |||||
| import { useSize } from 'ahooks'; | |||||
| import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types'; | |||||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||||
| export const useDocumentResizeObserver = () => { | |||||
| const [containerWidth, setContainerWidth] = useState<number>(); | |||||
| const [containerRef, setContainerRef] = useState<HTMLElement | null>(null); | |||||
| const size = useSize(containerRef); | |||||
| const onResize = useCallback((width?: number) => { | |||||
| if (width) { | |||||
| setContainerWidth(width); | |||||
| } | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| onResize(size?.width); | |||||
| }, [size?.width, onResize]); | |||||
| return { containerWidth, setContainerRef }; | |||||
| }; | |||||
| function highlightPattern(text: string, pattern: string, pageNumber: number) { | |||||
| if (pageNumber === 2) { | |||||
| return `<mark>${text}</mark>`; | |||||
| } | |||||
| if (text.trim() !== '' && pattern.match(text)) { | |||||
| // return pattern.replace(text, (value) => `<mark>${value}</mark>`); | |||||
| return `<mark>${text}</mark>`; | |||||
| } | |||||
| return text.replace(pattern, (value) => `<mark>${value}</mark>`); | |||||
| } | |||||
| export const useHighlightText = (searchText: string = '') => { | |||||
| const textRenderer: CustomTextRenderer = useCallback( | |||||
| (textItem) => { | |||||
| return highlightPattern(textItem.str, searchText, textItem.pageNumber); | |||||
| }, | |||||
| [searchText], | |||||
| ); | |||||
| return textRenderer; | |||||
| }; | |||||
| export const useGetDocumentUrl = () => { | |||||
| const { documentId } = useGetKnowledgeSearchParams(); | |||||
| const url = useMemo(() => { | |||||
| return `${api_host}/document/get/${documentId}`; | |||||
| }, [documentId]); | |||||
| return url; | |||||
| }; | 
| .documentContainer { | |||||
| width: 100%; | |||||
| height: calc(100vh - 284px); | |||||
| position: relative; | |||||
| :global(.PdfHighlighter) { | |||||
| overflow-x: hidden; | |||||
| } | |||||
| :global(.Highlight--scrolledTo .Highlight__part) { | |||||
| overflow-x: hidden; | |||||
| background-color: rgba(255, 226, 143, 1); | |||||
| } | |||||
| } | 
| import { Skeleton } from 'antd'; | |||||
| import { memo, useEffect, useRef } from 'react'; | |||||
| import { | |||||
| AreaHighlight, | |||||
| Highlight, | |||||
| IHighlight, | |||||
| PdfHighlighter, | |||||
| PdfLoader, | |||||
| Popup, | |||||
| } from 'react-pdf-highlighter'; | |||||
| import { useGetDocumentUrl } from './hooks'; | |||||
| import { useCatchDocumentError } from '@/components/pdf-previewer/hooks'; | |||||
| import FileError from '@/pages/document-viewer/file-error'; | |||||
| import styles from './index.less'; | |||||
| interface IProps { | |||||
| highlights: IHighlight[]; | |||||
| setWidthAndHeight: (width: number, height: number) => void; | |||||
| } | |||||
| const HighlightPopup = ({ | |||||
| comment, | |||||
| }: { | |||||
| comment: { text: string; emoji: string }; | |||||
| }) => | |||||
| comment.text ? ( | |||||
| <div className="Highlight__popup"> | |||||
| {comment.emoji} {comment.text} | |||||
| </div> | |||||
| ) : null; | |||||
| // TODO: merge with DocumentPreviewer | |||||
| const Preview = ({ highlights: state, setWidthAndHeight }: IProps) => { | |||||
| const url = useGetDocumentUrl(); | |||||
| const ref = useRef<(highlight: IHighlight) => void>(() => {}); | |||||
| const error = useCatchDocumentError(url); | |||||
| const resetHash = () => {}; | |||||
| useEffect(() => { | |||||
| if (state.length > 0) { | |||||
| ref?.current(state[0]); | |||||
| } | |||||
| }, [state]); | |||||
| return ( | |||||
| <div className={styles.documentContainer}> | |||||
| <PdfLoader | |||||
| url={url} | |||||
| beforeLoad={<Skeleton active />} | |||||
| workerSrc="/pdfjs-dist/pdf.worker.min.js" | |||||
| errorMessage={<FileError>{error}</FileError>} | |||||
| > | |||||
| {(pdfDocument) => { | |||||
| pdfDocument.getPage(1).then((page) => { | |||||
| const viewport = page.getViewport({ scale: 1 }); | |||||
| const width = viewport.width; | |||||
| const height = viewport.height; | |||||
| setWidthAndHeight(width, height); | |||||
| }); | |||||
| return ( | |||||
| <PdfHighlighter | |||||
| pdfDocument={pdfDocument} | |||||
| enableAreaSelection={(event) => event.altKey} | |||||
| onScrollChange={resetHash} | |||||
| scrollRef={(scrollTo) => { | |||||
| ref.current = scrollTo; | |||||
| }} | |||||
| onSelectionFinished={() => null} | |||||
| highlightTransform={( | |||||
| highlight, | |||||
| index, | |||||
| setTip, | |||||
| hideTip, | |||||
| viewportToScaled, | |||||
| screenshot, | |||||
| isScrolledTo, | |||||
| ) => { | |||||
| const isTextHighlight = !Boolean( | |||||
| highlight.content && highlight.content.image, | |||||
| ); | |||||
| const component = isTextHighlight ? ( | |||||
| <Highlight | |||||
| isScrolledTo={isScrolledTo} | |||||
| position={highlight.position} | |||||
| comment={highlight.comment} | |||||
| /> | |||||
| ) : ( | |||||
| <AreaHighlight | |||||
| isScrolledTo={isScrolledTo} | |||||
| highlight={highlight} | |||||
| onChange={() => {}} | |||||
| /> | |||||
| ); | |||||
| return ( | |||||
| <Popup | |||||
| popupContent={<HighlightPopup {...highlight} />} | |||||
| onMouseOver={(popupContent) => | |||||
| setTip(highlight, () => popupContent) | |||||
| } | |||||
| onMouseOut={hideTip} | |||||
| key={index} | |||||
| > | |||||
| {component} | |||||
| </Popup> | |||||
| ); | |||||
| }} | |||||
| highlights={state} | |||||
| /> | |||||
| ); | |||||
| }} | |||||
| </PdfLoader> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default memo(Preview); | 
| export enum ChunkTextMode { | |||||
| Full = 'full', | |||||
| Ellipse = 'ellipse', | |||||
| } | 
| import { | |||||
| useCreateChunk, | |||||
| useDeleteChunk, | |||||
| useSelectChunkList, | |||||
| } from '@/hooks/chunk-hooks'; | |||||
| import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks'; | |||||
| import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; | |||||
| import { IChunk } from '@/interfaces/database/knowledge'; | |||||
| import { buildChunkHighlights } from '@/utils/document-util'; | |||||
| import { useCallback, useMemo, useState } from 'react'; | |||||
| import { IHighlight } from 'react-pdf-highlighter'; | |||||
| import { ChunkTextMode } from './constant'; | |||||
| export const useHandleChunkCardClick = () => { | |||||
| const [selectedChunkId, setSelectedChunkId] = useState<string>(''); | |||||
| const handleChunkCardClick = useCallback((chunkId: string) => { | |||||
| setSelectedChunkId(chunkId); | |||||
| }, []); | |||||
| return { handleChunkCardClick, selectedChunkId }; | |||||
| }; | |||||
| export const useGetSelectedChunk = (selectedChunkId: string) => { | |||||
| const data = useSelectChunkList(); | |||||
| return ( | |||||
| data?.data?.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk) | |||||
| ); | |||||
| }; | |||||
| export const useGetChunkHighlights = (selectedChunkId: string) => { | |||||
| const [size, setSize] = useState({ width: 849, height: 1200 }); | |||||
| const selectedChunk: IChunk = useGetSelectedChunk(selectedChunkId); | |||||
| const highlights: IHighlight[] = useMemo(() => { | |||||
| return buildChunkHighlights(selectedChunk, size); | |||||
| }, [selectedChunk, size]); | |||||
| const setWidthAndHeight = useCallback((width: number, height: number) => { | |||||
| setSize((pre) => { | |||||
| if (pre.height !== height || pre.width !== width) { | |||||
| return { height, width }; | |||||
| } | |||||
| return pre; | |||||
| }); | |||||
| }, []); | |||||
| return { highlights, setWidthAndHeight }; | |||||
| }; | |||||
| // Switch chunk text to be fully displayed or ellipse | |||||
| export const useChangeChunkTextMode = () => { | |||||
| const [textMode, setTextMode] = useState<ChunkTextMode>(ChunkTextMode.Full); | |||||
| const changeChunkTextMode = useCallback((mode: ChunkTextMode) => { | |||||
| setTextMode(mode); | |||||
| }, []); | |||||
| return { textMode, changeChunkTextMode }; | |||||
| }; | |||||
| export const useDeleteChunkByIds = (): { | |||||
| removeChunk: (chunkIds: string[], documentId: string) => Promise<number>; | |||||
| } => { | |||||
| const { deleteChunk } = useDeleteChunk(); | |||||
| const showDeleteConfirm = useShowDeleteConfirm(); | |||||
| const removeChunk = useCallback( | |||||
| (chunkIds: string[], documentId: string) => () => { | |||||
| return deleteChunk({ chunkIds, doc_id: documentId }); | |||||
| }, | |||||
| [deleteChunk], | |||||
| ); | |||||
| const onRemoveChunk = useCallback( | |||||
| (chunkIds: string[], documentId: string): Promise<number> => { | |||||
| return showDeleteConfirm({ onOk: removeChunk(chunkIds, documentId) }); | |||||
| }, | |||||
| [removeChunk, showDeleteConfirm], | |||||
| ); | |||||
| return { | |||||
| removeChunk: onRemoveChunk, | |||||
| }; | |||||
| }; | |||||
| export const useUpdateChunk = () => { | |||||
| const [chunkId, setChunkId] = useState<string | undefined>(''); | |||||
| const { | |||||
| visible: chunkUpdatingVisible, | |||||
| hideModal: hideChunkUpdatingModal, | |||||
| showModal, | |||||
| } = useSetModalState(); | |||||
| const { createChunk, loading } = useCreateChunk(); | |||||
| const { documentId } = useGetKnowledgeSearchParams(); | |||||
| const onChunkUpdatingOk = useCallback( | |||||
| async (params: IChunk) => { | |||||
| const code = await createChunk({ | |||||
| ...params, | |||||
| doc_id: documentId, | |||||
| chunk_id: chunkId, | |||||
| }); | |||||
| if (code === 0) { | |||||
| hideChunkUpdatingModal(); | |||||
| } | |||||
| }, | |||||
| [createChunk, hideChunkUpdatingModal, chunkId, documentId], | |||||
| ); | |||||
| const handleShowChunkUpdatingModal = useCallback( | |||||
| async (id?: string) => { | |||||
| setChunkId(id); | |||||
| showModal(); | |||||
| }, | |||||
| [showModal], | |||||
| ); | |||||
| return { | |||||
| chunkUpdatingLoading: loading, | |||||
| onChunkUpdatingOk, | |||||
| chunkUpdatingVisible, | |||||
| hideChunkUpdatingModal, | |||||
| showChunkUpdatingModal: handleShowChunkUpdatingModal, | |||||
| chunkId, | |||||
| documentId, | |||||
| }; | |||||
| }; | 
| .chunkPage { | |||||
| padding: 24px; | |||||
| display: flex; | |||||
| // height: calc(100vh - 112px); | |||||
| flex-direction: column; | |||||
| .filter { | |||||
| margin: 10px 0; | |||||
| display: flex; | |||||
| height: 32px; | |||||
| justify-content: space-between; | |||||
| } | |||||
| .pagePdfWrapper { | |||||
| width: 60%; | |||||
| } | |||||
| .pageWrapper { | |||||
| width: 100%; | |||||
| } | |||||
| .pageContent { | |||||
| flex: 1; | |||||
| width: 100%; | |||||
| padding-right: 12px; | |||||
| overflow-y: auto; | |||||
| .spin { | |||||
| min-height: 400px; | |||||
| } | |||||
| } | |||||
| .documentPreview { | |||||
| width: 40%; | |||||
| height: 100%; | |||||
| } | |||||
| .chunkContainer { | |||||
| display: flex; | |||||
| height: calc(100vh - 332px); | |||||
| } | |||||
| .chunkOtherContainer { | |||||
| width: 100%; | |||||
| } | |||||
| .pageFooter { | |||||
| padding-top: 10px; | |||||
| height: 32px; | |||||
| } | |||||
| } | |||||
| .container { | |||||
| height: 100px; | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| justify-content: space-between; | |||||
| .content { | |||||
| display: flex; | |||||
| justify-content: space-between; | |||||
| .context { | |||||
| flex: 1; | |||||
| // width: 207px; | |||||
| height: 88px; | |||||
| overflow: hidden; | |||||
| } | |||||
| } | |||||
| .footer { | |||||
| height: 20px; | |||||
| .text { | |||||
| margin-left: 10px; | |||||
| } | |||||
| } | |||||
| } | |||||
| .card { | |||||
| :global { | |||||
| .ant-card-body { | |||||
| padding: 10px; | |||||
| margin: 0; | |||||
| } | |||||
| margin-bottom: 10px; | |||||
| } | |||||
| cursor: pointer; | |||||
| } | 
| import { useFetchNextChunkList, useSwitchChunk } from '@/hooks/chunk-hooks'; | |||||
| import type { PaginationProps } from 'antd'; | |||||
| import { Divider, Flex, Pagination, Space, Spin, message } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { useCallback, useState } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import ChunkCard from './components/chunk-card'; | |||||
| import CreatingModal from './components/chunk-creating-modal'; | |||||
| import ChunkToolBar from './components/chunk-toolbar'; | |||||
| import DocumentPreview from './components/document-preview/preview'; | |||||
| import { | |||||
| useChangeChunkTextMode, | |||||
| useDeleteChunkByIds, | |||||
| useGetChunkHighlights, | |||||
| useHandleChunkCardClick, | |||||
| useUpdateChunk, | |||||
| } from './hooks'; | |||||
| import styles from './index.less'; | |||||
| const Chunk = () => { | |||||
| const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]); | |||||
| const { removeChunk } = useDeleteChunkByIds(); | |||||
| const { | |||||
| data: { documentInfo, data = [], total }, | |||||
| pagination, | |||||
| loading, | |||||
| searchString, | |||||
| handleInputChange, | |||||
| available, | |||||
| handleSetAvailable, | |||||
| } = useFetchNextChunkList(); | |||||
| const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick(); | |||||
| const isPdf = documentInfo?.type === 'pdf'; | |||||
| const { t } = useTranslation(); | |||||
| const { changeChunkTextMode, textMode } = useChangeChunkTextMode(); | |||||
| const { switchChunk } = useSwitchChunk(); | |||||
| const { | |||||
| chunkUpdatingLoading, | |||||
| onChunkUpdatingOk, | |||||
| showChunkUpdatingModal, | |||||
| hideChunkUpdatingModal, | |||||
| chunkId, | |||||
| chunkUpdatingVisible, | |||||
| documentId, | |||||
| } = useUpdateChunk(); | |||||
| const onPaginationChange: PaginationProps['onShowSizeChange'] = ( | |||||
| page, | |||||
| size, | |||||
| ) => { | |||||
| setSelectedChunkIds([]); | |||||
| pagination.onChange?.(page, size); | |||||
| }; | |||||
| const selectAllChunk = useCallback( | |||||
| (checked: boolean) => { | |||||
| setSelectedChunkIds(checked ? data.map((x) => x.chunk_id) : []); | |||||
| }, | |||||
| [data], | |||||
| ); | |||||
| const handleSingleCheckboxClick = useCallback( | |||||
| (chunkId: string, checked: boolean) => { | |||||
| setSelectedChunkIds((previousIds) => { | |||||
| const idx = previousIds.findIndex((x) => x === chunkId); | |||||
| const nextIds = [...previousIds]; | |||||
| if (checked && idx === -1) { | |||||
| nextIds.push(chunkId); | |||||
| } else if (!checked && idx !== -1) { | |||||
| nextIds.splice(idx, 1); | |||||
| } | |||||
| return nextIds; | |||||
| }); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const showSelectedChunkWarning = useCallback(() => { | |||||
| message.warning(t('message.pleaseSelectChunk')); | |||||
| }, [t]); | |||||
| const handleRemoveChunk = useCallback(async () => { | |||||
| if (selectedChunkIds.length > 0) { | |||||
| const resCode: number = await removeChunk(selectedChunkIds, documentId); | |||||
| if (resCode === 0) { | |||||
| setSelectedChunkIds([]); | |||||
| } | |||||
| } else { | |||||
| showSelectedChunkWarning(); | |||||
| } | |||||
| }, [selectedChunkIds, documentId, removeChunk, showSelectedChunkWarning]); | |||||
| const handleSwitchChunk = useCallback( | |||||
| async (available?: number, chunkIds?: string[]) => { | |||||
| let ids = chunkIds; | |||||
| if (!chunkIds) { | |||||
| ids = selectedChunkIds; | |||||
| if (selectedChunkIds.length === 0) { | |||||
| showSelectedChunkWarning(); | |||||
| return; | |||||
| } | |||||
| } | |||||
| const resCode: number = await switchChunk({ | |||||
| chunk_ids: ids, | |||||
| available_int: available, | |||||
| doc_id: documentId, | |||||
| }); | |||||
| if (!chunkIds && resCode === 0) { | |||||
| } | |||||
| }, | |||||
| [switchChunk, documentId, selectedChunkIds, showSelectedChunkWarning], | |||||
| ); | |||||
| const { highlights, setWidthAndHeight } = | |||||
| useGetChunkHighlights(selectedChunkId); | |||||
| return ( | |||||
| <> | |||||
| <div className={styles.chunkPage}> | |||||
| <ChunkToolBar | |||||
| selectAllChunk={selectAllChunk} | |||||
| createChunk={showChunkUpdatingModal} | |||||
| removeChunk={handleRemoveChunk} | |||||
| checked={selectedChunkIds.length === data.length} | |||||
| switchChunk={handleSwitchChunk} | |||||
| changeChunkTextMode={changeChunkTextMode} | |||||
| searchString={searchString} | |||||
| handleInputChange={handleInputChange} | |||||
| available={available} | |||||
| handleSetAvailable={handleSetAvailable} | |||||
| ></ChunkToolBar> | |||||
| <Divider></Divider> | |||||
| <Flex flex={1} gap={'middle'}> | |||||
| <Flex | |||||
| vertical | |||||
| className={isPdf ? styles.pagePdfWrapper : styles.pageWrapper} | |||||
| > | |||||
| <Spin spinning={loading} className={styles.spin} size="large"> | |||||
| <div className={styles.pageContent}> | |||||
| <Space | |||||
| direction="vertical" | |||||
| size={'middle'} | |||||
| className={classNames(styles.chunkContainer, { | |||||
| [styles.chunkOtherContainer]: !isPdf, | |||||
| })} | |||||
| > | |||||
| {data.map((item) => ( | |||||
| <ChunkCard | |||||
| item={item} | |||||
| key={item.chunk_id} | |||||
| editChunk={showChunkUpdatingModal} | |||||
| checked={selectedChunkIds.some( | |||||
| (x) => x === item.chunk_id, | |||||
| )} | |||||
| handleCheckboxClick={handleSingleCheckboxClick} | |||||
| switchChunk={handleSwitchChunk} | |||||
| clickChunkCard={handleChunkCardClick} | |||||
| selected={item.chunk_id === selectedChunkId} | |||||
| textMode={textMode} | |||||
| ></ChunkCard> | |||||
| ))} | |||||
| </Space> | |||||
| </div> | |||||
| </Spin> | |||||
| <div className={styles.pageFooter}> | |||||
| <Pagination | |||||
| {...pagination} | |||||
| total={total} | |||||
| size={'small'} | |||||
| onChange={onPaginationChange} | |||||
| /> | |||||
| </div> | |||||
| </Flex> | |||||
| {isPdf && ( | |||||
| <section className={styles.documentPreview}> | |||||
| <DocumentPreview | |||||
| highlights={highlights} | |||||
| setWidthAndHeight={setWidthAndHeight} | |||||
| ></DocumentPreview> | |||||
| </section> | |||||
| )} | |||||
| </Flex> | |||||
| </div> | |||||
| {chunkUpdatingVisible && ( | |||||
| <CreatingModal | |||||
| doc_id={documentId} | |||||
| chunkId={chunkId} | |||||
| hideModal={hideChunkUpdatingModal} | |||||
| visible={chunkUpdatingVisible} | |||||
| loading={chunkUpdatingLoading} | |||||
| onOk={onChunkUpdatingOk} | |||||
| parserId={documentInfo.parser_id} | |||||
| /> | |||||
| )} | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Chunk; | 
| export type FormListItem = { | |||||
| frequency: number; | |||||
| tag: string; | |||||
| }; | |||||
| export function transformTagFeaturesArrayToObject( | |||||
| list: Array<FormListItem> = [], | |||||
| ) { | |||||
| return list.reduce<Record<string, number>>((pre, cur) => { | |||||
| pre[cur.tag] = cur.frequency; | |||||
| return pre; | |||||
| }, {}); | |||||
| } | |||||
| export function transformTagFeaturesObjectToArray( | |||||
| object: Record<string, number> = {}, | |||||
| ) { | |||||
| return Object.keys(object).reduce<Array<FormListItem>>((pre, key) => { | |||||
| pre.push({ frequency: object[key], tag: key }); | |||||
| return pre; | |||||
| }, []); | |||||
| } | 
| export default function DatasetWrapper() { | export default function DatasetWrapper() { | ||||
| const { navigateToDatasetList } = useNavigatePage(); | const { navigateToDatasetList } = useNavigatePage(); | ||||
| return ( | return ( | ||||
| <section> | <section> | ||||
| <PageHeader | <PageHeader | 
| import { Button } from '@/components/ui/button'; | |||||
| import { Loader2Icon } from 'lucide-react'; | |||||
| import { useFormContext, useWatch } from 'react-hook-form'; | import { useFormContext, useWatch } from 'react-hook-form'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { DocumentParserType } from '@/constants/knowledge'; | import { DocumentParserType } from '@/constants/knowledge'; | ||||
| import { useUpdateKnowledge } from '@/hooks/knowledge-hooks'; | |||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import { useParams } from 'umi'; | |||||
| import { AudioConfiguration } from './configuration/audio'; | import { AudioConfiguration } from './configuration/audio'; | ||||
| import { BookConfiguration } from './configuration/book'; | import { BookConfiguration } from './configuration/book'; | ||||
| import { EmailConfiguration } from './configuration/email'; | import { EmailConfiguration } from './configuration/email'; | ||||
| export function ChunkMethodForm() { | export function ChunkMethodForm() { | ||||
| const form = useFormContext(); | const form = useFormContext(); | ||||
| const { t } = useTranslation(); | |||||
| // const [submitLoading, setSubmitLoading] = useState(false); // submit button loading | |||||
| const { id: kb_id } = useParams(); | |||||
| const { saveKnowledgeConfiguration, loading: submitLoading } = | |||||
| useUpdateKnowledge(); | |||||
| const finalParserId: DocumentParserType = useWatch({ | const finalParserId: DocumentParserType = useWatch({ | ||||
| control: form.control, | control: form.control, | ||||
| }, [finalParserId]); | }, [finalParserId]); | ||||
| return ( | return ( | ||||
| <section className="overflow-auto max-h-[76vh]"> | |||||
| <ConfigurationComponent></ConfigurationComponent> | |||||
| </section> | |||||
| <> | |||||
| <section className="overflow-auto max-h-[76vh]"> | |||||
| <ConfigurationComponent></ConfigurationComponent> | |||||
| </section> | |||||
| <div className="text-right pt-4"> | |||||
| <Button | |||||
| disabled={submitLoading} | |||||
| onClick={() => { | |||||
| (async () => { | |||||
| try { | |||||
| let beValid = await form.formControl.trigger(); | |||||
| console.log('user chunk form: ', form); | |||||
| if (beValid) { | |||||
| // setSubmitLoading(true); | |||||
| let postData = form.formState.values; | |||||
| delete postData['avatar']; // has submitted in first form general | |||||
| saveKnowledgeConfiguration({ | |||||
| ...postData, | |||||
| kb_id, | |||||
| }); | |||||
| } | |||||
| } catch (e) { | |||||
| console.log(e); | |||||
| } finally { | |||||
| // setSubmitLoading(false); | |||||
| } | |||||
| })(); | |||||
| }} | |||||
| > | |||||
| {submitLoading && <Loader2Icon className="animate-spin" />} | |||||
| {t('common.submit')} | |||||
| </Button> | |||||
| </div> | |||||
| </> | |||||
| ); | ); | ||||
| } | } | 
| import { Button } from '@/components/ui/button'; | |||||
| import { X } from 'lucide-react'; | |||||
| import { useState } from 'react'; | |||||
| import CategoryPanel from './category-panel'; | |||||
| export default ({ | |||||
| tab = 'generalForm', | |||||
| parserId, | |||||
| }: { | |||||
| tab: 'generalForm' | 'chunkMethodForm'; | |||||
| parserId: string; | |||||
| }) => { | |||||
| const [visible, setVisible] = useState(true); | |||||
| return ( | |||||
| <div | |||||
| style={{ | |||||
| display: tab === 'chunkMethodForm' ? 'block' : 'none', | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| variant="outline" | |||||
| onClick={() => { | |||||
| setVisible(!visible); | |||||
| }} | |||||
| > | |||||
| Learn More | |||||
| </Button> | |||||
| <div | |||||
| className="bg-[#FFF]/10 p-[20px] rounded-[12px] mt-[10px] relative" | |||||
| style={{ display: visible ? 'block' : 'none' }} | |||||
| > | |||||
| <CategoryPanel chunkMethod={parserId}></CategoryPanel> | |||||
| <div | |||||
| className="absolute right-1 top-1 cursor-pointer hover:text-[#FFF]/30" | |||||
| onClick={() => { | |||||
| setVisible(false); | |||||
| }} | |||||
| > | |||||
| <X /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | 
| control={form.control} | control={form.control} | ||||
| name={'parser_id'} | name={'parser_id'} | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel tooltip={t('chunkMethodTip')}> | |||||
| {t('chunkMethod')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <RAGFlowSelect | |||||
| {...field} | |||||
| options={parserList} | |||||
| placeholder={t('chunkMethodPlaceholder')} | |||||
| // onChange={handleChunkMethodSelectChange} | |||||
| /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={t('chunkMethodTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('chunkMethod')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4 "> | |||||
| <FormControl> | |||||
| <RAGFlowSelect | |||||
| {...field} | |||||
| options={parserList} | |||||
| placeholder={t('chunkMethodPlaceholder')} | |||||
| // onChange={handleChunkMethodSelectChange} | |||||
| /> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | ||||
| control={form.control} | control={form.control} | ||||
| name={'embd_id'} | name={'embd_id'} | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel tooltip={t('embeddingModelTip')}> | |||||
| {t('embeddingModel')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <RAGFlowSelect | |||||
| {...field} | |||||
| options={embeddingModelOptions} | |||||
| disabled={disabled} | |||||
| placeholder={t('embeddingModelPlaceholder')} | |||||
| /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| tooltip={t('embeddingModelTip')} | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| > | |||||
| {t('embeddingModel')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <RAGFlowSelect | |||||
| {...field} | |||||
| options={embeddingModelOptions} | |||||
| disabled={disabled} | |||||
| placeholder={t('embeddingModelPlaceholder')} | |||||
| /> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | 
| description: z.string().min(2, { | description: z.string().min(2, { | ||||
| message: 'Username must be at least 2 characters.', | message: 'Username must be at least 2 characters.', | ||||
| }), | }), | ||||
| avatar: z.instanceof(File), | |||||
| // avatar: z.instanceof(File), | |||||
| avatar: z.any().nullish(), | |||||
| permission: z.string(), | permission: z.string(), | ||||
| parser_id: z.string(), | parser_id: z.string(), | ||||
| embd_id: z.string(), | embd_id: z.string(), | ||||
| parser_config: z.object({ | |||||
| layout_recognize: z.string(), | |||||
| chunk_token_num: z.number(), | |||||
| delimiter: z.string(), | |||||
| auto_keywords: z.number(), | |||||
| auto_questions: z.number(), | |||||
| html4excel: z.boolean(), | |||||
| tag_kb_ids: z.array(z.string()), | |||||
| topn_tags: z.number(), | |||||
| raptor: z.object({ | |||||
| use_raptor: z.boolean(), | |||||
| prompt: z.string(), | |||||
| max_token: z.number(), | |||||
| threshold: z.number(), | |||||
| max_cluster: z.number(), | |||||
| random_seed: z.number(), | |||||
| }), | |||||
| graphrag: z.object({ | |||||
| use_graphrag: z.boolean(), | |||||
| entity_types: z.array(z.string()), | |||||
| method: z.string(), | |||||
| resolution: z.boolean(), | |||||
| community: z.boolean(), | |||||
| }), | |||||
| }), | |||||
| parser_config: z | |||||
| .object({ | |||||
| layout_recognize: z.string(), | |||||
| chunk_token_num: z.number(), | |||||
| delimiter: z.string(), | |||||
| auto_keywords: z.number().optional(), | |||||
| auto_questions: z.number().optional(), | |||||
| html4excel: z.boolean(), | |||||
| tag_kb_ids: z.array(z.string()).nullish(), | |||||
| topn_tags: z.number().optional(), | |||||
| raptor: z | |||||
| .object({ | |||||
| use_raptor: z.boolean().optional(), | |||||
| prompt: z.string().optional(), | |||||
| max_token: z.number().optional(), | |||||
| threshold: z.number().optional(), | |||||
| max_cluster: z.number().optional(), | |||||
| random_seed: z.number().optional(), | |||||
| }) | |||||
| .refine( | |||||
| (data) => { | |||||
| if (data.use_raptor && !data.prompt) { | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| }, | |||||
| { | |||||
| message: 'Prompt is required', | |||||
| path: ['prompt'], | |||||
| }, | |||||
| ), | |||||
| graphrag: z | |||||
| .object({ | |||||
| use_graphrag: z.boolean().optional(), | |||||
| entity_types: z.array(z.string()).optional(), | |||||
| method: z.string().optional(), | |||||
| resolution: z.boolean().optional(), | |||||
| community: z.boolean().optional(), | |||||
| }) | |||||
| .refine( | |||||
| (data) => { | |||||
| if ( | |||||
| data.use_graphrag && | |||||
| (!data.entity_types || data.entity_types.length === 0) | |||||
| ) { | |||||
| return false; | |||||
| } | |||||
| return true; | |||||
| }, | |||||
| { | |||||
| message: 'Please enter Entity types', | |||||
| path: ['entity_types'], | |||||
| }, | |||||
| ), | |||||
| }) | |||||
| .optional(), | |||||
| pagerank: z.number(), | pagerank: z.number(), | ||||
| // icon: z.array(z.instanceof(File)), | // icon: z.array(z.instanceof(File)), | ||||
| }); | }); | 
| import { FileUploader } from '@/components/file-uploader'; | |||||
| import { FormContainer } from '@/components/form-container'; | import { FormContainer } from '@/components/form-container'; | ||||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { | import { | ||||
| FormControl, | FormControl, | ||||
| FormField, | FormField, | ||||
| } from '@/components/ui/form'; | } from '@/components/ui/form'; | ||||
| import { Input } from '@/components/ui/input'; | import { Input } from '@/components/ui/input'; | ||||
| import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; | import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; | ||||
| import { useUpdateKnowledge } from '@/hooks/knowledge-hooks'; | |||||
| import { transformFile2Base64 } from '@/utils/file-util'; | |||||
| import { Loader2Icon, Pencil, Upload } from 'lucide-react'; | |||||
| import { useEffect, useMemo, useState } from 'react'; | |||||
| import { useFormContext } from 'react-hook-form'; | import { useFormContext } from 'react-hook-form'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { useParams } from 'umi'; | |||||
| export function GeneralForm() { | export function GeneralForm() { | ||||
| const form = useFormContext(); | const form = useFormContext(); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const [avatarFile, setAvatarFile] = useState<File | null>(null); | |||||
| const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 | |||||
| // const [submitLoading, setSubmitLoading] = useState(false); // submit button loading | |||||
| return ( | |||||
| <FormContainer className="space-y-2 p-10"> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="name" | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>{t('knowledgeConfiguration.name')}</FormLabel> | |||||
| <FormControl> | |||||
| <Input {...field}></Input> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="description" | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>{t('knowledgeConfiguration.description')}</FormLabel> | |||||
| <FormControl> | |||||
| <Input {...field}></Input> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="avatar" | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>{t('knowledgeConfiguration.photo')}</FormLabel> | |||||
| <FormControl> | |||||
| <FileUploader | |||||
| value={field.value} | |||||
| onValueChange={field.onChange} | |||||
| maxFileCount={1} | |||||
| maxSize={4 * 1024 * 1024} | |||||
| // progresses={progresses} | |||||
| // pass the onUpload function here for direct upload | |||||
| // onUpload={uploadFiles} | |||||
| // disabled={isUploading} | |||||
| /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| const { saveKnowledgeConfiguration, loading: submitLoading } = | |||||
| useUpdateKnowledge(); | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="type" | |||||
| render={({ field }) => ( | |||||
| <FormItem className="space-y-3"> | |||||
| <FormLabel tooltip={t('knowledgeConfiguration.permissionsTip')}> | |||||
| {t('knowledgeConfiguration.permissions')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <RadioGroup | |||||
| onValueChange={field.onChange} | |||||
| defaultValue={field.value} | |||||
| className="flex flex-col space-y-1" | |||||
| > | |||||
| <FormItem className="flex items-center space-x-3 space-y-0"> | |||||
| <FormControl> | |||||
| <RadioGroupItem value="me" /> | |||||
| </FormControl> | |||||
| <FormLabel className="font-normal"> | |||||
| {t('knowledgeConfiguration.me')} | |||||
| const defaultValues = useMemo( | |||||
| () => form.formState.defaultValues ?? {}, | |||||
| [form.formState.defaultValues], | |||||
| ); | |||||
| const parser_id = defaultValues['parser_id']; | |||||
| const { id: kb_id } = useParams(); | |||||
| // init avatar file if it exists in defaultValues | |||||
| useEffect(() => { | |||||
| if (!avatarFile) { | |||||
| let avatarList = defaultValues['avatar']; | |||||
| if (avatarList && avatarList.length > 0) { | |||||
| setAvatarBase64Str(avatarList[0].thumbUrl); | |||||
| } | |||||
| } | |||||
| }, [avatarFile, defaultValues]); | |||||
| // input[type=file] on change event, get img base64 | |||||
| useEffect(() => { | |||||
| if (avatarFile) { | |||||
| (async () => { | |||||
| // make use of img compression transformFile2Base64 | |||||
| setAvatarBase64Str(await transformFile2Base64(avatarFile)); | |||||
| })(); | |||||
| } | |||||
| }, [avatarFile]); | |||||
| return ( | |||||
| <> | |||||
| <FormContainer className="space-y-10 p-10"> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="name" | |||||
| render={({ field }) => ( | |||||
| <FormItem className="items-center space-y-0"> | |||||
| <div className="flex"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| <span className="text-red-600">*</span> | |||||
| {t('common.name')} | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/4"> | |||||
| <Input {...field}></Input> | |||||
| </FormControl> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="description" | |||||
| render={({ field }) => { | |||||
| // null initialize empty string | |||||
| if (typeof field.value === 'object' && !field.value) { | |||||
| form.setValue('description', ' '); | |||||
| } | |||||
| return ( | |||||
| <FormItem className="items-center space-y-0"> | |||||
| <div className="flex"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| {t('flow.description')} | |||||
| </FormLabel> | </FormLabel> | ||||
| </FormItem> | |||||
| <FormItem className="flex items-center space-x-3 space-y-0"> | |||||
| <FormControl> | |||||
| <RadioGroupItem value="team" /> | |||||
| <FormControl className="w-3/4"> | |||||
| <Input {...field}></Input> | |||||
| </FormControl> | </FormControl> | ||||
| <FormLabel className="font-normal"> | |||||
| {t('knowledgeConfiguration.team')} | |||||
| </FormLabel> | |||||
| </FormItem> | |||||
| </RadioGroup> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| </FormContainer> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="avatar" | |||||
| render={({ field }) => ( | |||||
| <FormItem className="items-center space-y-0"> | |||||
| <div className="flex"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| {t('setting.avatar')} | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/4"> | |||||
| <> | |||||
| <div className="relative group"> | |||||
| {!avatarBase64Str ? ( | |||||
| <div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md"> | |||||
| <div className="flex flex-col items-center"> | |||||
| <Upload /> | |||||
| <p>Upload</p> | |||||
| </div> | |||||
| </div> | |||||
| ) : ( | |||||
| <div className="w-[64px] h-[64px] relative grid place-content-center"> | |||||
| <Avatar className="w-[64px] h-[64px]"> | |||||
| <AvatarImage | |||||
| className=" block" | |||||
| src={avatarBase64Str} | |||||
| alt="" | |||||
| /> | |||||
| <AvatarFallback></AvatarFallback> | |||||
| </Avatar> | |||||
| <div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60"> | |||||
| <Pencil | |||||
| size={20} | |||||
| className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| <Input | |||||
| placeholder="" | |||||
| // {...field} | |||||
| type="file" | |||||
| title="" | |||||
| accept="image/*" | |||||
| className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" | |||||
| onChange={(ev) => { | |||||
| const file = ev.target?.files?.[0]; | |||||
| if ( | |||||
| /\.(jpg|jpeg|png|webp|bmp)$/i.test(file?.name ?? '') | |||||
| ) { | |||||
| setAvatarFile(file!); | |||||
| } | |||||
| ev.target.value = ''; | |||||
| }} | |||||
| /> | |||||
| </div> | |||||
| </> | |||||
| </FormControl> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="permission" | |||||
| render={({ field }) => ( | |||||
| <FormItem className="flex items-center space-y-0"> | |||||
| <FormLabel | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| tooltip={t('knowledgeConfiguration.permissionsTip')} | |||||
| > | |||||
| {t('knowledgeConfiguration.permissions')} | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/4"> | |||||
| <RadioGroup | |||||
| onValueChange={field.onChange} | |||||
| value={field.value} | |||||
| className="flex space-y-1 gap-5" | |||||
| > | |||||
| <FormItem className="flex items-center space-x-1 space-y-0"> | |||||
| <FormControl> | |||||
| <RadioGroupItem value="me" /> | |||||
| </FormControl> | |||||
| <FormLabel className="font-normal"> | |||||
| {t('knowledgeConfiguration.me')} | |||||
| </FormLabel> | |||||
| </FormItem> | |||||
| <FormItem className="flex items-center space-x-1 space-y-0"> | |||||
| <FormControl> | |||||
| <RadioGroupItem value="team" /> | |||||
| </FormControl> | |||||
| <FormLabel className="font-normal"> | |||||
| {t('knowledgeConfiguration.team')} | |||||
| </FormLabel> | |||||
| </FormItem> | |||||
| </RadioGroup> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| </FormContainer> | |||||
| <div className="text-right pt-4"> | |||||
| <Button | |||||
| type="button" | |||||
| disabled={submitLoading} | |||||
| onClick={() => { | |||||
| // console.log('form.formControl: ', form.formState.values); | |||||
| (async () => { | |||||
| let isValidate = await form.formControl.trigger('name'); | |||||
| // console.log(isValidate); | |||||
| const { name, description, permission } = form.formState.values; | |||||
| const avatar = avatarBase64Str; | |||||
| if (isValidate) { | |||||
| saveKnowledgeConfiguration({ | |||||
| kb_id, | |||||
| parser_id, | |||||
| name, | |||||
| description, | |||||
| permission, | |||||
| avatar, | |||||
| }); | |||||
| } | |||||
| })(); | |||||
| }} | |||||
| > | |||||
| {submitLoading && <Loader2Icon className="animate-spin" />} | |||||
| {t('common.submit')} | |||||
| </Button> | |||||
| </div> | |||||
| </> | |||||
| ); | ); | ||||
| } | } | 
| import { ButtonLoading } from '@/components/ui/button'; | |||||
| import { Form } from '@/components/ui/form'; | import { Form } from '@/components/ui/form'; | ||||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; | |||||
| import { | |||||
| Tabs, | |||||
| TabsContent, | |||||
| TabsList, | |||||
| TabsTrigger, | |||||
| } from '@/components/ui/tabs-underlined'; | |||||
| import { DocumentParserType } from '@/constants/knowledge'; | import { DocumentParserType } from '@/constants/knowledge'; | ||||
| import { zodResolver } from '@hookform/resolvers/zod'; | import { zodResolver } from '@hookform/resolvers/zod'; | ||||
| import { useState } from 'react'; | |||||
| import { useForm, useWatch } from 'react-hook-form'; | import { useForm, useWatch } from 'react-hook-form'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { z } from 'zod'; | import { z } from 'zod'; | ||||
| import { TopTitle } from '../dataset-title'; | import { TopTitle } from '../dataset-title'; | ||||
| import CategoryPanel from './category-panel'; | |||||
| import { ChunkMethodForm } from './chunk-method-form'; | import { ChunkMethodForm } from './chunk-method-form'; | ||||
| import ChunkMethodLearnMore from './chunk-method-learn-more'; | |||||
| import { formSchema } from './form-schema'; | import { formSchema } from './form-schema'; | ||||
| import { GeneralForm } from './general-form'; | import { GeneralForm } from './general-form'; | ||||
| import { useFetchKnowledgeConfigurationOnMount } from './hooks'; | import { useFetchKnowledgeConfigurationOnMount } from './hooks'; | ||||
| } | } | ||||
| export default function DatasetSettings() { | export default function DatasetSettings() { | ||||
| const { t } = useTranslation(); | |||||
| const form = useForm<z.infer<typeof formSchema>>({ | const form = useForm<z.infer<typeof formSchema>>({ | ||||
| resolver: zodResolver(formSchema), | resolver: zodResolver(formSchema), | ||||
| defaultValues: { | defaultValues: { | ||||
| useFetchKnowledgeConfigurationOnMount(form); | useFetchKnowledgeConfigurationOnMount(form); | ||||
| const [currentTab, setCurrentTab] = useState< | |||||
| 'generalForm' | 'chunkMethodForm' | |||||
| >('generalForm'); // currnet Tab state | |||||
| const parserId = useWatch({ | const parserId = useWatch({ | ||||
| control: form.control, | control: form.control, | ||||
| name: 'parser_id', | name: 'parser_id', | ||||
| return ( | return ( | ||||
| <section className="p-5 "> | <section className="p-5 "> | ||||
| <TopTitle | <TopTitle | ||||
| title={'Configuration'} | |||||
| description={` Update your knowledge base configuration here, particularly the chunk | |||||
| method.`} | |||||
| title={t('knowledgeDetails.configuration')} | |||||
| description={t('knowledgeConfiguration.titleDescription')} | |||||
| ></TopTitle> | ></TopTitle> | ||||
| <div className="flex gap-14"> | <div className="flex gap-14"> | ||||
| <Form {...form}> | <Form {...form}> | ||||
| <form | <form | ||||
| onSubmit={form.handleSubmit(onSubmit)} | onSubmit={form.handleSubmit(onSubmit)} | ||||
| className="space-y-6 basis-full" | |||||
| className="space-y-6 basis-full min-w-[1000px] max-w-[1000px]" | |||||
| > | > | ||||
| <Tabs defaultValue="account"> | |||||
| <TabsList className="grid w-full grid-cols-2"> | |||||
| <TabsTrigger value="account">Account</TabsTrigger> | |||||
| <TabsTrigger value="password">Password</TabsTrigger> | |||||
| <Tabs | |||||
| defaultValue="generalForm" | |||||
| onValueChange={(val) => { | |||||
| setCurrentTab(val); | |||||
| }} | |||||
| > | |||||
| <TabsList className="grid w-full bg-background grid-cols-2 rounded-none bg-[#161618]"> | |||||
| <TabsTrigger | |||||
| value="generalForm" | |||||
| className="group bg-transparent p-0 !border-transparent" | |||||
| > | |||||
| <div className="flex w-full h-full justify-center items-center bg-[#161618]"> | |||||
| <span className="h-full group-data-[state=active]:border-b-2 border-white "> | |||||
| General | |||||
| </span> | |||||
| </div> | |||||
| </TabsTrigger> | |||||
| <TabsTrigger | |||||
| value="chunkMethodForm" | |||||
| className="group bg-transparent p-0 !border-transparent" | |||||
| > | |||||
| <div className="flex w-full h-full justify-center items-center bg-[#161618]"> | |||||
| <span className="h-full group-data-[state=active]:border-b-2 border-white "> | |||||
| Chunk Method | |||||
| </span> | |||||
| </div> | |||||
| </TabsTrigger> | |||||
| </TabsList> | </TabsList> | ||||
| <TabsContent value="account"> | |||||
| <TabsContent value="generalForm"> | |||||
| <GeneralForm></GeneralForm> | <GeneralForm></GeneralForm> | ||||
| </TabsContent> | </TabsContent> | ||||
| <TabsContent value="password"> | |||||
| <TabsContent value="chunkMethodForm"> | |||||
| <ChunkMethodForm></ChunkMethodForm> | <ChunkMethodForm></ChunkMethodForm> | ||||
| </TabsContent> | </TabsContent> | ||||
| </Tabs> | </Tabs> | ||||
| <div className="text-right"> | |||||
| {/* <div className="text-right"> | |||||
| <ButtonLoading type="submit">Submit</ButtonLoading> | <ButtonLoading type="submit">Submit</ButtonLoading> | ||||
| </div> | |||||
| </div> */} | |||||
| </form> | </form> | ||||
| </Form> | </Form> | ||||
| <CategoryPanel chunkMethod={parserId}></CategoryPanel> | |||||
| <ChunkMethodLearnMore tab={currentTab} parserId={parserId} /> | |||||
| {/* <div | |||||
| style={{ | |||||
| display: currentTab === 'chunkMethodForm' ? 'block' : 'none', | |||||
| }} | |||||
| > | |||||
| <Button variant="outline">Learn More</Button> | |||||
| <div className="bg-[#FFF]/10 p-[20px] rounded-[12px] mt-[10px]"> | |||||
| <CategoryPanel chunkMethod={parserId}></CategoryPanel> | |||||
| </div> | |||||
| </div> */} | |||||
| </div> | </div> | ||||
| </section> | </section> | ||||
| ); | ); | 
| control={form.control} | control={form.control} | ||||
| name="parser_config.tag_kb_ids" | name="parser_config.tag_kb_ids" | ||||
| render={({ field }) => ( | render={({ field }) => ( | ||||
| <FormItem> | |||||
| <FormLabel | |||||
| tooltip={ | |||||
| <div | |||||
| dangerouslySetInnerHTML={{ | |||||
| __html: DOMPurify.sanitize( | |||||
| t('knowledgeConfiguration.tagSetTip'), | |||||
| ), | |||||
| }} | |||||
| ></div> | |||||
| } | |||||
| > | |||||
| {t('knowledgeConfiguration.tagSet')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <MultiSelect | |||||
| options={knowledgeOptions} | |||||
| onValueChange={field.onChange} | |||||
| placeholder={t('chat.knowledgeBasesMessage')} | |||||
| variant="inverted" | |||||
| maxCount={0} | |||||
| {...field} | |||||
| /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex items-center"> | |||||
| <FormLabel | |||||
| className="text-sm text-muted-foreground whitespace-nowrap w-1/4" | |||||
| tooltip={ | |||||
| <div | |||||
| dangerouslySetInnerHTML={{ | |||||
| __html: DOMPurify.sanitize( | |||||
| t('knowledgeConfiguration.tagSetTip'), | |||||
| ), | |||||
| }} | |||||
| ></div> | |||||
| } | |||||
| > | |||||
| {t('knowledgeConfiguration.tagSet')} | |||||
| </FormLabel> | |||||
| <div className="w-3/4"> | |||||
| <FormControl> | |||||
| <MultiSelect | |||||
| options={knowledgeOptions} | |||||
| onValueChange={field.onChange} | |||||
| placeholder={t('chat.knowledgeBasesMessage')} | |||||
| variant="inverted" | |||||
| maxCount={0} | |||||
| {...field} | |||||
| /> | |||||
| </FormControl> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | 
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { useHandleMenuClick } from './hooks'; | import { useHandleMenuClick } from './hooks'; | ||||
| export function SideBar() { | |||||
| type PropType = { | |||||
| refreshCount?: number; | |||||
| }; | |||||
| export function SideBar({ refreshCount }: PropType) { | |||||
| const pathName = useSecondPathName(); | const pathName = useSecondPathName(); | ||||
| const { handleMenuClick } = useHandleMenuClick(); | const { handleMenuClick } = useHandleMenuClick(); | ||||
| const { data } = useFetchKnowledgeBaseConfiguration(); | |||||
| // refreshCount: be for avatar img sync update on top left | |||||
| const { data } = useFetchKnowledgeBaseConfiguration(refreshCount); | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const items = useMemo(() => { | const items = useMemo(() => { | 
| import { Button } from '@/components/ui/button'; | |||||
| import { useTestRetrieval } from '@/hooks/use-knowledge-request'; | import { useTestRetrieval } from '@/hooks/use-knowledge-request'; | ||||
| import { Plus } from 'lucide-react'; | |||||
| import { useCallback, useState } from 'react'; | import { useCallback, useState } from 'react'; | ||||
| import { TopTitle } from '../dataset-title'; | import { TopTitle } from '../dataset-title'; | ||||
| import TestingForm from './testing-form'; | import TestingForm from './testing-form'; | ||||
| description={` Update your knowledge base configuration here, particularly the chunk | description={` Update your knowledge base configuration here, particularly the chunk | ||||
| method.`} | method.`} | ||||
| ></TopTitle> | ></TopTitle> | ||||
| <Button>Save as Preset</Button> | |||||
| {/* <Button>Save as Preset</Button> */} | |||||
| </section> | </section> | ||||
| {count === 1 ? ( | {count === 1 ? ( | ||||
| <section className="flex divide-x h-full"> | <section className="flex divide-x h-full"> | ||||
| <span className="text-text-title font-semibold text-2xl"> | <span className="text-text-title font-semibold text-2xl"> | ||||
| Test setting | Test setting | ||||
| </span> | </span> | ||||
| <Button variant={'outline'} onClick={addCount}> | |||||
| {/* <Button variant={'outline'} onClick={addCount}> | |||||
| <Plus /> Add New Test | <Plus /> Add New Test | ||||
| </Button> | |||||
| </Button> */} | |||||
| </div> | </div> | ||||
| <TestingForm | <TestingForm | ||||
| loading={loading} | loading={loading} | 
| import { useForm, useWatch } from 'react-hook-form'; | import { useForm, useWatch } from 'react-hook-form'; | ||||
| import { z } from 'zod'; | import { z } from 'zod'; | ||||
| import { CrossLanguageItem } from '@/components/cross-language-item-ui'; | |||||
| import { FormContainer } from '@/components/form-container'; | import { FormContainer } from '@/components/form-container'; | ||||
| import { | import { | ||||
| initialTopKValue, | initialTopKValue, | ||||
| import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item'; | import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item'; | ||||
| import { useTestRetrieval } from '@/hooks/use-knowledge-request'; | import { useTestRetrieval } from '@/hooks/use-knowledge-request'; | ||||
| import { trim } from 'lodash'; | import { trim } from 'lodash'; | ||||
| import { useEffect } from 'react'; | |||||
| import { CirclePlay } from 'lucide-react'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| type TestingFormProps = Pick< | type TestingFormProps = Pick< | ||||
| setValues, | setValues, | ||||
| }: TestingFormProps) { | }: TestingFormProps) { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const [cross_languages, setCrossLangArr] = useState<string[]>([]); | |||||
| const formSchema = z.object({ | const formSchema = z.object({ | ||||
| question: z.string().min(1, { | question: z.string().min(1, { | ||||
| const values = useWatch({ control: form.control }); | const values = useWatch({ control: form.control }); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| setValues(values as Required<z.infer<typeof formSchema>>); | |||||
| }, [setValues, values]); | |||||
| // setValues(values as Required<z.infer<typeof formSchema>>); | |||||
| setValues({ ...values, cross_languages }); | |||||
| }, [setValues, values, cross_languages]); | |||||
| function onSubmit() { | function onSubmit() { | ||||
| refetch(); | refetch(); | ||||
| ></SimilaritySliderFormField> | ></SimilaritySliderFormField> | ||||
| <RerankFormFields></RerankFormFields> | <RerankFormFields></RerankFormFields> | ||||
| <UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField> | <UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField> | ||||
| <CrossLanguageItem | |||||
| name={'cross_languages'} | |||||
| onChange={(valArr) => { | |||||
| setCrossLangArr(valArr); | |||||
| }} | |||||
| ></CrossLanguageItem> | |||||
| </FormContainer> | </FormContainer> | ||||
| <FormField | <FormField | ||||
| control={form.control} | control={form.control} | ||||
| </FormItem> | </FormItem> | ||||
| )} | )} | ||||
| /> | /> | ||||
| <ButtonLoading | |||||
| type="submit" | |||||
| disabled={!!!trim(question)} | |||||
| loading={loading} | |||||
| > | |||||
| {t('knowledgeDetails.testingLabel')} | |||||
| </ButtonLoading> | |||||
| <div className="flex justify-end"> | |||||
| <ButtonLoading | |||||
| type="submit" | |||||
| disabled={!!!trim(question)} | |||||
| loading={loading} | |||||
| > | |||||
| {!loading && <CirclePlay />} | |||||
| {t('knowledgeDetails.testingLabel')} | |||||
| </ButtonLoading> | |||||
| </div> | |||||
| </form> | </form> | ||||
| </Form> | </Form> | ||||
| ); | ); | 
| import PasswordInput from '@/components/password-input'; | |||||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | ||||
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { | |||||
| Form, | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from '@/components/ui/form'; | |||||
| import { Input } from '@/components/ui/input'; | import { Input } from '@/components/ui/input'; | ||||
| import { | import { | ||||
| Select, | Select, | ||||
| SelectTrigger, | SelectTrigger, | ||||
| SelectValue, | SelectValue, | ||||
| } from '@/components/ui/select'; | } from '@/components/ui/select'; | ||||
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import { useFetchUserInfo, useSaveSetting } from '@/hooks/user-setting-hooks'; | |||||
| import { TimezoneList } from '@/pages/user-setting/constants'; | |||||
| import { rsaPsw } from '@/utils'; | |||||
| import { transformFile2Base64 } from '@/utils/file-util'; | |||||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||||
| import { TFunction } from 'i18next'; | |||||
| import { Loader2Icon, Pencil, Upload } from 'lucide-react'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import { useForm } from 'react-hook-form'; | |||||
| import { z } from 'zod'; | |||||
| function defineSchema(t: TFunction<'translation', string>) { | |||||
| return z | |||||
| .object({ | |||||
| userName: z | |||||
| .string() | |||||
| .min(1, { | |||||
| message: t('usernameMessage'), | |||||
| }) | |||||
| .trim(), | |||||
| avatarUrl: z.string().trim(), | |||||
| timeZone: z | |||||
| .string() | |||||
| .trim() | |||||
| .min(1, { | |||||
| message: t('timezonePlaceholder'), | |||||
| }), | |||||
| email: z | |||||
| .string({ | |||||
| required_error: 'Please select an email to display.', | |||||
| }) | |||||
| .trim() | |||||
| .regex( | |||||
| /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/, | |||||
| { | |||||
| message: 'Enter a valid email address.', | |||||
| }, | |||||
| ), | |||||
| currPasswd: z | |||||
| .string() | |||||
| .trim() | |||||
| .min(1, { | |||||
| message: t('currentPasswordMessage'), | |||||
| }), | |||||
| newPasswd: z | |||||
| .string() | |||||
| .trim() | |||||
| .min(8, { | |||||
| message: t('confirmPasswordMessage'), | |||||
| }), | |||||
| confirmPasswd: z | |||||
| .string() | |||||
| .trim() | |||||
| .min(8, { | |||||
| message: t('newPasswordDescription'), | |||||
| }), | |||||
| }) | |||||
| .refine((data) => data.newPasswd === data.confirmPasswd, { | |||||
| message: t('confirmPasswordNonMatchMessage'), | |||||
| path: ['confirmPasswd'], | |||||
| }); | |||||
| } | |||||
| export default function Profile() { | export default function Profile() { | ||||
| return ( | |||||
| <section className="p-8"> | |||||
| <h1 className="text-3xl font-bold mb-6">User profile</h1> | |||||
| <Avatar className="w-[120px] h-[120px] mb-6"> | |||||
| <AvatarImage | |||||
| src={ | |||||
| 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg' | |||||
| } | |||||
| alt="Profile" | |||||
| /> | |||||
| <AvatarFallback>YW</AvatarFallback> | |||||
| </Avatar> | |||||
| const [avatarFile, setAvatarFile] = useState<File | null>(null); | |||||
| const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64 | |||||
| const { data: userInfo } = useFetchUserInfo(); | |||||
| const { saveSetting, loading: submitLoading } = useSaveSetting(); | |||||
| <div className="space-y-6 max-w-[600px]"> | |||||
| <div className="space-y-2"> | |||||
| <label className="text-sm text-muted-foreground">User name</label> | |||||
| <Input defaultValue="username" /> | |||||
| </div> | |||||
| const { t } = useTranslate('setting'); | |||||
| const FormSchema = defineSchema(t); | |||||
| <div className="space-y-2"> | |||||
| <label className="text-sm text-muted-foreground">Email</label> | |||||
| <Input defaultValue="address@example.com" /> | |||||
| </div> | |||||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||||
| resolver: zodResolver(FormSchema), | |||||
| defaultValues: { | |||||
| userName: '', | |||||
| avatarUrl: '', | |||||
| timeZone: '', | |||||
| email: '', | |||||
| currPasswd: '', | |||||
| newPasswd: '', | |||||
| confirmPasswd: '', | |||||
| }, | |||||
| }); | |||||
| <div className="space-y-2"> | |||||
| <label className="text-sm text-muted-foreground">Language</label> | |||||
| <Select defaultValue="english"> | |||||
| <SelectTrigger> | |||||
| <SelectValue /> | |||||
| </SelectTrigger> | |||||
| <SelectContent> | |||||
| <SelectItem value="english">English</SelectItem> | |||||
| </SelectContent> | |||||
| </Select> | |||||
| </div> | |||||
| useEffect(() => { | |||||
| // init user info when mounted | |||||
| form.setValue('email', userInfo?.email); // email | |||||
| form.setValue('userName', userInfo?.nickname); // nickname | |||||
| form.setValue('timeZone', userInfo?.timezone); // time zone | |||||
| form.setValue('currPasswd', ''); // current password | |||||
| setAvatarBase64Str(userInfo?.avatar ?? ''); | |||||
| }, [userInfo]); | |||||
| <div className="space-y-2"> | |||||
| <label className="text-sm text-muted-foreground">Timezone</label> | |||||
| <Select defaultValue="utc9"> | |||||
| <SelectTrigger> | |||||
| <SelectValue /> | |||||
| </SelectTrigger> | |||||
| <SelectContent> | |||||
| <SelectItem value="utc9">UTC+9 Asia/Shanghai</SelectItem> | |||||
| </SelectContent> | |||||
| </Select> | |||||
| </div> | |||||
| useEffect(() => { | |||||
| if (avatarFile) { | |||||
| // make use of img compression transformFile2Base64 | |||||
| (async () => { | |||||
| setAvatarBase64Str(await transformFile2Base64(avatarFile)); | |||||
| })(); | |||||
| } | |||||
| }, [avatarFile]); | |||||
| <Button variant="outline" className="mt-4"> | |||||
| Change password | |||||
| </Button> | |||||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||||
| // toast('You submitted the following values', { | |||||
| // description: ( | |||||
| // <pre className="mt-2 w-[320px] rounded-md bg-neutral-950 p-4"> | |||||
| // <code className="text-white">{JSON.stringify(data, null, 2)}</code> | |||||
| // </pre> | |||||
| // ), | |||||
| // }); | |||||
| // console.log('data=', data); | |||||
| // final submit form | |||||
| saveSetting({ | |||||
| nickname: data.userName, | |||||
| password: rsaPsw(data.currPasswd) as string, | |||||
| new_password: rsaPsw(data.newPasswd) as string, | |||||
| avatar: avatarBase64Str, | |||||
| timezone: data.timeZone, | |||||
| }); | |||||
| } | |||||
| return ( | |||||
| <section className="p-8"> | |||||
| <h1 className="text-3xl font-bold">{t('profile')}</h1> | |||||
| <div className="text-sm text-muted-foreground mb-6"> | |||||
| {t('profileDescription')} | |||||
| </div> | |||||
| <div> | |||||
| <Form {...form}> | |||||
| <form | |||||
| onSubmit={form.handleSubmit(onSubmit)} | |||||
| className="block space-y-6" | |||||
| > | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="userName" | |||||
| render={({ field }) => ( | |||||
| <FormItem className=" items-center space-y-0 "> | |||||
| <div className="flex w-[600px]"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| <span className="text-red-600">*</span> | |||||
| {t('username')} | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/4"> | |||||
| <Input | |||||
| placeholder="" | |||||
| {...field} | |||||
| className="bg-colors-background-inverse-weak" | |||||
| /> | |||||
| </FormControl> | |||||
| </div> | |||||
| <div className="flex w-[600px] pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="avatarUrl" | |||||
| render={({ field }) => ( | |||||
| <FormItem className="flex items-center space-y-0"> | |||||
| <div className="flex w-[600px]"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| Avatar | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/4"> | |||||
| <> | |||||
| <div className="relative group"> | |||||
| {!avatarBase64Str ? ( | |||||
| <div className="w-[64px] h-[64px] grid place-content-center"> | |||||
| <div className="flex flex-col items-center"> | |||||
| <Upload /> | |||||
| <p>Upload</p> | |||||
| </div> | |||||
| </div> | |||||
| ) : ( | |||||
| <div className="w-[64px] h-[64px] relative grid place-content-center"> | |||||
| <Avatar className="w-[64px] h-[64px]"> | |||||
| <AvatarImage | |||||
| className=" block" | |||||
| src={avatarBase64Str} | |||||
| alt="" | |||||
| /> | |||||
| <AvatarFallback></AvatarFallback> | |||||
| </Avatar> | |||||
| <div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60"> | |||||
| <Pencil | |||||
| size={20} | |||||
| className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| <Input | |||||
| placeholder="" | |||||
| {...field} | |||||
| type="file" | |||||
| title="" | |||||
| accept="image/*" | |||||
| className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" | |||||
| onChange={(ev) => { | |||||
| const file = ev.target?.files?.[0]; | |||||
| if ( | |||||
| /\.(jpg|jpeg|png|webp|bmp)$/i.test( | |||||
| file?.name ?? '', | |||||
| ) | |||||
| ) { | |||||
| setAvatarFile(file!); | |||||
| } | |||||
| ev.target.value = ''; | |||||
| }} | |||||
| /> | |||||
| </div> | |||||
| </> | |||||
| </FormControl> | |||||
| </div> | |||||
| <div className="flex w-[600px] pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="timeZone" | |||||
| render={({ field }) => ( | |||||
| <FormItem className="items-center space-y-0"> | |||||
| <div className="flex w-[600px]"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| <span className="text-red-600">*</span> | |||||
| {t('timezone')} | |||||
| </FormLabel> | |||||
| <Select onValueChange={field.onChange} value={field.value}> | |||||
| <FormControl className="w-3/4"> | |||||
| <SelectTrigger> | |||||
| <SelectValue placeholder="Select a timeZone" /> | |||||
| </SelectTrigger> | |||||
| </FormControl> | |||||
| <SelectContent> | |||||
| {TimezoneList.map((timeStr) => ( | |||||
| <SelectItem key={timeStr} value={timeStr}> | |||||
| {timeStr} | |||||
| </SelectItem> | |||||
| ))} | |||||
| </SelectContent> | |||||
| </Select> | |||||
| </div> | |||||
| <div className="flex w-[600px] pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="email" | |||||
| render={({ field }) => ( | |||||
| <div> | |||||
| <FormItem className="items-center space-y-0"> | |||||
| <div className="flex w-[600px]"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4"> | |||||
| {t('email')} | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/4"> | |||||
| <Input | |||||
| placeholder="Alex@gmail.com" | |||||
| disabled | |||||
| {...field} | |||||
| /> | |||||
| </FormControl> | |||||
| </div> | |||||
| <div className="flex w-[600px] pt-1"> | |||||
| <div className="w-1/4"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| <div className="flex w-[600px] pt-1"> | |||||
| <p className="w-1/4"> </p> | |||||
| <p className="text-sm text-muted-foreground whitespace-nowrap w-3/4"> | |||||
| {t('emailDescription')} | |||||
| </p> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| /> | |||||
| <div className="h-[10px]"></div> | |||||
| <div className="pb-6"> | |||||
| <h1 className="text-3xl font-bold">{t('password')}</h1> | |||||
| <div className="text-sm text-muted-foreground"> | |||||
| {t('passwordDescription')} | |||||
| </div> | |||||
| </div> | |||||
| <div className="h-0 overflow-hidden absolute"> | |||||
| <input type="password" className=" w-0 height-0 opacity-0" /> | |||||
| </div> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="currPasswd" | |||||
| render={({ field }) => ( | |||||
| <FormItem className=" items-center space-y-0"> | |||||
| <div className="flex w-[600px]"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5"> | |||||
| <span className="text-red-600">*</span> | |||||
| {t('currentPassword')} | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/5"> | |||||
| <PasswordInput {...field} /> | |||||
| </FormControl> | |||||
| </div> | |||||
| <div className="flex w-[600px] pt-1"> | |||||
| <div className="min-w-[170px] max-w-[170px]"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="newPasswd" | |||||
| render={({ field }) => ( | |||||
| <FormItem className=" items-center space-y-0"> | |||||
| <div className="flex w-[600px]"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5"> | |||||
| <span className="text-red-600">*</span> | |||||
| {t('newPassword')} | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/5"> | |||||
| <PasswordInput {...field} /> | |||||
| </FormControl> | |||||
| </div> | |||||
| <div className="flex w-[600px] pt-1"> | |||||
| <div className="min-w-[170px] max-w-[170px]"></div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name="confirmPasswd" | |||||
| render={({ field }) => ( | |||||
| <FormItem className=" items-center space-y-0"> | |||||
| <div className="flex w-[600px]"> | |||||
| <FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5"> | |||||
| <span className="text-red-600">*</span> | |||||
| {t('confirmPassword')} | |||||
| </FormLabel> | |||||
| <FormControl className="w-3/5"> | |||||
| <PasswordInput | |||||
| {...field} | |||||
| onBlur={() => { | |||||
| form.trigger('confirmPasswd'); | |||||
| }} | |||||
| onChange={(ev) => { | |||||
| form.setValue( | |||||
| 'confirmPasswd', | |||||
| ev.target.value.trim(), | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </FormControl> | |||||
| </div> | |||||
| <div className="flex w-[600px] pt-1"> | |||||
| <div className="min-w-[170px] max-w-[170px]"> </div> | |||||
| <FormMessage /> | |||||
| </div> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <div className="w-[600px] text-right space-x-4"> | |||||
| <Button variant="secondary">{t('cancel')}</Button> | |||||
| <Button type="submit" disabled={submitLoading}> | |||||
| {submitLoading && <Loader2Icon className="animate-spin" />} | |||||
| {t('save', { keyPrefix: 'common' })} | |||||
| </Button> | |||||
| </div> | |||||
| </form> | |||||
| </Form> | |||||
| </div> | </div> | ||||
| </section> | </section> | ||||
| ); | ); | 
| import { Label } from '@/components/ui/label'; | import { Label } from '@/components/ui/label'; | ||||
| import { Switch } from '@/components/ui/switch'; | import { Switch } from '@/components/ui/switch'; | ||||
| import { ProfileSettingRouteKey } from '@/constants/setting'; | import { ProfileSettingRouteKey } from '@/constants/setting'; | ||||
| import { useLogout } from '@/hooks/login-hooks'; | |||||
| import { useSecondPathName } from '@/hooks/route-hook'; | import { useSecondPathName } from '@/hooks/route-hook'; | ||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| import { | import { | ||||
| const { setTheme } = useTheme(); | const { setTheme } = useTheme(); | ||||
| const isDarkTheme = useIsDarkTheme(); | const isDarkTheme = useIsDarkTheme(); | ||||
| const { logout } = useLogout(); | |||||
| const handleThemeChange = useCallback( | const handleThemeChange = useCallback( | ||||
| (checked: boolean) => { | (checked: boolean) => { | ||||
| setTheme(checked ? 'dark' : 'light'); | setTheme(checked ? 'dark' : 'light'); | ||||
| Dark | Dark | ||||
| </Label> | </Label> | ||||
| </div> | </div> | ||||
| <Button variant="outline" className="w-full gap-3"> | |||||
| <Button | |||||
| variant="outline" | |||||
| className="w-full gap-3" | |||||
| onClick={() => { | |||||
| logout(); | |||||
| }} | |||||
| > | |||||
| <LogOut className="w-6 h-6" /> | <LogOut className="w-6 h-6" /> | ||||
| Logout | Logout | ||||
| </Button> | </Button> | 
| { | { | ||||
| path: '/knowledge', | path: '/knowledge', | ||||
| component: '@/pages/knowledge', | component: '@/pages/knowledge', | ||||
| // component: '@/pages/knowledge/datasets', | |||||
| }, | }, | ||||
| { | { | ||||
| path: '/knowledge', | path: '/knowledge', | ||||
| { path: '/user-setting', redirect: '/user-setting/profile' }, | { path: '/user-setting', redirect: '/user-setting/profile' }, | ||||
| { | { | ||||
| path: '/user-setting/profile', | path: '/user-setting/profile', | ||||
| // component: '@/pages/user-setting/setting-profile', | |||||
| component: '@/pages/user-setting/setting-profile', | component: '@/pages/user-setting/setting-profile', | ||||
| }, | }, | ||||
| { | { |