Parcourir la source

Refactor: Datasets UI #3221 (#8349)

### 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
BlueYu-0221 il y a 4 mois
Parent
révision
fa3e90c72e
Aucun compte lié à l'adresse e-mail de l'auteur
55 fichiers modifiés avec 2956 ajouts et 421 suppressions
  1. 48
    0
      web/src/components/cross-language-item-ui.tsx
  2. 27
    11
      web/src/components/delimiter-form-field.tsx
  3. 3
    2
      web/src/components/edit-tag/index.less
  4. 17
    13
      web/src/components/edit-tag/index.tsx
  5. 15
    6
      web/src/components/entity-types-form-field.tsx
  6. 31
    12
      web/src/components/excel-to-html-form-field.tsx
  7. 31
    11
      web/src/components/layout-recognize-form-field.tsx
  8. 25
    0
      web/src/components/originui/input.tsx
  9. 1
    1
      web/src/components/page-rank-form-field.tsx
  10. 90
    52
      web/src/components/parse-configuration/graph-rag-form-fields.tsx
  11. 146
    0
      web/src/components/parse-configuration/raptor-form-fields-old.tsx
  12. 74
    32
      web/src/components/parse-configuration/raptor-form-fields.tsx
  13. 58
    0
      web/src/components/password-input/index.tsx
  14. 43
    31
      web/src/components/slider-input-form-field.tsx
  15. 64
    0
      web/src/components/ui/tabs-underlined.tsx
  16. 9
    2
      web/src/hooks/use-knowledge-request.ts
  17. 1
    0
      web/src/interfaces/database/user-setting.ts
  18. 1
    0
      web/src/locales/de.ts
  19. 1
    0
      web/src/locales/en.ts
  20. 1
    0
      web/src/locales/es.ts
  21. 1
    0
      web/src/locales/id.ts
  22. 1
    0
      web/src/locales/ja.ts
  23. 1
    0
      web/src/locales/pt-br.ts
  24. 1
    0
      web/src/locales/vi.ts
  25. 1
    0
      web/src/locales/zh-traditional.ts
  26. 3
    1
      web/src/locales/zh.ts
  27. 68
    0
      web/src/pages/chunk/index-old.tsx
  28. 10
    0
      web/src/pages/chunk/parsed-result/index-old.tsx
  29. 34
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-card/index.less
  30. 101
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-card/index.tsx
  31. 140
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-creating-modal/index.tsx
  32. 107
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-creating-modal/tag-feature-item.tsx
  33. 221
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-toolbar/index.tsx
  34. 55
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/components/document-preview/hooks.ts
  35. 12
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/components/document-preview/index.less
  36. 121
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/components/document-preview/preview.tsx
  37. 4
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/constant.ts
  38. 129
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/hooks.ts
  39. 92
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/index.less
  40. 202
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/index.tsx
  41. 24
    0
      web/src/pages/chunk/parsed-result/knowledge-chunk/utils.ts
  42. 1
    0
      web/src/pages/dataset/index.tsx
  43. 47
    3
      web/src/pages/dataset/setting/chunk-method-form.tsx
  44. 45
    0
      web/src/pages/dataset/setting/chunk-method-learn-more.tsx
  45. 46
    26
      web/src/pages/dataset/setting/configuration/common-item.tsx
  46. 58
    26
      web/src/pages/dataset/setting/form-schema.ts
  47. 222
    86
      web/src/pages/dataset/setting/general-form.tsx
  48. 59
    17
      web/src/pages/dataset/setting/index.tsx
  49. 33
    25
      web/src/pages/dataset/setting/tag-item.tsx
  50. 7
    2
      web/src/pages/dataset/sidebar/index.tsx
  51. 3
    5
      web/src/pages/dataset/testing/index.tsx
  52. 23
    10
      web/src/pages/dataset/testing/testing-form.tsx
  53. 386
    46
      web/src/pages/profile-setting/profile/index.tsx
  54. 10
    1
      web/src/pages/profile-setting/sidebar/index.tsx
  55. 2
    0
      web/src/routes.ts

+ 48
- 0
web/src/components/cross-language-item-ui.tsx Voir le fichier

@@ -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>
);
};

+ 27
- 11
web/src/components/delimiter-form-field.tsx Voir le fichier

@@ -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>
);
}}
/>
);
}

+ 3
- 2
web/src/components/edit-tag/index.less Voir le fichier

@@ -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;

+ 17
- 13
web/src/components/edit-tag/index.tsx Voir le fichier

@@ -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>
);

+ 15
- 6
web/src/components/entity-types-form-field.tsx Voir le fichier

@@ -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>
)}
/>

+ 31
- 12
web/src/components/excel-to-html-form-field.tsx Voir le fichier

@@ -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>
);
}}
/>
);
}

+ 31
- 11
web/src/components/layout-recognize-form-field.tsx Voir le fichier

@@ -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>
);
}}
/>
);
}

+ 25
- 0
web/src/components/originui/input.tsx Voir le fichier

@@ -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 };

+ 1
- 1
web/src/components/page-rank-form-field.tsx Voir le fichier

@@ -11,7 +11,7 @@ export function PageRankFormField() {
tooltip={t('pageRankTip')}
defaultValue={0}
max={100}
min={1}
min={0}
></SliderInputFormField>
);
}

+ 90
- 52
web/src/components/parse-configuration/graph-rag-form-fields.tsx Voir le fichier

@@ -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>
)}
/>

+ 146
- 0
web/src/components/parse-configuration/raptor-form-fields-old.tsx Voir le fichier

@@ -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;

+ 74
- 32
web/src/components/parse-configuration/raptor-form-fields.tsx Voir le fichier

@@ -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>
)}
/>

+ 58
- 0
web/src/components/password-input/index.tsx Voir le fichier

@@ -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);

+ 43
- 31
web/src/components/slider-input-form-field.tsx Voir le fichier

@@ -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>

+ 64
- 0
web/src/components/ui/tabs-underlined.tsx Voir le fichier

@@ -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 };

+ 9
- 2
web/src/hooks/use-knowledge-request.ts Voir le fichier

@@ -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 () => {

+ 1
- 0
web/src/interfaces/database/user-setting.ts Voir le fichier

@@ -16,6 +16,7 @@ export interface IUserInfo {
nickname: string;
password: string;
status: string;
timezone: string;
update_date: string;
update_time: number;
}

+ 1
- 0
web/src/locales/de.ts Voir le fichier

@@ -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',

+ 1
- 0
web/src/locales/en.ts Voir le fichier

@@ -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',

+ 1
- 0
web/src/locales/es.ts Voir le fichier

@@ -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',

+ 1
- 0
web/src/locales/id.ts Voir le fichier

@@ -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',

+ 1
- 0
web/src/locales/ja.ts Voir le fichier

@@ -453,6 +453,7 @@ export default {
},
setting: {
profile: 'プロファイル',
avatar: 'アバター‌',
profileDescription: 'ここで写真と個人情報を更新してください。',
maxTokens: '最大トークン数',
maxTokensMessage: '最大トークン数は必須です',

+ 1
- 0
web/src/locales/pt-br.ts Voir le fichier

@@ -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',

+ 1
- 0
web/src/locales/vi.ts Voir le fichier

@@ -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',

+ 1
- 0
web/src/locales/zh-traditional.ts Voir le fichier

@@ -534,6 +534,7 @@ export default {
},
setting: {
profile: '概述',
avatar: '头像',
profileDescription: '在此更新您的照片和個人詳細信息。',
maxTokens: '最大token數',
maxTokensMessage: '最大token數是必填項',

+ 3
- 1
web/src/locales/zh.ts Voir le fichier

@@ -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数是必填项',

+ 68
- 0
web/src/pages/chunk/index-old.tsx Voir le fichier

@@ -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>
);
}

+ 10
- 0
web/src/pages/chunk/parsed-result/index-old.tsx Voir le fichier

@@ -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>
);
}

+ 34
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-card/index.less Voir le fichier

@@ -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;
}

+ 101
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-card/index.tsx Voir le fichier

@@ -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;

+ 140
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-creating-modal/index.tsx Voir le fichier

@@ -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;

+ 107
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-creating-modal/tag-feature-item.tsx Voir le fichier

@@ -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>
);
};

+ 221
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/components/chunk-toolbar/index.tsx Voir le fichier

@@ -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;

+ 55
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/components/document-preview/hooks.ts Voir le fichier

@@ -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;
};

+ 12
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/components/document-preview/index.less Voir le fichier

@@ -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);
}
}

+ 121
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/components/document-preview/preview.tsx Voir le fichier

@@ -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);

+ 4
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/constant.ts Voir le fichier

@@ -0,0 +1,4 @@
export enum ChunkTextMode {
Full = 'full',
Ellipse = 'ellipse',
}

+ 129
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/hooks.ts Voir le fichier

@@ -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,
};
};

+ 92
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/index.less Voir le fichier

@@ -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;
}

+ 202
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/index.tsx Voir le fichier

@@ -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;

+ 24
- 0
web/src/pages/chunk/parsed-result/knowledge-chunk/utils.ts Voir le fichier

@@ -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;
}, []);
}

+ 1
- 0
web/src/pages/dataset/index.tsx Voir le fichier

@@ -5,6 +5,7 @@ import { SideBar } from './sidebar';

export default function DatasetWrapper() {
const { navigateToDatasetList } = useNavigatePage();

return (
<section>
<PageHeader

+ 47
- 3
web/src/pages/dataset/setting/chunk-method-form.tsx Voir le fichier

@@ -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>
</>
);
}

+ 45
- 0
web/src/pages/dataset/setting/chunk-method-learn-more.tsx Voir le fichier

@@ -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>
);
};

+ 46
- 26
web/src/pages/dataset/setting/configuration/common-item.tsx Voir le fichier

@@ -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>
)}
/>

+ 58
- 26
web/src/pages/dataset/setting/form-schema.ts Voir le fichier

@@ -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)),
});

+ 222
- 86
web/src/pages/dataset/setting/general-form.tsx Voir le fichier

@@ -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>
</>
);
}

+ 59
- 17
web/src/pages/dataset/setting/index.tsx Voir le fichier

@@ -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>
);

+ 33
- 25
web/src/pages/dataset/setting/tag-item.tsx Voir le fichier

@@ -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>
)}
/>

+ 7
- 2
web/src/pages/dataset/sidebar/index.tsx Voir le fichier

@@ -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(() => {

+ 3
- 5
web/src/pages/dataset/testing/index.tsx Voir le fichier

@@ -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}

+ 23
- 10
web/src/pages/dataset/testing/testing-form.tsx Voir le fichier

@@ -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>
);

+ 386
- 46
web/src/pages/profile-setting/profile/index.tsx Voir le fichier

@@ -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">&nbsp;</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]">&nbsp;</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>
);

+ 10
- 1
web/src/pages/profile-setting/sidebar/index.tsx Voir le fichier

@@ -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>

+ 2
- 0
web/src/routes.ts Voir le fichier

@@ -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',
},
{

Chargement…
Annuler
Enregistrer