### 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
| @@ -0,0 +1,48 @@ | |||
| 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> | |||
| ); | |||
| }; | |||
| @@ -43,17 +43,33 @@ export function DelimiterFormField() { | |||
| <FormField | |||
| control={form.control} | |||
| 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> | |||
| ); | |||
| }} | |||
| /> | |||
| ); | |||
| } | |||
| @@ -2,14 +2,15 @@ | |||
| display: flex; | |||
| gap: 8px; | |||
| flex-wrap: wrap; | |||
| width: 100%; | |||
| // width: 100%; | |||
| margin-bottom: 8px; | |||
| } | |||
| .tag { | |||
| max-width: 100%; | |||
| margin: 0; | |||
| padding: 2px 20px 2px 4px; | |||
| padding: 2px 20px 0px 4px; | |||
| height: 26px; | |||
| font-size: 14px; | |||
| .textEllipsis(); | |||
| position: relative; | |||
| @@ -74,7 +74,7 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => { | |||
| }; | |||
| return ( | |||
| <div> | |||
| <div className="flex gap-[8px] items-start"> | |||
| {Array.isArray(tagChild) && tagChild.length > 0 && ( | |||
| <TweenOneGroup | |||
| className={styles.tweenGroup} | |||
| @@ -96,19 +96,23 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => { | |||
| </TweenOneGroup> | |||
| )} | |||
| {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> | |||
| ); | |||
| @@ -24,12 +24,21 @@ export function EntityTypesFormField({ | |||
| control={form.control} | |||
| name={name} | |||
| 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> | |||
| )} | |||
| /> | |||
| @@ -17,18 +17,37 @@ export function ExcelToHtmlFormField() { | |||
| <FormField | |||
| control={form.control} | |||
| 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> | |||
| ); | |||
| }} | |||
| /> | |||
| ); | |||
| } | |||
| @@ -54,17 +54,37 @@ export function LayoutRecognizeFormField() { | |||
| <FormField | |||
| control={form.control} | |||
| 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> | |||
| ); | |||
| }} | |||
| /> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| 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 }; | |||
| @@ -11,7 +11,7 @@ export function PageRankFormField() { | |||
| tooltip={t('pageRankTip')} | |||
| defaultValue={0} | |||
| max={100} | |||
| min={1} | |||
| min={0} | |||
| ></SliderInputFormField> | |||
| ); | |||
| } | |||
| @@ -58,17 +58,27 @@ export function UseGraphRagFormField() { | |||
| control={form.control} | |||
| name="parser_config.graphrag.use_graphrag" | |||
| 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> | |||
| )} | |||
| /> | |||
| @@ -112,25 +122,33 @@ const GraphRagItems = ({ | |||
| control={form.control} | |||
| name="parser_config.graphrag.method" | |||
| 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> | |||
| )} | |||
| /> | |||
| @@ -139,17 +157,27 @@ const GraphRagItems = ({ | |||
| control={form.control} | |||
| name="parser_config.graphrag.resolution" | |||
| 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> | |||
| )} | |||
| /> | |||
| @@ -158,17 +186,27 @@ const GraphRagItems = ({ | |||
| control={form.control} | |||
| name="parser_config.graphrag.community" | |||
| 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> | |||
| )} | |||
| /> | |||
| @@ -0,0 +1,146 @@ | |||
| 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; | |||
| @@ -62,18 +62,39 @@ const RaptorFormFields = () => { | |||
| <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> | |||
| )} | |||
| 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 && ( | |||
| <div className="space-y-3"> | |||
| @@ -81,12 +102,24 @@ const RaptorFormFields = () => { | |||
| 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 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> | |||
| )} | |||
| /> | |||
| @@ -119,21 +152,30 @@ const RaptorFormFields = () => { | |||
| 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> | |||
| <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> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </div> | |||
| <div className="flex pt-1"> | |||
| <div className="w-1/4"></div> | |||
| <FormMessage /> | |||
| </div> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| @@ -0,0 +1,58 @@ | |||
| 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); | |||
| @@ -38,38 +38,50 @@ export function SliderInputFormField({ | |||
| <FormField | |||
| control={form.control} | |||
| name={name} | |||
| defaultValue={defaultValue} | |||
| defaultValue={defaultValue || 0} | |||
| 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> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| @@ -0,0 +1,64 @@ | |||
| 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 }; | |||
| @@ -228,11 +228,18 @@ export const useUpdateKnowledge = (shouldFetchList = false) => { | |||
| return { data, loading, saveKnowledgeConfiguration: mutateAsync }; | |||
| }; | |||
| export const useFetchKnowledgeBaseConfiguration = () => { | |||
| export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => { | |||
| const { id } = useParams(); | |||
| let queryKey: (KnowledgeApiAction | number)[] = [ | |||
| KnowledgeApiAction.FetchKnowledgeDetail, | |||
| ]; | |||
| if (typeof refreshCount === 'number') { | |||
| queryKey = [KnowledgeApiAction.FetchKnowledgeDetail, refreshCount]; | |||
| } | |||
| const { data, isFetching: loading } = useQuery<IKnowledge>({ | |||
| queryKey: [KnowledgeApiAction.FetchKnowledgeDetail], | |||
| queryKey, | |||
| initialData: {} as IKnowledge, | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| @@ -16,6 +16,7 @@ export interface IUserInfo { | |||
| nickname: string; | |||
| password: string; | |||
| status: string; | |||
| timezone: string; | |||
| update_date: string; | |||
| update_time: number; | |||
| } | |||
| @@ -565,6 +565,7 @@ export default { | |||
| }, | |||
| setting: { | |||
| profile: 'Profil', | |||
| avatar: 'Avatar', | |||
| profileDescription: | |||
| 'Aktualisieren Sie hier Ihr Foto und Ihre persönlichen Daten.', | |||
| maxTokens: 'Maximale Tokens', | |||
| @@ -551,6 +551,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s | |||
| }, | |||
| setting: { | |||
| profile: 'Profile', | |||
| avatar: 'Avatar', | |||
| profileDescription: 'Update your photo and personal details here.', | |||
| maxTokens: 'Max Tokens', | |||
| maxTokensMessage: 'Max Tokens is required', | |||
| @@ -282,6 +282,7 @@ export default { | |||
| }, | |||
| setting: { | |||
| profile: 'Perfil', | |||
| avatar: 'Avatar', | |||
| profileDescription: 'Actualiza tu foto y tus datos personales aquí.', | |||
| maxTokens: 'Máximo de tokens', | |||
| maxTokensMessage: 'El máximo de tokens es obligatorio', | |||
| @@ -456,6 +456,7 @@ export default { | |||
| }, | |||
| setting: { | |||
| profile: 'Profil', | |||
| avatar: 'Avatar', | |||
| profileDescription: 'Perbarui foto dan detail pribadi Anda di sini.', | |||
| maxTokens: 'Token Maksimum', | |||
| maxTokensMessage: 'Token Maksimum diperlukan', | |||
| @@ -453,6 +453,7 @@ export default { | |||
| }, | |||
| setting: { | |||
| profile: 'プロファイル', | |||
| avatar: 'アバター', | |||
| profileDescription: 'ここで写真と個人情報を更新してください。', | |||
| maxTokens: '最大トークン数', | |||
| maxTokensMessage: '最大トークン数は必須です', | |||
| @@ -451,6 +451,7 @@ export default { | |||
| }, | |||
| setting: { | |||
| profile: 'Perfil', | |||
| avatar: 'Avatar', | |||
| profileDescription: 'Atualize sua foto e detalhes pessoais aqui.', | |||
| maxTokens: 'Máximo de Tokens', | |||
| maxTokensMessage: 'Máximo de Tokens é obrigatório', | |||
| @@ -505,6 +505,7 @@ export default { | |||
| }, | |||
| setting: { | |||
| 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.', | |||
| maxTokens: 'Token tối đa', | |||
| maxTokensMessage: 'Token tối đa là bắt buộc', | |||
| @@ -534,6 +534,7 @@ export default { | |||
| }, | |||
| setting: { | |||
| profile: '概述', | |||
| avatar: '头像', | |||
| profileDescription: '在此更新您的照片和個人詳細信息。', | |||
| maxTokens: '最大token數', | |||
| maxTokensMessage: '最大token數是必填項', | |||
| @@ -232,7 +232,8 @@ export default { | |||
| cancel: '取消', | |||
| methodTitle: '分块方法说明', | |||
| methodExamples: '示例', | |||
| methodExamplesDescription: '为帮助您更好地理解,我们提供了相关截图供您参考。', | |||
| methodExamplesDescription: | |||
| '为帮助您更好地理解,我们提供了相关截图供您参考。', | |||
| dialogueExamplesTitle: '对话示例', | |||
| methodEmpty: '这将显示知识库类别的可视化解释', | |||
| book: `<p>支持的文件格式为<b>DOCX</b>、<b>PDF</b>、<b>TXT</b>。</p><p> | |||
| @@ -554,6 +555,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 | |||
| }, | |||
| setting: { | |||
| profile: '概要', | |||
| avatar: '头像', | |||
| profileDescription: '在此更新您的照片和个人详细信息。', | |||
| maxTokens: '最大token数', | |||
| maxTokensMessage: '最大token数是必填项', | |||
| @@ -0,0 +1,68 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| import ParsedResultPanel from '../parsed-result-panel'; | |||
| export default function ParsedResult() { | |||
| return ( | |||
| <section className="flex"> | |||
| <div className="flex-1"></div> | |||
| <ParsedResultPanel></ParsedResultPanel> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| .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; | |||
| } | |||
| @@ -0,0 +1,101 @@ | |||
| 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; | |||
| @@ -0,0 +1,140 @@ | |||
| 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; | |||
| @@ -0,0 +1,107 @@ | |||
| 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> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,221 @@ | |||
| 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; | |||
| @@ -0,0 +1,55 @@ | |||
| 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; | |||
| }; | |||
| @@ -0,0 +1,12 @@ | |||
| .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); | |||
| } | |||
| } | |||
| @@ -0,0 +1,121 @@ | |||
| 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); | |||
| @@ -0,0 +1,4 @@ | |||
| export enum ChunkTextMode { | |||
| Full = 'full', | |||
| Ellipse = 'ellipse', | |||
| } | |||
| @@ -0,0 +1,129 @@ | |||
| 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, | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,92 @@ | |||
| .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; | |||
| } | |||
| @@ -0,0 +1,202 @@ | |||
| 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; | |||
| @@ -0,0 +1,24 @@ | |||
| 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; | |||
| }, []); | |||
| } | |||
| @@ -5,6 +5,7 @@ import { SideBar } from './sidebar'; | |||
| export default function DatasetWrapper() { | |||
| const { navigateToDatasetList } = useNavigatePage(); | |||
| return ( | |||
| <section> | |||
| <PageHeader | |||
| @@ -1,7 +1,12 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Loader2Icon } from 'lucide-react'; | |||
| import { useFormContext, useWatch } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { DocumentParserType } from '@/constants/knowledge'; | |||
| import { useUpdateKnowledge } from '@/hooks/knowledge-hooks'; | |||
| import { useMemo } from 'react'; | |||
| import { useParams } from 'umi'; | |||
| import { AudioConfiguration } from './configuration/audio'; | |||
| import { BookConfiguration } from './configuration/book'; | |||
| import { EmailConfiguration } from './configuration/email'; | |||
| @@ -42,6 +47,12 @@ function EmptyComponent() { | |||
| export function ChunkMethodForm() { | |||
| 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({ | |||
| control: form.control, | |||
| @@ -55,8 +66,41 @@ export function ChunkMethodForm() { | |||
| }, [finalParserId]); | |||
| 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> | |||
| </> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| 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> | |||
| ); | |||
| }; | |||
| @@ -25,19 +25,29 @@ export function ChunkMethodItem() { | |||
| control={form.control} | |||
| name={'parser_id'} | |||
| 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> | |||
| )} | |||
| /> | |||
| @@ -55,19 +65,29 @@ export function EmbeddingModelItem() { | |||
| control={form.control} | |||
| name={'embd_id'} | |||
| 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> | |||
| )} | |||
| /> | |||
| @@ -7,35 +7,67 @@ export const formSchema = z.object({ | |||
| description: z.string().min(2, { | |||
| message: 'Username must be at least 2 characters.', | |||
| }), | |||
| avatar: z.instanceof(File), | |||
| // avatar: z.instanceof(File), | |||
| avatar: z.any().nullish(), | |||
| permission: z.string(), | |||
| parser_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(), | |||
| // icon: z.array(z.instanceof(File)), | |||
| }); | |||
| @@ -1,5 +1,6 @@ | |||
| import { FileUploader } from '@/components/file-uploader'; | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| FormControl, | |||
| FormField, | |||
| @@ -9,100 +10,235 @@ import { | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| 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 { useTranslation } from 'react-i18next'; | |||
| import { useParams } from 'umi'; | |||
| export function GeneralForm() { | |||
| const form = useFormContext(); | |||
| 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> | |||
| </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> | |||
| <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> | |||
| </> | |||
| ); | |||
| } | |||
| @@ -1,13 +1,19 @@ | |||
| import { ButtonLoading } from '@/components/ui/button'; | |||
| 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 { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useState } from 'react'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { TopTitle } from '../dataset-title'; | |||
| import CategoryPanel from './category-panel'; | |||
| import { ChunkMethodForm } from './chunk-method-form'; | |||
| import ChunkMethodLearnMore from './chunk-method-learn-more'; | |||
| import { formSchema } from './form-schema'; | |||
| import { GeneralForm } from './general-form'; | |||
| import { useFetchKnowledgeConfigurationOnMount } from './hooks'; | |||
| @@ -31,6 +37,7 @@ const enum MethodValue { | |||
| } | |||
| export default function DatasetSettings() { | |||
| const { t } = useTranslation(); | |||
| const form = useForm<z.infer<typeof formSchema>>({ | |||
| resolver: zodResolver(formSchema), | |||
| defaultValues: { | |||
| @@ -64,6 +71,10 @@ export default function DatasetSettings() { | |||
| useFetchKnowledgeConfigurationOnMount(form); | |||
| const [currentTab, setCurrentTab] = useState< | |||
| 'generalForm' | 'chunkMethodForm' | |||
| >('generalForm'); // currnet Tab state | |||
| const parserId = useWatch({ | |||
| control: form.control, | |||
| name: 'parser_id', | |||
| @@ -76,35 +87,66 @@ export default function DatasetSettings() { | |||
| return ( | |||
| <section className="p-5 "> | |||
| <TopTitle | |||
| title={'Configuration'} | |||
| description={` Update your knowledge base configuration here, particularly the chunk | |||
| method.`} | |||
| title={t('knowledgeDetails.configuration')} | |||
| description={t('knowledgeConfiguration.titleDescription')} | |||
| ></TopTitle> | |||
| <div className="flex gap-14"> | |||
| <Form {...form}> | |||
| <form | |||
| 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> | |||
| <TabsContent value="account"> | |||
| <TabsContent value="generalForm"> | |||
| <GeneralForm></GeneralForm> | |||
| </TabsContent> | |||
| <TabsContent value="password"> | |||
| <TabsContent value="chunkMethodForm"> | |||
| <ChunkMethodForm></ChunkMethodForm> | |||
| </TabsContent> | |||
| </Tabs> | |||
| <div className="text-right"> | |||
| {/* <div className="text-right"> | |||
| <ButtonLoading type="submit">Submit</ButtonLoading> | |||
| </div> | |||
| </div> */} | |||
| </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> | |||
| </section> | |||
| ); | |||
| @@ -38,31 +38,39 @@ export const TagSetItem = () => { | |||
| control={form.control} | |||
| name="parser_config.tag_kb_ids" | |||
| 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> | |||
| )} | |||
| /> | |||
| @@ -10,10 +10,15 @@ import { useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useHandleMenuClick } from './hooks'; | |||
| export function SideBar() { | |||
| type PropType = { | |||
| refreshCount?: number; | |||
| }; | |||
| export function SideBar({ refreshCount }: PropType) { | |||
| const pathName = useSecondPathName(); | |||
| const { handleMenuClick } = useHandleMenuClick(); | |||
| const { data } = useFetchKnowledgeBaseConfiguration(); | |||
| // refreshCount: be for avatar img sync update on top left | |||
| const { data } = useFetchKnowledgeBaseConfiguration(refreshCount); | |||
| const { t } = useTranslation(); | |||
| const items = useMemo(() => { | |||
| @@ -1,6 +1,4 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { useTestRetrieval } from '@/hooks/use-knowledge-request'; | |||
| import { Plus } from 'lucide-react'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { TopTitle } from '../dataset-title'; | |||
| import TestingForm from './testing-form'; | |||
| @@ -41,7 +39,7 @@ export default function RetrievalTesting() { | |||
| description={` Update your knowledge base configuration here, particularly the chunk | |||
| method.`} | |||
| ></TopTitle> | |||
| <Button>Save as Preset</Button> | |||
| {/* <Button>Save as Preset</Button> */} | |||
| </section> | |||
| {count === 1 ? ( | |||
| <section className="flex divide-x h-full"> | |||
| @@ -50,9 +48,9 @@ export default function RetrievalTesting() { | |||
| <span className="text-text-title font-semibold text-2xl"> | |||
| Test setting | |||
| </span> | |||
| <Button variant={'outline'} onClick={addCount}> | |||
| {/* <Button variant={'outline'} onClick={addCount}> | |||
| <Plus /> Add New Test | |||
| </Button> | |||
| </Button> */} | |||
| </div> | |||
| <TestingForm | |||
| loading={loading} | |||
| @@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm, useWatch } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { CrossLanguageItem } from '@/components/cross-language-item-ui'; | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { | |||
| initialTopKValue, | |||
| @@ -30,7 +31,8 @@ import { Textarea } from '@/components/ui/textarea'; | |||
| import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item'; | |||
| import { useTestRetrieval } from '@/hooks/use-knowledge-request'; | |||
| import { trim } from 'lodash'; | |||
| import { useEffect } from 'react'; | |||
| import { CirclePlay } from 'lucide-react'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| type TestingFormProps = Pick< | |||
| @@ -44,6 +46,7 @@ export default function TestingForm({ | |||
| setValues, | |||
| }: TestingFormProps) { | |||
| const { t } = useTranslation(); | |||
| const [cross_languages, setCrossLangArr] = useState<string[]>([]); | |||
| const formSchema = z.object({ | |||
| question: z.string().min(1, { | |||
| @@ -68,8 +71,9 @@ export default function TestingForm({ | |||
| const values = useWatch({ control: form.control }); | |||
| 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() { | |||
| refetch(); | |||
| @@ -85,6 +89,12 @@ export default function TestingForm({ | |||
| ></SimilaritySliderFormField> | |||
| <RerankFormFields></RerankFormFields> | |||
| <UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField> | |||
| <CrossLanguageItem | |||
| name={'cross_languages'} | |||
| onChange={(valArr) => { | |||
| setCrossLangArr(valArr); | |||
| }} | |||
| ></CrossLanguageItem> | |||
| </FormContainer> | |||
| <FormField | |||
| control={form.control} | |||
| @@ -103,13 +113,16 @@ export default function TestingForm({ | |||
| </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> | |||
| ); | |||
| @@ -1,5 +1,14 @@ | |||
| import PasswordInput from '@/components/password-input'; | |||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { | |||
| Select, | |||
| @@ -8,59 +17,390 @@ import { | |||
| SelectTrigger, | |||
| SelectValue, | |||
| } 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() { | |||
| 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> | |||
| </section> | |||
| ); | |||
| @@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'; | |||
| import { Label } from '@/components/ui/label'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { ProfileSettingRouteKey } from '@/constants/setting'; | |||
| import { useLogout } from '@/hooks/login-hooks'; | |||
| import { useSecondPathName } from '@/hooks/route-hook'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { | |||
| @@ -54,6 +55,8 @@ export function SideBar() { | |||
| const { setTheme } = useTheme(); | |||
| const isDarkTheme = useIsDarkTheme(); | |||
| const { logout } = useLogout(); | |||
| const handleThemeChange = useCallback( | |||
| (checked: boolean) => { | |||
| setTheme(checked ? 'dark' : 'light'); | |||
| @@ -99,7 +102,13 @@ export function SideBar() { | |||
| Dark | |||
| </Label> | |||
| </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 | |||
| </Button> | |||
| @@ -49,6 +49,7 @@ const routes = [ | |||
| { | |||
| path: '/knowledge', | |||
| component: '@/pages/knowledge', | |||
| // component: '@/pages/knowledge/datasets', | |||
| }, | |||
| { | |||
| path: '/knowledge', | |||
| @@ -93,6 +94,7 @@ const routes = [ | |||
| { path: '/user-setting', redirect: '/user-setting/profile' }, | |||
| { | |||
| path: '/user-setting/profile', | |||
| // component: '@/pages/user-setting/setting-profile', | |||
| component: '@/pages/user-setting/setting-profile', | |||
| }, | |||
| { | |||