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