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
|
||||
control={form.control}
|
||||
name={'parser_config.delimiter'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('knowledgeDetails.delimiterTip')}>
|
||||
{t('knowledgeDetails.delimiter')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DelimiterInput {...field}></DelimiterInput>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
if (typeof field.value === 'undefined') {
|
||||
// default value set
|
||||
form.setValue('parser_config.delimiter', '\n');
|
||||
}
|
||||
return (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('knowledgeDetails.delimiterTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('knowledgeDetails.delimiter')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<DelimiterInput {...field}></DelimiterInput>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,14 +2,15 @@
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
// width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 2px 20px 2px 4px;
|
||||
padding: 2px 20px 0px 4px;
|
||||
height: 26px;
|
||||
font-size: 14px;
|
||||
.textEllipsis();
|
||||
position: relative;
|
||||
|
||||
@ -74,7 +74,7 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-[8px] items-start">
|
||||
{Array.isArray(tagChild) && tagChild.length > 0 && (
|
||||
<TweenOneGroup
|
||||
className={styles.tweenGroup}
|
||||
@ -96,19 +96,23 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => {
|
||||
</TweenOneGroup>
|
||||
)}
|
||||
{inputVisible ? (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
size="small"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputConfirm}
|
||||
onPressEnter={handleInputConfirm}
|
||||
/>
|
||||
<div className="w-[180px] mb-[8px]">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
size="small"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleInputConfirm}
|
||||
onPressEnter={handleInputConfirm}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Tag onClick={showInput} style={tagPlusStyle}>
|
||||
<PlusOutlined />
|
||||
</Tag>
|
||||
<div className="mb-[8px]">
|
||||
<Tag onClick={showInput} style={tagPlusStyle}>
|
||||
<PlusOutlined />
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -24,12 +24,21 @@ export function EntityTypesFormField({
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('entityTypes')}</FormLabel>
|
||||
<FormControl>
|
||||
<EditTag {...field}></EditTag>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
<span className="text-red-600">*</span> {t('entityTypes')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<EditTag {...field}></EditTag>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -17,18 +17,37 @@ export function ExcelToHtmlFormField() {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="parser_config.html4excel"
|
||||
render={({ field }) => (
|
||||
<FormItem defaultChecked={false}>
|
||||
<FormLabel tooltip={t('html4excelTip')}>{t('html4excel')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
if (typeof field.value === 'undefined') {
|
||||
// default value set
|
||||
form.setValue('parser_config.html4excel', false);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem defaultChecked={false} className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('html4excelTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('html4excel')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -54,17 +54,37 @@ export function LayoutRecognizeFormField() {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="parser_config.layout_recognize"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('layoutRecognizeTip')}>
|
||||
{t('layoutRecognize')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RAGFlowSelect {...field} options={options}></RAGFlowSelect>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
if (typeof field.value === 'undefined') {
|
||||
// default value set
|
||||
form.setValue(
|
||||
'parser_config.layout_recognize',
|
||||
form.formState.defaultValues?.parser_config?.layout_recognize ??
|
||||
'DeepDOC',
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('layoutRecognizeTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('layoutRecognize')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<RAGFlowSelect {...field} options={options}></RAGFlowSelect>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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')}
|
||||
defaultValue={0}
|
||||
max={100}
|
||||
min={1}
|
||||
min={0}
|
||||
></SliderInputFormField>
|
||||
);
|
||||
}
|
||||
|
||||
@ -58,17 +58,27 @@ export function UseGraphRagFormField() {
|
||||
control={form.control}
|
||||
name="parser_config.graphrag.use_graphrag"
|
||||
render={({ field }) => (
|
||||
<FormItem defaultChecked={false}>
|
||||
<FormLabel tooltip={t('useGraphRagTip')}>
|
||||
{t('useGraphRag')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem defaultChecked={false} className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('useGraphRagTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('useGraphRag')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@ -112,25 +122,33 @@ const GraphRagItems = ({
|
||||
control={form.control}
|
||||
name="parser_config.graphrag.method"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
tooltip={renderWideTooltip(
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('graphRagMethodTip'),
|
||||
}}
|
||||
></div>,
|
||||
)}
|
||||
>
|
||||
{t('graphRagMethod')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
options={methodOptions}
|
||||
></RAGFlowSelect>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
tooltip={renderWideTooltip(
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('graphRagMethodTip'),
|
||||
}}
|
||||
></div>,
|
||||
)}
|
||||
>
|
||||
{t('graphRagMethod')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
options={methodOptions}
|
||||
></RAGFlowSelect>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@ -139,17 +157,27 @@ const GraphRagItems = ({
|
||||
control={form.control}
|
||||
name="parser_config.graphrag.resolution"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={renderWideTooltip('resolutionTip')}>
|
||||
{t('resolution')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={renderWideTooltip('resolutionTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('resolution')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@ -158,17 +186,27 @@ const GraphRagItems = ({
|
||||
control={form.control}
|
||||
name="parser_config.graphrag.community"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={renderWideTooltip('communityTip')}>
|
||||
{t('community')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={renderWideTooltip('communityTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('community')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import random from 'lodash/random';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { SliderInputFormField } from '../slider-input-form-field';
|
||||
import { Button } from '../ui/button';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '../ui/form';
|
||||
import { Input } from '../ui/input';
|
||||
import { Switch } from '../ui/switch';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
|
||||
export const excludedParseMethods = [
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.Resume,
|
||||
DocumentParserType.One,
|
||||
DocumentParserType.Picture,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
DocumentParserType.Qa,
|
||||
DocumentParserType.Tag,
|
||||
];
|
||||
|
||||
export const showRaptorParseConfiguration = (
|
||||
parserId: DocumentParserType | undefined,
|
||||
) => {
|
||||
return !excludedParseMethods.some((x) => x === parserId);
|
||||
};
|
||||
|
||||
export const excludedTagParseMethods = [
|
||||
DocumentParserType.Table,
|
||||
DocumentParserType.KnowledgeGraph,
|
||||
DocumentParserType.Tag,
|
||||
];
|
||||
|
||||
export const showTagItems = (parserId: DocumentParserType) => {
|
||||
return !excludedTagParseMethods.includes(parserId);
|
||||
};
|
||||
|
||||
const UseRaptorField = 'parser_config.raptor.use_raptor';
|
||||
const RandomSeedField = 'parser_config.raptor.random_seed';
|
||||
|
||||
// The three types "table", "resume" and "one" do not display this configuration.
|
||||
|
||||
const RaptorFormFields = () => {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslate('knowledgeConfiguration');
|
||||
const useRaptor = useWatch({ name: UseRaptorField });
|
||||
|
||||
const handleGenerate = useCallback(() => {
|
||||
form.setValue(RandomSeedField, random(10000));
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={UseRaptorField}
|
||||
render={({ field }) => (
|
||||
<FormItem defaultChecked={false}>
|
||||
<FormLabel tooltip={t('useRaptorTip')}>{t('useRaptor')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{useRaptor && (
|
||||
<div className="space-y-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parser_config.raptor.prompt'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('promptTip')}>{t('prompt')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} rows={8} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SliderInputFormField
|
||||
name={'parser_config.raptor.max_token'}
|
||||
label={t('maxToken')}
|
||||
tooltip={t('maxTokenTip')}
|
||||
defaultValue={256}
|
||||
max={2048}
|
||||
min={0}
|
||||
></SliderInputFormField>
|
||||
<SliderInputFormField
|
||||
name={'parser_config.raptor.threshold'}
|
||||
label={t('threshold')}
|
||||
tooltip={t('thresholdTip')}
|
||||
defaultValue={0.1}
|
||||
step={0.01}
|
||||
max={1}
|
||||
min={0}
|
||||
></SliderInputFormField>
|
||||
<SliderInputFormField
|
||||
name={'parser_config.raptor.max_cluster'}
|
||||
label={t('maxCluster')}
|
||||
tooltip={t('maxClusterTip')}
|
||||
defaultValue={64}
|
||||
max={1024}
|
||||
min={1}
|
||||
></SliderInputFormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'parser_config.raptor.random_seed'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('randomSeed')}</FormLabel>
|
||||
<FormControl defaultValue={0}>
|
||||
<div className="flex gap-4">
|
||||
<Input {...field} />
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={handleGenerate}
|
||||
type={'button'}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RaptorFormFields;
|
||||
@ -62,18 +62,39 @@ const RaptorFormFields = () => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={UseRaptorField}
|
||||
render={({ field }) => (
|
||||
<FormItem defaultChecked={false}>
|
||||
<FormLabel tooltip={t('useRaptorTip')}>{t('useRaptor')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
if (typeof field.value === 'undefined') {
|
||||
// default value set
|
||||
form.setValue('parser_config.raptor.use_raptor', false);
|
||||
}
|
||||
return (
|
||||
<FormItem
|
||||
defaultChecked={false}
|
||||
className="items-center space-y-0 "
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('useRaptorTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('useRaptor')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{useRaptor && (
|
||||
<div className="space-y-3">
|
||||
@ -81,12 +102,24 @@ const RaptorFormFields = () => {
|
||||
control={form.control}
|
||||
name={'parser_config.raptor.prompt'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('promptTip')}>{t('prompt')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} rows={8} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-start">
|
||||
<FormLabel
|
||||
tooltip={t('promptTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('prompt')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<Textarea {...field} rows={8} />
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@ -119,21 +152,30 @@ const RaptorFormFields = () => {
|
||||
control={form.control}
|
||||
name={'parser_config.raptor.random_seed'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('randomSeed')}</FormLabel>
|
||||
<FormControl defaultValue={0}>
|
||||
<div className="flex gap-4">
|
||||
<Input {...field} />
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={handleGenerate}
|
||||
type={'button'}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
{t('randomSeed')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl defaultValue={0}>
|
||||
<div className="flex gap-4">
|
||||
<Input {...field} />
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={handleGenerate}
|
||||
type={'button'}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
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,38 +38,50 @@ export function SliderInputFormField({
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
defaultValue={defaultValue}
|
||||
defaultValue={defaultValue || 0}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={tooltip}>{label}</FormLabel>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-14 justify-between',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<FormControl>
|
||||
<SingleFormSlider
|
||||
{...field}
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
// defaultValue={
|
||||
// typeof defaultValue === 'number' ? [defaultValue] : undefined
|
||||
// }
|
||||
></SingleFormSlider>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Input
|
||||
type={'number'}
|
||||
className="h-7 w-20"
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
{...field}
|
||||
// defaultValue={defaultValue}
|
||||
></Input>
|
||||
</FormControl>
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={tooltip}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-14 justify-between',
|
||||
'w-3/4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<FormControl>
|
||||
<SingleFormSlider
|
||||
{...field}
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
// defaultValue={
|
||||
// typeof defaultValue === 'number' ? [defaultValue] : undefined
|
||||
// }
|
||||
></SingleFormSlider>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Input
|
||||
type={'number'}
|
||||
className="h-7 w-20"
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
{...field}
|
||||
onChange={(ev) => {
|
||||
const value = ev.target.value;
|
||||
field.onChange(value === '' ? 0 : Number(value)); // convert to number
|
||||
}}
|
||||
// defaultValue={defaultValue}
|
||||
></Input>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
64
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 };
|
||||
};
|
||||
|
||||
export const useFetchKnowledgeBaseConfiguration = () => {
|
||||
export const useFetchKnowledgeBaseConfiguration = (refreshCount?: number) => {
|
||||
const { id } = useParams();
|
||||
|
||||
let queryKey: (KnowledgeApiAction | number)[] = [
|
||||
KnowledgeApiAction.FetchKnowledgeDetail,
|
||||
];
|
||||
if (typeof refreshCount === 'number') {
|
||||
queryKey = [KnowledgeApiAction.FetchKnowledgeDetail, refreshCount];
|
||||
}
|
||||
|
||||
const { data, isFetching: loading } = useQuery<IKnowledge>({
|
||||
queryKey: [KnowledgeApiAction.FetchKnowledgeDetail],
|
||||
queryKey,
|
||||
initialData: {} as IKnowledge,
|
||||
gcTime: 0,
|
||||
queryFn: async () => {
|
||||
|
||||
@ -16,6 +16,7 @@ export interface IUserInfo {
|
||||
nickname: string;
|
||||
password: string;
|
||||
status: string;
|
||||
timezone: string;
|
||||
update_date: string;
|
||||
update_time: number;
|
||||
}
|
||||
|
||||
@ -565,6 +565,7 @@ export default {
|
||||
},
|
||||
setting: {
|
||||
profile: 'Profil',
|
||||
avatar: 'Avatar',
|
||||
profileDescription:
|
||||
'Aktualisieren Sie hier Ihr Foto und Ihre persönlichen Daten.',
|
||||
maxTokens: 'Maximale Tokens',
|
||||
|
||||
@ -551,6 +551,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
},
|
||||
setting: {
|
||||
profile: 'Profile',
|
||||
avatar: 'Avatar',
|
||||
profileDescription: 'Update your photo and personal details here.',
|
||||
maxTokens: 'Max Tokens',
|
||||
maxTokensMessage: 'Max Tokens is required',
|
||||
|
||||
@ -282,6 +282,7 @@ export default {
|
||||
},
|
||||
setting: {
|
||||
profile: 'Perfil',
|
||||
avatar: 'Avatar',
|
||||
profileDescription: 'Actualiza tu foto y tus datos personales aquí.',
|
||||
maxTokens: 'Máximo de tokens',
|
||||
maxTokensMessage: 'El máximo de tokens es obligatorio',
|
||||
|
||||
@ -456,6 +456,7 @@ export default {
|
||||
},
|
||||
setting: {
|
||||
profile: 'Profil',
|
||||
avatar: 'Avatar',
|
||||
profileDescription: 'Perbarui foto dan detail pribadi Anda di sini.',
|
||||
maxTokens: 'Token Maksimum',
|
||||
maxTokensMessage: 'Token Maksimum diperlukan',
|
||||
|
||||
@ -453,6 +453,7 @@ export default {
|
||||
},
|
||||
setting: {
|
||||
profile: 'プロファイル',
|
||||
avatar: 'アバター',
|
||||
profileDescription: 'ここで写真と個人情報を更新してください。',
|
||||
maxTokens: '最大トークン数',
|
||||
maxTokensMessage: '最大トークン数は必須です',
|
||||
|
||||
@ -451,6 +451,7 @@ export default {
|
||||
},
|
||||
setting: {
|
||||
profile: 'Perfil',
|
||||
avatar: 'Avatar',
|
||||
profileDescription: 'Atualize sua foto e detalhes pessoais aqui.',
|
||||
maxTokens: 'Máximo de Tokens',
|
||||
maxTokensMessage: 'Máximo de Tokens é obrigatório',
|
||||
|
||||
@ -505,6 +505,7 @@ export default {
|
||||
},
|
||||
setting: {
|
||||
profile: 'Hồ sơ',
|
||||
avatar: 'Avatar',
|
||||
profileDescription: 'Cập nhật ảnh và thông tin cá nhân của bạn tại đây.',
|
||||
maxTokens: 'Token tối đa',
|
||||
maxTokensMessage: 'Token tối đa là bắt buộc',
|
||||
|
||||
@ -534,6 +534,7 @@ export default {
|
||||
},
|
||||
setting: {
|
||||
profile: '概述',
|
||||
avatar: '头像',
|
||||
profileDescription: '在此更新您的照片和個人詳細信息。',
|
||||
maxTokens: '最大token數',
|
||||
maxTokensMessage: '最大token數是必填項',
|
||||
|
||||
@ -232,7 +232,8 @@ export default {
|
||||
cancel: '取消',
|
||||
methodTitle: '分块方法说明',
|
||||
methodExamples: '示例',
|
||||
methodExamplesDescription: '为帮助您更好地理解,我们提供了相关截图供您参考。',
|
||||
methodExamplesDescription:
|
||||
'为帮助您更好地理解,我们提供了相关截图供您参考。',
|
||||
dialogueExamplesTitle: '对话示例',
|
||||
methodEmpty: '这将显示知识库类别的可视化解释',
|
||||
book: `<p>支持的文件格式为<b>DOCX</b>、<b>PDF</b>、<b>TXT</b>。</p><p>
|
||||
@ -554,6 +555,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
},
|
||||
setting: {
|
||||
profile: '概要',
|
||||
avatar: '头像',
|
||||
profileDescription: '在此更新您的照片和个人详细信息。',
|
||||
maxTokens: '最大token数',
|
||||
maxTokensMessage: '最大token数是必填项',
|
||||
|
||||
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() {
|
||||
const { navigateToDatasetList } = useNavigatePage();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<PageHeader
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2Icon } from 'lucide-react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { useUpdateKnowledge } from '@/hooks/knowledge-hooks';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'umi';
|
||||
import { AudioConfiguration } from './configuration/audio';
|
||||
import { BookConfiguration } from './configuration/book';
|
||||
import { EmailConfiguration } from './configuration/email';
|
||||
@ -42,6 +47,12 @@ function EmptyComponent() {
|
||||
|
||||
export function ChunkMethodForm() {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslation();
|
||||
// const [submitLoading, setSubmitLoading] = useState(false); // submit button loading
|
||||
const { id: kb_id } = useParams();
|
||||
|
||||
const { saveKnowledgeConfiguration, loading: submitLoading } =
|
||||
useUpdateKnowledge();
|
||||
|
||||
const finalParserId: DocumentParserType = useWatch({
|
||||
control: form.control,
|
||||
@ -55,8 +66,41 @@ export function ChunkMethodForm() {
|
||||
}, [finalParserId]);
|
||||
|
||||
return (
|
||||
<section className="overflow-auto max-h-[76vh]">
|
||||
<ConfigurationComponent></ConfigurationComponent>
|
||||
</section>
|
||||
<>
|
||||
<section className="overflow-auto max-h-[76vh]">
|
||||
<ConfigurationComponent></ConfigurationComponent>
|
||||
</section>
|
||||
<div className="text-right pt-4">
|
||||
<Button
|
||||
disabled={submitLoading}
|
||||
onClick={() => {
|
||||
(async () => {
|
||||
try {
|
||||
let beValid = await form.formControl.trigger();
|
||||
console.log('user chunk form: ', form);
|
||||
|
||||
if (beValid) {
|
||||
// setSubmitLoading(true);
|
||||
let postData = form.formState.values;
|
||||
delete postData['avatar']; // has submitted in first form general
|
||||
|
||||
saveKnowledgeConfiguration({
|
||||
...postData,
|
||||
kb_id,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
// setSubmitLoading(false);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
{submitLoading && <Loader2Icon className="animate-spin" />}
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,19 +25,29 @@ export function ChunkMethodItem() {
|
||||
control={form.control}
|
||||
name={'parser_id'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('chunkMethodTip')}>
|
||||
{t('chunkMethod')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
options={parserList}
|
||||
placeholder={t('chunkMethodPlaceholder')}
|
||||
// onChange={handleChunkMethodSelectChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('chunkMethodTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('chunkMethod')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4 ">
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
options={parserList}
|
||||
placeholder={t('chunkMethodPlaceholder')}
|
||||
// onChange={handleChunkMethodSelectChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@ -55,19 +65,29 @@ export function EmbeddingModelItem() {
|
||||
control={form.control}
|
||||
name={'embd_id'}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel tooltip={t('embeddingModelTip')}>
|
||||
{t('embeddingModel')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
options={embeddingModelOptions}
|
||||
disabled={disabled}
|
||||
placeholder={t('embeddingModelPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
tooltip={t('embeddingModelTip')}
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
>
|
||||
{t('embeddingModel')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<RAGFlowSelect
|
||||
{...field}
|
||||
options={embeddingModelOptions}
|
||||
disabled={disabled}
|
||||
placeholder={t('embeddingModelPlaceholder')}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -7,35 +7,67 @@ export const formSchema = z.object({
|
||||
description: z.string().min(2, {
|
||||
message: 'Username must be at least 2 characters.',
|
||||
}),
|
||||
avatar: z.instanceof(File),
|
||||
// avatar: z.instanceof(File),
|
||||
avatar: z.any().nullish(),
|
||||
permission: z.string(),
|
||||
parser_id: z.string(),
|
||||
embd_id: z.string(),
|
||||
parser_config: z.object({
|
||||
layout_recognize: z.string(),
|
||||
chunk_token_num: z.number(),
|
||||
delimiter: z.string(),
|
||||
auto_keywords: z.number(),
|
||||
auto_questions: z.number(),
|
||||
html4excel: z.boolean(),
|
||||
tag_kb_ids: z.array(z.string()),
|
||||
topn_tags: z.number(),
|
||||
raptor: z.object({
|
||||
use_raptor: z.boolean(),
|
||||
prompt: z.string(),
|
||||
max_token: z.number(),
|
||||
threshold: z.number(),
|
||||
max_cluster: z.number(),
|
||||
random_seed: z.number(),
|
||||
}),
|
||||
graphrag: z.object({
|
||||
use_graphrag: z.boolean(),
|
||||
entity_types: z.array(z.string()),
|
||||
method: z.string(),
|
||||
resolution: z.boolean(),
|
||||
community: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
parser_config: z
|
||||
.object({
|
||||
layout_recognize: z.string(),
|
||||
chunk_token_num: z.number(),
|
||||
delimiter: z.string(),
|
||||
auto_keywords: z.number().optional(),
|
||||
auto_questions: z.number().optional(),
|
||||
html4excel: z.boolean(),
|
||||
tag_kb_ids: z.array(z.string()).nullish(),
|
||||
topn_tags: z.number().optional(),
|
||||
raptor: z
|
||||
.object({
|
||||
use_raptor: z.boolean().optional(),
|
||||
prompt: z.string().optional(),
|
||||
max_token: z.number().optional(),
|
||||
threshold: z.number().optional(),
|
||||
max_cluster: z.number().optional(),
|
||||
random_seed: z.number().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.use_raptor && !data.prompt) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Prompt is required',
|
||||
path: ['prompt'],
|
||||
},
|
||||
),
|
||||
graphrag: z
|
||||
.object({
|
||||
use_graphrag: z.boolean().optional(),
|
||||
entity_types: z.array(z.string()).optional(),
|
||||
method: z.string().optional(),
|
||||
resolution: z.boolean().optional(),
|
||||
community: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (
|
||||
data.use_graphrag &&
|
||||
(!data.entity_types || data.entity_types.length === 0)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: 'Please enter Entity types',
|
||||
path: ['entity_types'],
|
||||
},
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
pagerank: z.number(),
|
||||
// icon: z.array(z.instanceof(File)),
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { FileUploader } from '@/components/file-uploader';
|
||||
import { FormContainer } from '@/components/form-container';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
@ -9,100 +10,235 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { useUpdateKnowledge } from '@/hooks/knowledge-hooks';
|
||||
import { transformFile2Base64 } from '@/utils/file-util';
|
||||
import { Loader2Icon, Pencil, Upload } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'umi';
|
||||
|
||||
export function GeneralForm() {
|
||||
const form = useFormContext();
|
||||
const { t } = useTranslation();
|
||||
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
||||
const [avatarBase64Str, setAvatarBase64Str] = useState(''); // Avatar Image base64
|
||||
// const [submitLoading, setSubmitLoading] = useState(false); // submit button loading
|
||||
|
||||
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 (
|
||||
<FormContainer className="space-y-2 p-10">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('knowledgeConfiguration.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field}></Input>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('knowledgeConfiguration.description')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field}></Input>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="avatar"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('knowledgeConfiguration.photo')}</FormLabel>
|
||||
<FormControl>
|
||||
<FileUploader
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
maxFileCount={1}
|
||||
maxSize={4 * 1024 * 1024}
|
||||
// progresses={progresses}
|
||||
// pass the onUpload function here for direct upload
|
||||
// onUpload={uploadFiles}
|
||||
// disabled={isUploading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel tooltip={t('knowledgeConfiguration.permissionsTip')}>
|
||||
{t('knowledgeConfiguration.permissions')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
<>
|
||||
<FormContainer className="space-y-10 p-10">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
<span className="text-red-600">*</span>
|
||||
{t('common.name')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<Input {...field}></Input>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
// null initialize empty string
|
||||
if (typeof field.value === 'object' && !field.value) {
|
||||
form.setValue('description', ' ');
|
||||
}
|
||||
return (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
{t('flow.description')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<Input {...field}></Input>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="avatar"
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
{t('setting.avatar')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<>
|
||||
<div className="relative group">
|
||||
{!avatarBase64Str ? (
|
||||
<div className="w-[64px] h-[64px] grid place-content-center border border-dashed rounded-md">
|
||||
<div className="flex flex-col items-center">
|
||||
<Upload />
|
||||
<p>Upload</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-[64px] h-[64px] relative grid place-content-center">
|
||||
<Avatar className="w-[64px] h-[64px]">
|
||||
<AvatarImage
|
||||
className=" block"
|
||||
src={avatarBase64Str}
|
||||
alt=""
|
||||
/>
|
||||
<AvatarFallback></AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
|
||||
<Pencil
|
||||
size={20}
|
||||
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
placeholder=""
|
||||
// {...field}
|
||||
type="file"
|
||||
title=""
|
||||
accept="image/*"
|
||||
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
onChange={(ev) => {
|
||||
const file = ev.target?.files?.[0];
|
||||
if (
|
||||
/\.(jpg|jpeg|png|webp|bmp)$/i.test(file?.name ?? '')
|
||||
) {
|
||||
setAvatarFile(file!);
|
||||
}
|
||||
ev.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="permission"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-y-0">
|
||||
<FormLabel
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
tooltip={t('knowledgeConfiguration.permissionsTip')}
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="me" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{t('knowledgeConfiguration.me')}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="team" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{t('knowledgeConfiguration.team')}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</FormContainer>
|
||||
{t('knowledgeConfiguration.permissions')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex space-y-1 gap-5"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-1 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="me" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{t('knowledgeConfiguration.me')}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-1 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="team" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{t('knowledgeConfiguration.team')}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</FormContainer>
|
||||
<div className="text-right pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
disabled={submitLoading}
|
||||
onClick={() => {
|
||||
// console.log('form.formControl: ', form.formState.values);
|
||||
(async () => {
|
||||
let isValidate = await form.formControl.trigger('name');
|
||||
// console.log(isValidate);
|
||||
const { name, description, permission } = form.formState.values;
|
||||
const avatar = avatarBase64Str;
|
||||
|
||||
if (isValidate) {
|
||||
saveKnowledgeConfiguration({
|
||||
kb_id,
|
||||
parser_id,
|
||||
name,
|
||||
description,
|
||||
permission,
|
||||
avatar,
|
||||
});
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
{submitLoading && <Loader2Icon className="animate-spin" />}
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
import { ButtonLoading } from '@/components/ui/button';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@/components/ui/tabs-underlined';
|
||||
import { DocumentParserType } from '@/constants/knowledge';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { TopTitle } from '../dataset-title';
|
||||
import CategoryPanel from './category-panel';
|
||||
import { ChunkMethodForm } from './chunk-method-form';
|
||||
import ChunkMethodLearnMore from './chunk-method-learn-more';
|
||||
import { formSchema } from './form-schema';
|
||||
import { GeneralForm } from './general-form';
|
||||
import { useFetchKnowledgeConfigurationOnMount } from './hooks';
|
||||
@ -31,6 +37,7 @@ const enum MethodValue {
|
||||
}
|
||||
|
||||
export default function DatasetSettings() {
|
||||
const { t } = useTranslation();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@ -64,6 +71,10 @@ export default function DatasetSettings() {
|
||||
|
||||
useFetchKnowledgeConfigurationOnMount(form);
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<
|
||||
'generalForm' | 'chunkMethodForm'
|
||||
>('generalForm'); // currnet Tab state
|
||||
|
||||
const parserId = useWatch({
|
||||
control: form.control,
|
||||
name: 'parser_id',
|
||||
@ -76,35 +87,66 @@ export default function DatasetSettings() {
|
||||
return (
|
||||
<section className="p-5 ">
|
||||
<TopTitle
|
||||
title={'Configuration'}
|
||||
description={` Update your knowledge base configuration here, particularly the chunk
|
||||
method.`}
|
||||
title={t('knowledgeDetails.configuration')}
|
||||
description={t('knowledgeConfiguration.titleDescription')}
|
||||
></TopTitle>
|
||||
<div className="flex gap-14">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 basis-full"
|
||||
className="space-y-6 basis-full min-w-[1000px] max-w-[1000px]"
|
||||
>
|
||||
<Tabs defaultValue="account">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="account">Account</TabsTrigger>
|
||||
<TabsTrigger value="password">Password</TabsTrigger>
|
||||
<Tabs
|
||||
defaultValue="generalForm"
|
||||
onValueChange={(val) => {
|
||||
setCurrentTab(val);
|
||||
}}
|
||||
>
|
||||
<TabsList className="grid w-full bg-background grid-cols-2 rounded-none bg-[#161618]">
|
||||
<TabsTrigger
|
||||
value="generalForm"
|
||||
className="group bg-transparent p-0 !border-transparent"
|
||||
>
|
||||
<div className="flex w-full h-full justify-center items-center bg-[#161618]">
|
||||
<span className="h-full group-data-[state=active]:border-b-2 border-white ">
|
||||
General
|
||||
</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="chunkMethodForm"
|
||||
className="group bg-transparent p-0 !border-transparent"
|
||||
>
|
||||
<div className="flex w-full h-full justify-center items-center bg-[#161618]">
|
||||
<span className="h-full group-data-[state=active]:border-b-2 border-white ">
|
||||
Chunk Method
|
||||
</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="account">
|
||||
<TabsContent value="generalForm">
|
||||
<GeneralForm></GeneralForm>
|
||||
</TabsContent>
|
||||
<TabsContent value="password">
|
||||
<TabsContent value="chunkMethodForm">
|
||||
<ChunkMethodForm></ChunkMethodForm>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className="text-right">
|
||||
{/* <div className="text-right">
|
||||
<ButtonLoading type="submit">Submit</ButtonLoading>
|
||||
</div>
|
||||
</div> */}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<CategoryPanel chunkMethod={parserId}></CategoryPanel>
|
||||
<ChunkMethodLearnMore tab={currentTab} parserId={parserId} />
|
||||
{/* <div
|
||||
style={{
|
||||
display: currentTab === 'chunkMethodForm' ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<Button variant="outline">Learn More</Button>
|
||||
<div className="bg-[#FFF]/10 p-[20px] rounded-[12px] mt-[10px]">
|
||||
<CategoryPanel chunkMethod={parserId}></CategoryPanel>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -38,31 +38,39 @@ export const TagSetItem = () => {
|
||||
control={form.control}
|
||||
name="parser_config.tag_kb_ids"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel
|
||||
tooltip={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
t('knowledgeConfiguration.tagSetTip'),
|
||||
),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
>
|
||||
{t('knowledgeConfiguration.tagSet')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={knowledgeOptions}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('chat.knowledgeBasesMessage')}
|
||||
variant="inverted"
|
||||
maxCount={0}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
|
||||
tooltip={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
t('knowledgeConfiguration.tagSetTip'),
|
||||
),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
>
|
||||
{t('knowledgeConfiguration.tagSet')}
|
||||
</FormLabel>
|
||||
<div className="w-3/4">
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={knowledgeOptions}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('chat.knowledgeBasesMessage')}
|
||||
variant="inverted"
|
||||
maxCount={0}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -10,10 +10,15 @@ import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHandleMenuClick } from './hooks';
|
||||
|
||||
export function SideBar() {
|
||||
type PropType = {
|
||||
refreshCount?: number;
|
||||
};
|
||||
|
||||
export function SideBar({ refreshCount }: PropType) {
|
||||
const pathName = useSecondPathName();
|
||||
const { handleMenuClick } = useHandleMenuClick();
|
||||
const { data } = useFetchKnowledgeBaseConfiguration();
|
||||
// refreshCount: be for avatar img sync update on top left
|
||||
const { data } = useFetchKnowledgeBaseConfiguration(refreshCount);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items = useMemo(() => {
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useTestRetrieval } from '@/hooks/use-knowledge-request';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { TopTitle } from '../dataset-title';
|
||||
import TestingForm from './testing-form';
|
||||
@ -41,7 +39,7 @@ export default function RetrievalTesting() {
|
||||
description={` Update your knowledge base configuration here, particularly the chunk
|
||||
method.`}
|
||||
></TopTitle>
|
||||
<Button>Save as Preset</Button>
|
||||
{/* <Button>Save as Preset</Button> */}
|
||||
</section>
|
||||
{count === 1 ? (
|
||||
<section className="flex divide-x h-full">
|
||||
@ -50,9 +48,9 @@ export default function RetrievalTesting() {
|
||||
<span className="text-text-title font-semibold text-2xl">
|
||||
Test setting
|
||||
</span>
|
||||
<Button variant={'outline'} onClick={addCount}>
|
||||
{/* <Button variant={'outline'} onClick={addCount}>
|
||||
<Plus /> Add New Test
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
<TestingForm
|
||||
loading={loading}
|
||||
|
||||
@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CrossLanguageItem } from '@/components/cross-language-item-ui';
|
||||
import { FormContainer } from '@/components/form-container';
|
||||
import {
|
||||
initialTopKValue,
|
||||
@ -30,7 +31,8 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item';
|
||||
import { useTestRetrieval } from '@/hooks/use-knowledge-request';
|
||||
import { trim } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
import { CirclePlay } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type TestingFormProps = Pick<
|
||||
@ -44,6 +46,7 @@ export default function TestingForm({
|
||||
setValues,
|
||||
}: TestingFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const [cross_languages, setCrossLangArr] = useState<string[]>([]);
|
||||
|
||||
const formSchema = z.object({
|
||||
question: z.string().min(1, {
|
||||
@ -68,8 +71,9 @@ export default function TestingForm({
|
||||
const values = useWatch({ control: form.control });
|
||||
|
||||
useEffect(() => {
|
||||
setValues(values as Required<z.infer<typeof formSchema>>);
|
||||
}, [setValues, values]);
|
||||
// setValues(values as Required<z.infer<typeof formSchema>>);
|
||||
setValues({ ...values, cross_languages });
|
||||
}, [setValues, values, cross_languages]);
|
||||
|
||||
function onSubmit() {
|
||||
refetch();
|
||||
@ -85,6 +89,12 @@ export default function TestingForm({
|
||||
></SimilaritySliderFormField>
|
||||
<RerankFormFields></RerankFormFields>
|
||||
<UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField>
|
||||
<CrossLanguageItem
|
||||
name={'cross_languages'}
|
||||
onChange={(valArr) => {
|
||||
setCrossLangArr(valArr);
|
||||
}}
|
||||
></CrossLanguageItem>
|
||||
</FormContainer>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@ -103,13 +113,16 @@ export default function TestingForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
disabled={!!!trim(question)}
|
||||
loading={loading}
|
||||
>
|
||||
{t('knowledgeDetails.testingLabel')}
|
||||
</ButtonLoading>
|
||||
<div className="flex justify-end">
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
disabled={!!!trim(question)}
|
||||
loading={loading}
|
||||
>
|
||||
{!loading && <CirclePlay />}
|
||||
{t('knowledgeDetails.testingLabel')}
|
||||
</ButtonLoading>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import PasswordInput from '@/components/password-input';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@ -8,59 +17,390 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useTranslate } from '@/hooks/common-hooks';
|
||||
import { useFetchUserInfo, useSaveSetting } from '@/hooks/user-setting-hooks';
|
||||
import { TimezoneList } from '@/pages/user-setting/constants';
|
||||
import { rsaPsw } from '@/utils';
|
||||
import { transformFile2Base64 } from '@/utils/file-util';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { TFunction } from 'i18next';
|
||||
import { Loader2Icon, Pencil, Upload } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
function defineSchema(t: TFunction<'translation', string>) {
|
||||
return z
|
||||
.object({
|
||||
userName: z
|
||||
.string()
|
||||
.min(1, {
|
||||
message: t('usernameMessage'),
|
||||
})
|
||||
.trim(),
|
||||
avatarUrl: z.string().trim(),
|
||||
timeZone: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, {
|
||||
message: t('timezonePlaceholder'),
|
||||
}),
|
||||
email: z
|
||||
.string({
|
||||
required_error: 'Please select an email to display.',
|
||||
})
|
||||
.trim()
|
||||
.regex(
|
||||
/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/,
|
||||
{
|
||||
message: 'Enter a valid email address.',
|
||||
},
|
||||
),
|
||||
currPasswd: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, {
|
||||
message: t('currentPasswordMessage'),
|
||||
}),
|
||||
newPasswd: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(8, {
|
||||
message: t('confirmPasswordMessage'),
|
||||
}),
|
||||
confirmPasswd: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(8, {
|
||||
message: t('newPasswordDescription'),
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.newPasswd === data.confirmPasswd, {
|
||||
message: t('confirmPasswordNonMatchMessage'),
|
||||
path: ['confirmPasswd'],
|
||||
});
|
||||
}
|
||||
|
||||
export default function Profile() {
|
||||
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 (
|
||||
<section className="p-8">
|
||||
<h1 className="text-3xl font-bold mb-6">User profile</h1>
|
||||
<Avatar className="w-[120px] h-[120px] mb-6">
|
||||
<AvatarImage
|
||||
src={
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg'
|
||||
}
|
||||
alt="Profile"
|
||||
/>
|
||||
<AvatarFallback>YW</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="space-y-6 max-w-[600px]">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-muted-foreground">User name</label>
|
||||
<Input defaultValue="username" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-muted-foreground">Email</label>
|
||||
<Input defaultValue="address@example.com" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-muted-foreground">Language</label>
|
||||
<Select defaultValue="english">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="english">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-muted-foreground">Timezone</label>
|
||||
<Select defaultValue="utc9">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="utc9">UTC+9 Asia/Shanghai</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" className="mt-4">
|
||||
Change password
|
||||
</Button>
|
||||
<h1 className="text-3xl font-bold">{t('profile')}</h1>
|
||||
<div className="text-sm text-muted-foreground mb-6">
|
||||
{t('profileDescription')}
|
||||
</div>
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="block space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="userName"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0 ">
|
||||
<div className="flex w-[600px]">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
<span className="text-red-600">*</span>
|
||||
{t('username')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<Input
|
||||
placeholder=""
|
||||
{...field}
|
||||
className="bg-colors-background-inverse-weak"
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex w-[600px] pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="avatarUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-y-0">
|
||||
<div className="flex w-[600px]">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
Avatar
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<>
|
||||
<div className="relative group">
|
||||
{!avatarBase64Str ? (
|
||||
<div className="w-[64px] h-[64px] grid place-content-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<Upload />
|
||||
<p>Upload</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-[64px] h-[64px] relative grid place-content-center">
|
||||
<Avatar className="w-[64px] h-[64px]">
|
||||
<AvatarImage
|
||||
className=" block"
|
||||
src={avatarBase64Str}
|
||||
alt=""
|
||||
/>
|
||||
<AvatarFallback></AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="absolute inset-0 bg-[#000]/20 group-hover:bg-[#000]/60">
|
||||
<Pencil
|
||||
size={20}
|
||||
className="absolute right-2 bottom-0 opacity-50 hidden group-hover:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
placeholder=""
|
||||
{...field}
|
||||
type="file"
|
||||
title=""
|
||||
accept="image/*"
|
||||
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
|
||||
onChange={(ev) => {
|
||||
const file = ev.target?.files?.[0];
|
||||
if (
|
||||
/\.(jpg|jpeg|png|webp|bmp)$/i.test(
|
||||
file?.name ?? '',
|
||||
)
|
||||
) {
|
||||
setAvatarFile(file!);
|
||||
}
|
||||
ev.target.value = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex w-[600px] pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeZone"
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex w-[600px]">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
<span className="text-red-600">*</span>
|
||||
{t('timezone')}
|
||||
</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl className="w-3/4">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a timeZone" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{TimezoneList.map((timeStr) => (
|
||||
<SelectItem key={timeStr} value={timeStr}>
|
||||
{timeStr}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex w-[600px] pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div className="flex w-[600px]">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
|
||||
{t('email')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/4">
|
||||
<Input
|
||||
placeholder="Alex@gmail.com"
|
||||
disabled
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex w-[600px] pt-1">
|
||||
<div className="w-1/4"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
<div className="flex w-[600px] pt-1">
|
||||
<p className="w-1/4"> </p>
|
||||
<p className="text-sm text-muted-foreground whitespace-nowrap w-3/4">
|
||||
{t('emailDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<div className="h-[10px]"></div>
|
||||
<div className="pb-6">
|
||||
<h1 className="text-3xl font-bold">{t('password')}</h1>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('passwordDescription')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-0 overflow-hidden absolute">
|
||||
<input type="password" className=" w-0 height-0 opacity-0" />
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="currPasswd"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0">
|
||||
<div className="flex w-[600px]">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
|
||||
<span className="text-red-600">*</span>
|
||||
{t('currentPassword')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/5">
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex w-[600px] pt-1">
|
||||
<div className="min-w-[170px] max-w-[170px]"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newPasswd"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0">
|
||||
<div className="flex w-[600px]">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
|
||||
<span className="text-red-600">*</span>
|
||||
{t('newPassword')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/5">
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex w-[600px] pt-1">
|
||||
<div className="min-w-[170px] max-w-[170px]"></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPasswd"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" items-center space-y-0">
|
||||
<div className="flex w-[600px]">
|
||||
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-2/5">
|
||||
<span className="text-red-600">*</span>
|
||||
{t('confirmPassword')}
|
||||
</FormLabel>
|
||||
<FormControl className="w-3/5">
|
||||
<PasswordInput
|
||||
{...field}
|
||||
onBlur={() => {
|
||||
form.trigger('confirmPasswd');
|
||||
}}
|
||||
onChange={(ev) => {
|
||||
form.setValue(
|
||||
'confirmPasswd',
|
||||
ev.target.value.trim(),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className="flex w-[600px] pt-1">
|
||||
<div className="min-w-[170px] max-w-[170px]"> </div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="w-[600px] text-right space-x-4">
|
||||
<Button variant="secondary">{t('cancel')}</Button>
|
||||
<Button type="submit" disabled={submitLoading}>
|
||||
{submitLoading && <Loader2Icon className="animate-spin" />}
|
||||
{t('save', { keyPrefix: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { ProfileSettingRouteKey } from '@/constants/setting';
|
||||
import { useLogout } from '@/hooks/login-hooks';
|
||||
import { useSecondPathName } from '@/hooks/route-hook';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
@ -54,6 +55,8 @@ export function SideBar() {
|
||||
const { setTheme } = useTheme();
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
|
||||
const { logout } = useLogout();
|
||||
|
||||
const handleThemeChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
setTheme(checked ? 'dark' : 'light');
|
||||
@ -99,7 +102,13 @@ export function SideBar() {
|
||||
Dark
|
||||
</Label>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-3"
|
||||
onClick={() => {
|
||||
logout();
|
||||
}}
|
||||
>
|
||||
<LogOut className="w-6 h-6" />
|
||||
Logout
|
||||
</Button>
|
||||
|
||||
@ -49,6 +49,7 @@ const routes = [
|
||||
{
|
||||
path: '/knowledge',
|
||||
component: '@/pages/knowledge',
|
||||
// component: '@/pages/knowledge/datasets',
|
||||
},
|
||||
{
|
||||
path: '/knowledge',
|
||||
@ -93,6 +94,7 @@ const routes = [
|
||||
{ path: '/user-setting', redirect: '/user-setting/profile' },
|
||||
{
|
||||
path: '/user-setting/profile',
|
||||
// component: '@/pages/user-setting/setting-profile',
|
||||
component: '@/pages/user-setting/setting-profile',
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user