chat quote reader (#3912)
* init chat quote full text reader * linked structure * dataset data linked * optimize code * fix ts build * test finish * delete log * fix * fix ts * fix ts * remove nextId * initial scroll * fix * fix
1
packages/global/core/chat/type.d.ts
vendored
@ -134,6 +134,7 @@ export type ChatItemType = (UserChatItemType | SystemChatItemType | AIChatItemTy
|
||||
|
||||
// Frontend type
|
||||
export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatItemType) & {
|
||||
_id?: string;
|
||||
dataId: string;
|
||||
status: `${ChatStatusEnum}`;
|
||||
moduleName?: string;
|
||||
|
||||
7
packages/global/core/dataset/type.d.ts
vendored
@ -112,12 +112,15 @@ export type DatasetDataSchemaType = {
|
||||
tmbId: string;
|
||||
datasetId: string;
|
||||
collectionId: string;
|
||||
datasetId: string;
|
||||
collectionId: string;
|
||||
chunkIndex: number;
|
||||
updateTime: Date;
|
||||
q: string; // large chunks or question
|
||||
a: string; // answer or custom content
|
||||
history?: {
|
||||
q: string;
|
||||
a: string;
|
||||
updateTime: Date;
|
||||
}[];
|
||||
forbid?: boolean;
|
||||
fullTextToken: string;
|
||||
indexes: DatasetDataIndexItemType[];
|
||||
|
||||
3
packages/global/support/outLink/type.d.ts
vendored
@ -63,6 +63,8 @@ export type OutLinkSchema<T extends OutlinkAppType = undefined> = {
|
||||
responseDetail: boolean;
|
||||
// whether to hide the node status
|
||||
showNodeStatus?: boolean;
|
||||
// wheter to show the full text reader
|
||||
// showFullText?: boolean;
|
||||
// whether to show the complete quote
|
||||
showRawSource?: boolean;
|
||||
|
||||
@ -89,6 +91,7 @@ export type OutLinkEditType<T = undefined> = {
|
||||
name: string;
|
||||
responseDetail?: OutLinkSchema<T>['responseDetail'];
|
||||
showNodeStatus?: OutLinkSchema<T>['showNodeStatus'];
|
||||
// showFullText?: OutLinkSchema<T>['showFullText'];
|
||||
showRawSource?: OutLinkSchema<T>['showRawSource'];
|
||||
// response when request
|
||||
immediateResponse?: string;
|
||||
|
||||
@ -15,6 +15,7 @@ import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
|
||||
import { pushChatLog } from './pushChatLog';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
|
||||
type Props = {
|
||||
chatId: string;
|
||||
@ -74,8 +75,42 @@ export async function saveChat({
|
||||
)?.inputs;
|
||||
|
||||
await mongoSessionRun(async (session) => {
|
||||
const processedContent = content.map((item) => {
|
||||
if (item.obj === ChatRoleEnum.AI) {
|
||||
const nodeResponse = item[DispatchNodeResponseKeyEnum.nodeResponse];
|
||||
|
||||
if (nodeResponse) {
|
||||
return {
|
||||
...item,
|
||||
[DispatchNodeResponseKeyEnum.nodeResponse]: nodeResponse.map((responseItem) => {
|
||||
if (
|
||||
responseItem.moduleType === FlowNodeTypeEnum.datasetSearchNode &&
|
||||
responseItem.quoteList
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
quoteList: responseItem.quoteList.map((quote: any) => ({
|
||||
id: quote.id,
|
||||
chunkIndex: quote.chunkIndex,
|
||||
datasetId: quote.datasetId,
|
||||
collectionId: quote.collectionId,
|
||||
sourceId: quote.sourceId,
|
||||
sourceName: quote.sourceName,
|
||||
score: quote.score,
|
||||
tokens: quote.tokens
|
||||
}))
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
const [{ _id: chatItemIdHuman }, { _id: chatItemIdAi }] = await MongoChatItem.insertMany(
|
||||
content.map((item) => ({
|
||||
processedContent.map((item) => ({
|
||||
chatId,
|
||||
teamId,
|
||||
tmbId,
|
||||
|
||||
@ -40,6 +40,15 @@ const DatasetDataSchema = new Schema({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
history: {
|
||||
type: [
|
||||
{
|
||||
q: String,
|
||||
a: String,
|
||||
updateTime: Date
|
||||
}
|
||||
]
|
||||
},
|
||||
indexes: {
|
||||
type: [
|
||||
{
|
||||
|
||||
@ -51,6 +51,9 @@ const OutLinkSchema = new Schema({
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// showFullText: {
|
||||
// type: Boolean
|
||||
// },
|
||||
showRawSource: {
|
||||
type: Boolean
|
||||
},
|
||||
|
||||
19
packages/web/common/fetch/type.d.ts
vendored
@ -11,3 +11,22 @@ type PaginationResponse<T = {}> = {
|
||||
total: number;
|
||||
list: T[];
|
||||
};
|
||||
|
||||
type LinkedPaginationProps<T = {}> = T & {
|
||||
pageSize: number;
|
||||
} & RequireOnlyOne<{
|
||||
initialId: string;
|
||||
nextId: string;
|
||||
prevId: string;
|
||||
}> &
|
||||
RequireOnlyOne<{
|
||||
initialIndex: number;
|
||||
nextIndex: number;
|
||||
prevIndex: number;
|
||||
}>;
|
||||
|
||||
type LinkedListResponse<T = {}> = {
|
||||
list: Array<T & { _id: string; index: number }>;
|
||||
hasMorePrev: boolean;
|
||||
hasMoreNext: boolean;
|
||||
};
|
||||
|
||||
@ -35,10 +35,13 @@ export const iconPaths = {
|
||||
'common/dingtalkFill': () => import('./icons/common/dingtalkFill.svg'),
|
||||
'common/disable': () => import('./icons/common/disable.svg'),
|
||||
'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'),
|
||||
'common/download': () => import('./icons/common/download.svg'),
|
||||
'common/edit': () => import('./icons/common/edit.svg'),
|
||||
'common/editor/resizer': () => import('./icons/common/editor/resizer.svg'),
|
||||
'common/enable': () => import('./icons/common/enable.svg'),
|
||||
'common/errorFill': () => import('./icons/common/errorFill.svg'),
|
||||
'common/file/move': () => import('./icons/common/file/move.svg'),
|
||||
'common/fileNotFound': () => import('./icons/common/fileNotFound.svg'),
|
||||
'common/folderFill': () => import('./icons/common/folderFill.svg'),
|
||||
'common/folderImport': () => import('./icons/common/folderImport.svg'),
|
||||
'common/fullScreenLight': () => import('./icons/common/fullScreenLight.svg'),
|
||||
@ -86,7 +89,9 @@ export const iconPaths = {
|
||||
'common/selectLight': () => import('./icons/common/selectLight.svg'),
|
||||
'common/setting': () => import('./icons/common/setting.svg'),
|
||||
'common/settingLight': () => import('./icons/common/settingLight.svg'),
|
||||
'common/solidChevronDown': () => import('./icons/common/solidChevronDown.svg'),
|
||||
'common/solidChevronRight': () => import('./icons/common/solidChevronRight.svg'),
|
||||
'common/solidChevronUp': () => import('./icons/common/solidChevronUp.svg'),
|
||||
'common/subtract': () => import('./icons/common/subtract.svg'),
|
||||
'common/templateMarket': () => import('./icons/common/templateMarket.svg'),
|
||||
'common/text/t': () => import('./icons/common/text/t.svg'),
|
||||
@ -96,6 +101,7 @@ export const iconPaths = {
|
||||
'common/trash': () => import('./icons/common/trash.svg'),
|
||||
'common/upRightArrowLight': () => import('./icons/common/upRightArrowLight.svg'),
|
||||
'common/uploadFileFill': () => import('./icons/common/uploadFileFill.svg'),
|
||||
'common/upperRight': () => import('./icons/common/upperRight.svg'),
|
||||
'common/userInfo': () => import('./icons/common/userInfo.svg'),
|
||||
'common/variable': () => import('./icons/common/variable.svg'),
|
||||
'common/viewLight': () => import('./icons/common/viewLight.svg'),
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.2703 9.04924C15.2703 10.8968 13.8022 12.4015 11.9689 12.4602V12.462H11.7009C11.3327 12.462 11.0342 12.1635 11.0342 11.7953C11.0342 11.4271 11.3327 11.1287 11.7009 11.1287H11.8689C13.0121 11.1226 13.937 10.1939 13.937 9.04924C13.937 8.07583 13.2669 7.25561 12.361 7.03087L11.5956 6.84096L11.3934 6.0787C10.9949 4.57696 9.62484 3.47188 8.00003 3.47188C6.37523 3.47188 5.00513 4.57696 4.60668 6.0787L4.40444 6.84096L3.63901 7.03087C2.73317 7.25561 2.06307 8.07583 2.06307 9.04924C2.06307 10.1977 2.99405 11.1287 4.14248 11.1287L4.14588 11.1286L4.33524 11.1287C4.70343 11.1287 5.00191 11.4271 5.00191 11.7953C5.00191 12.1635 4.70343 12.462 4.33524 12.462H4.14248H4.06858V12.4612C2.2179 12.4219 0.729736 10.9093 0.729736 9.04924C0.729736 7.44874 1.83149 6.10557 3.31794 5.73677C3.86757 3.6652 5.75552 2.13855 8.00003 2.13855C10.2445 2.13855 12.1325 3.6652 12.6821 5.73677C14.1686 6.10557 15.2703 7.44874 15.2703 9.04924ZM8.00002 7.30026C7.63183 7.30026 7.33335 7.59874 7.33335 7.96693V11.8435L6.96682 11.477C6.74816 11.2583 6.39364 11.2583 6.17498 11.477C5.95632 11.6957 5.95632 12.0502 6.17498 12.2688L7.50545 13.5993C7.52049 13.6159 7.53634 13.6318 7.55296 13.6468L7.60322 13.6971C7.82237 13.9162 8.17767 13.9162 8.39682 13.6971L8.44709 13.6468C8.4637 13.6318 8.47955 13.6159 8.49457 13.5993L9.82418 12.2697C10.0433 12.0506 10.0433 11.6953 9.82418 11.4761C9.60503 11.257 9.24973 11.257 9.03058 11.4761L8.66668 11.84V7.96693C8.66668 7.59874 8.36821 7.30026 8.00002 7.30026Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none">
|
||||
<path d="M0.246607 10.0893C0.241922 10.0968 0.237518 10.1045 0.23341 10.1124C0.194824 10.1867 0.194824 10.2843 0.194824 10.4795V10.7803C0.194824 10.9755 0.194824 11.0731 0.23341 11.1473C0.265926 11.2099 0.316964 11.261 0.37956 11.2935C0.453841 11.3321 0.55143 11.3321 0.746607 11.3321H1.04222C1.20929 11.3321 1.30485 11.3321 1.3755 11.3079C1.38016 11.3066 1.38481 11.3052 1.38944 11.3037C1.50916 11.2658 1.61267 11.1623 1.81968 10.9553L8.4032 4.37179L7.15796 3.12655L0.574444 9.71007C0.397236 9.88728 0.29587 9.98864 0.246607 10.0893Z" />
|
||||
<path d="M9.43317 3.34182L8.18793 2.09658L9.34895 0.935566C9.36436 0.920152 9.37233 0.912183 9.37902 0.905775C9.71036 0.588537 10.2328 0.588537 10.5641 0.905775C10.5708 0.91219 10.5786 0.919938 10.594 0.935383L10.5946 0.935931C10.6099 0.951255 10.6176 0.958969 10.624 0.965642C10.9412 1.29698 10.9412 1.81939 10.624 2.15073C10.6175 2.15745 10.6098 2.16524 10.5942 2.1808L9.43317 3.34182Z" />
|
||||
<path d="M4.47921 10.6383C4.47921 10.2552 4.78981 9.9446 5.17296 9.9446H11.1114C11.4946 9.9446 11.8052 10.2552 11.8052 10.6383C11.8052 11.0215 11.4946 11.3321 11.1114 11.3321H5.17296C4.78981 11.3321 4.47921 11.0215 4.47921 10.6383Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M4 5H26V10H4V5Z" fill="#ECECEC"/>
|
||||
<path d="M4 12H26V17H4V12Z" fill="#ECECEC"/>
|
||||
<path d="M4 19H26V24H4V19Z" fill="#ECECEC"/>
|
||||
<path d="M21.8982 27.7967C18.0945 27.7967 15 24.7021 15 20.8983C15 17.0947 18.0945 14 21.8982 14C25.7019 14 28.7965 17.0947 28.7965 20.8983C28.7965 24.702 25.7019 27.7967 21.8982 27.7967ZM21.8982 15.698C19.0307 15.698 16.698 18.0309 16.698 20.8983C16.698 23.7658 19.0307 26.0988 21.8982 26.0988C24.7657 26.0988 27.0986 23.7658 27.0986 20.8983C27.0986 18.0309 24.7657 15.698 21.8982 15.698Z" fill="#BEC2C9"/>
|
||||
<path d="M29.9551 30C29.6876 30 29.4203 29.8979 29.2162 29.694L25.6122 26.0902C25.2042 25.6821 25.2042 25.0205 25.6122 24.6124C26.0204 24.2044 26.6819 24.2044 27.09 24.6124L30.694 28.2162C31.102 28.6242 31.102 29.2859 30.694 29.694C30.4899 29.8979 30.2225 30 29.9551 30Z" fill="#BEC2C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 942 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19 18" fill="none">
|
||||
<path d="M13.4032 6C14.7396 6 15.4088 7.61571 14.4639 8.56066L10.6388 12.3858C10.053 12.9715 9.10324 12.9715 8.51745 12.3858L4.69235 8.56066C3.7474 7.61571 4.41665 6 5.75301 6H13.4032Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 270 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19 18" fill="none">
|
||||
<path d="M13.4032 12.0751C14.7396 12.0751 15.4088 10.4594 14.4639 9.51441L10.6388 5.68931C10.053 5.10352 9.10324 5.10352 8.51745 5.68931L4.69235 9.51441C3.7474 10.4594 4.41665 12.0751 5.75301 12.0751H13.4032Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M5.44766 4.22876C5.44765 4.59695 5.74613 4.89542 6.11432 4.89542L10.1617 4.89542L3.7573 11.2998C3.49695 11.5602 3.49695 11.9823 3.7573 12.2426C4.01765 12.503 4.43976 12.503 4.70011 12.2426L11.1045 5.83823L11.1045 9.88561C11.1045 10.2538 11.403 10.5523 11.7712 10.5523C12.1394 10.5523 12.4378 10.2538 12.4378 9.88561L12.4378 4.22876C12.4378 3.86057 12.1394 3.56209 11.7712 3.56209L6.11432 3.56209C5.74613 3.56209 5.44765 3.86057 5.44766 4.22876Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 539 B |
@ -1 +1,3 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1698497259520" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10081" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M156.09136 606.57001a457.596822 457.596822 0 0 1 221.680239-392.516385 50.844091 50.844091 0 1 1 50.844091 86.943396 355.90864 355.90864 0 0 0-138.804369 152.532274h16.77855a152.532274 152.532274 0 1 1-152.532274 152.532274z m406.752731 0a457.596822 457.596822 0 0 1 221.680239-392.007944 50.844091 50.844091 0 1 1 50.844091 86.943396 355.90864 355.90864 0 0 0-138.804369 152.532274h16.77855a152.532274 152.532274 0 1 1-152.532274 152.532274z" fill="#E67E22" p-id="10082"></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M11.2308 13.6099V18.2252C11.2308 18.8663 11.0064 19.4111 10.5577 19.8599C10.109 20.3086 9.5641 20.5329 8.92307 20.5329H4.30769C3.66667 20.5329 3.12179 20.3086 2.67308 19.8599C2.22436 19.4111 2 18.8663 2 18.2252V9.76371C2 8.93038 2.16226 8.1351 2.48678 7.37789C2.8113 6.62068 3.25 5.96563 3.80288 5.41275C4.35577 4.85986 5.01082 4.42116 5.76803 4.09664C6.52524 3.77212 7.32051 3.60986 8.15384 3.60986H8.92307C9.13141 3.60986 9.3117 3.68598 9.46394 3.83823C9.61618 3.99047 9.6923 4.17076 9.6923 4.37909V5.91756C9.6923 6.12589 9.61618 6.30618 9.46394 6.45842C9.3117 6.61066 9.13141 6.68679 8.92307 6.68679H8.15384C7.30448 6.68679 6.57933 6.98727 5.97836 7.58823C5.3774 8.18919 5.07692 8.91435 5.07692 9.76371V10.1483C5.07692 10.4688 5.1891 10.7413 5.41346 10.9656C5.63782 11.19 5.91025 11.3022 6.23077 11.3022H8.92307C9.5641 11.3022 10.109 11.5265 10.5577 11.9752C11.0064 12.424 11.2308 12.9688 11.2308 13.6099ZM22 13.6099V18.2252C22 18.8663 21.7756 19.4111 21.3269 19.8599C20.8782 20.3086 20.3333 20.5329 19.6923 20.5329H15.0769C14.4359 20.5329 13.891 20.3086 13.4423 19.8599C12.9936 19.4111 12.7692 18.8663 12.7692 18.2252V9.76371C12.7692 8.93038 12.9315 8.1351 13.256 7.37789C13.5805 6.62068 14.0192 5.96563 14.5721 5.41275C15.125 4.85986 15.78 4.42116 16.5373 4.09664C17.2945 3.77212 18.0897 3.60986 18.9231 3.60986H19.6923C19.9006 3.60986 20.0809 3.68598 20.2332 3.83823C20.3854 3.99047 20.4615 4.17076 20.4615 4.37909V5.91756C20.4615 6.12589 20.3854 6.30618 20.2332 6.45842C20.0809 6.61066 19.9006 6.68679 19.6923 6.68679H18.9231C18.0737 6.68679 17.3486 6.98727 16.7476 7.58823C16.1466 8.18919 15.8461 8.91435 15.8461 9.76371V10.1483C15.8461 10.4688 15.9583 10.7413 16.1827 10.9656C16.407 11.19 16.6795 11.3022 17 11.3022H19.6923C20.3333 11.3022 20.8782 11.5265 21.3269 11.9752C21.7756 12.424 22 12.9688 22 13.6099Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 819 B After Width: | Height: | Size: 1.9 KiB |
307
packages/web/hooks/useLinkedScroll.tsx
Normal file
@ -0,0 +1,307 @@
|
||||
import { useCallback, useEffect, useRef, useState, ReactNode } from 'react';
|
||||
import { LinkedListResponse, LinkedPaginationProps } from '../common/fetch/type';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useScroll, useMemoizedFn, useDebounceEffect } from 'ahooks';
|
||||
import MyBox from '../components/common/MyBox';
|
||||
import { useRequest2 } from './useRequest';
|
||||
|
||||
const threshold = 100;
|
||||
|
||||
export function useLinkedScroll<
|
||||
TParams extends LinkedPaginationProps & { isInitialLoad?: boolean },
|
||||
TData extends LinkedListResponse
|
||||
>(
|
||||
api: (data: TParams) => Promise<TData>,
|
||||
{
|
||||
refreshDeps = [],
|
||||
pageSize = 15,
|
||||
params = {},
|
||||
initialId,
|
||||
initialIndex,
|
||||
canLoadData = false
|
||||
}: {
|
||||
refreshDeps?: any[];
|
||||
pageSize?: number;
|
||||
params?: Record<string, any>;
|
||||
initialId?: string;
|
||||
initialIndex?: number;
|
||||
canLoadData?: boolean;
|
||||
}
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const [dataList, setDataList] = useState<TData['list']>([]);
|
||||
const [hasMorePrev, setHasMorePrev] = useState(true);
|
||||
const [hasMoreNext, setHasMoreNext] = useState(true);
|
||||
const [initialLoadDone, setInitialLoadDone] = useState(false);
|
||||
const hasScrolledToInitial = useRef(false);
|
||||
|
||||
const anchorRef = useRef({
|
||||
top: null as { _id: string; index: number } | null,
|
||||
bottom: null as { _id: string; index: number } | null
|
||||
});
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const itemRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
const { runAsync: callApi, loading: isLoading } = useRequest2(
|
||||
async (apiParams: TParams) => await api(apiParams),
|
||||
{
|
||||
onError: (error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const loadData = useCallback(
|
||||
async ({
|
||||
id,
|
||||
index,
|
||||
isInitialLoad = false
|
||||
}: {
|
||||
id: string;
|
||||
index: number;
|
||||
isInitialLoad?: boolean;
|
||||
}) => {
|
||||
if (isLoading) return null;
|
||||
|
||||
const response = await callApi({
|
||||
initialId: id,
|
||||
initialIndex: index,
|
||||
pageSize,
|
||||
isInitialLoad,
|
||||
...params
|
||||
} as TParams);
|
||||
|
||||
if (!response) return null;
|
||||
|
||||
setHasMorePrev(response.hasMorePrev);
|
||||
setHasMoreNext(response.hasMoreNext);
|
||||
setDataList(response.list);
|
||||
|
||||
if (response.list.length > 0) {
|
||||
anchorRef.current.top = response.list[0];
|
||||
anchorRef.current.bottom = response.list[response.list.length - 1];
|
||||
}
|
||||
|
||||
setInitialLoadDone(true);
|
||||
|
||||
const scrollIndex = response.list.findIndex((item) => item._id === id);
|
||||
|
||||
if (scrollIndex !== -1 && itemRefs.current?.[scrollIndex]) {
|
||||
setTimeout(() => {
|
||||
scrollToItem(scrollIndex);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
[callApi, params, dataList, hasMorePrev, hasMoreNext, isLoading]
|
||||
);
|
||||
|
||||
const loadPrevData = useCallback(
|
||||
async (scrollRef = containerRef) => {
|
||||
if (!anchorRef.current.top || !hasMorePrev || isLoading) return;
|
||||
|
||||
const prevScrollTop = scrollRef?.current?.scrollTop || 0;
|
||||
const prevScrollHeight = scrollRef?.current?.scrollHeight || 0;
|
||||
|
||||
const response = await callApi({
|
||||
prevId: anchorRef.current.top._id,
|
||||
prevIndex: anchorRef.current.top.index,
|
||||
pageSize,
|
||||
...params
|
||||
} as TParams);
|
||||
|
||||
if (!response) return;
|
||||
|
||||
setHasMorePrev(response.hasMorePrev);
|
||||
|
||||
if (response.list.length > 0) {
|
||||
setDataList((prev) => [...response.list, ...prev]);
|
||||
anchorRef.current.top = response.list[0];
|
||||
|
||||
setTimeout(() => {
|
||||
if (scrollRef?.current) {
|
||||
const newHeight = scrollRef.current.scrollHeight;
|
||||
const heightDiff = newHeight - prevScrollHeight;
|
||||
scrollRef.current.scrollTop = prevScrollTop + heightDiff;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
[callApi, hasMorePrev, isLoading, params, pageSize]
|
||||
);
|
||||
|
||||
const loadNextData = useCallback(
|
||||
async (scrollRef = containerRef) => {
|
||||
if (!anchorRef.current.bottom || !hasMoreNext || isLoading) return;
|
||||
|
||||
const prevScrollTop = scrollRef?.current?.scrollTop || 0;
|
||||
|
||||
const response = await callApi({
|
||||
nextId: anchorRef.current.bottom._id,
|
||||
nextIndex: anchorRef.current.bottom.index,
|
||||
pageSize,
|
||||
...params
|
||||
} as TParams);
|
||||
|
||||
if (!response) return;
|
||||
|
||||
setHasMoreNext(response.hasMoreNext);
|
||||
|
||||
if (response.list.length > 0) {
|
||||
setDataList((prev) => [...prev, ...response.list]);
|
||||
anchorRef.current.bottom = response.list[response.list.length - 1];
|
||||
|
||||
setTimeout(() => {
|
||||
if (scrollRef?.current) {
|
||||
scrollRef.current.scrollTop = prevScrollTop;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
[callApi, hasMoreNext, isLoading, params, pageSize]
|
||||
);
|
||||
|
||||
const scrollToItem = useCallback(
|
||||
(itemIndex: number) => {
|
||||
if (itemIndex >= 0 && itemIndex < dataList.length && itemRefs.current?.[itemIndex]) {
|
||||
try {
|
||||
const element = itemRefs.current[itemIndex];
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (element && containerRef.current) {
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
|
||||
const relativeTop = elementRect.top - containerRect.top;
|
||||
|
||||
const scrollTop =
|
||||
containerRef.current.scrollTop +
|
||||
relativeTop -
|
||||
containerRect.height / 2 +
|
||||
elementRect.height / 2;
|
||||
|
||||
containerRef.current.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error scrolling to item:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[dataList.length]
|
||||
);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
if (canLoadData) {
|
||||
// 重置初始滚动状态
|
||||
hasScrolledToInitial.current = false;
|
||||
|
||||
loadData({
|
||||
id: initialId || '',
|
||||
index: initialIndex || 0,
|
||||
isInitialLoad: true
|
||||
});
|
||||
}
|
||||
}, [canLoadData, ...refreshDeps]);
|
||||
|
||||
// 监听初始加载完成,执行初始滚动
|
||||
useEffect(() => {
|
||||
if (initialLoadDone && dataList.length > 0 && !hasScrolledToInitial.current) {
|
||||
hasScrolledToInitial.current = true;
|
||||
const foundIndex = dataList.findIndex((item) => item._id === initialId);
|
||||
|
||||
if (foundIndex >= 0) {
|
||||
setTimeout(() => {
|
||||
scrollToItem(foundIndex);
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}, [initialLoadDone, ...refreshDeps]);
|
||||
|
||||
const ScrollData = useMemoizedFn(
|
||||
({
|
||||
children,
|
||||
ScrollContainerRef,
|
||||
isLoading: externalLoading,
|
||||
...props
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
children: ReactNode;
|
||||
ScrollContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
} & BoxProps) => {
|
||||
const ref = ScrollContainerRef || containerRef;
|
||||
const scroll = useScroll(ref);
|
||||
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
if (!ref?.current || isLoading || !initialLoadDone) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = ref.current;
|
||||
|
||||
// 滚动到底部附近,加载更多下方数据
|
||||
if (scrollTop + clientHeight >= scrollHeight - threshold && hasMoreNext) {
|
||||
loadNextData(ref);
|
||||
}
|
||||
|
||||
// 滚动到顶部附近,加载更多上方数据
|
||||
if (scrollTop <= threshold && hasMorePrev) {
|
||||
loadPrevData(ref);
|
||||
}
|
||||
},
|
||||
[scroll],
|
||||
{ wait: 200 }
|
||||
);
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
ref={ref}
|
||||
h={'100%'}
|
||||
overflow={'auto'}
|
||||
isLoading={externalLoading || isLoading}
|
||||
{...props}
|
||||
>
|
||||
{hasMorePrev && isLoading && initialLoadDone && (
|
||||
<Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}>
|
||||
{t('common:common.is_requesting')}
|
||||
</Box>
|
||||
)}
|
||||
{children}
|
||||
{hasMoreNext && isLoading && initialLoadDone && (
|
||||
<Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}>
|
||||
{t('common:common.is_requesting')}
|
||||
</Box>
|
||||
)}
|
||||
</MyBox>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
dataList,
|
||||
setDataList,
|
||||
isLoading,
|
||||
loadData,
|
||||
initialLoadDone,
|
||||
ScrollData,
|
||||
itemRefs,
|
||||
scrollToItem
|
||||
};
|
||||
}
|
||||
@ -3,6 +3,8 @@
|
||||
"Delete_all": "Clear All Lexicon",
|
||||
"LLM_model_response_empty": "The model flow response is empty, please check whether the model flow output is normal.",
|
||||
"ai_reasoning": "Thinking process",
|
||||
"chat.quote.No Data": "The file cannot be found",
|
||||
"chat.quote.deleted": "This data has been deleted ~",
|
||||
"chat_history": "Conversation History",
|
||||
"chat_input_guide_lexicon_is_empty": "Lexicon not configured yet",
|
||||
"chat_test_app": "Debug-{{name}}",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"App": "Application",
|
||||
"Download": "Download",
|
||||
"Export": "Export",
|
||||
"FAQ.ai_point_a": "Each time you use the AI model, a certain amount of AI points will be deducted. For detailed calculation standards, please refer to the 'AI Points Calculation Standards' above.\nToken calculation uses the same formula as GPT-3.5, where 1 Token ≈ 0.7 Chinese characters ≈ 0.9 English words. Consecutive characters may be considered as 1 Token.",
|
||||
"FAQ.ai_point_expire_a": "Yes, they will expire. After the current package expires, the AI points will be reset to the new package's AI points. Annual package AI points are valid for one year, not monthly.",
|
||||
@ -450,6 +451,8 @@
|
||||
"core.chat.module_unexist": "Running failed: Application missing components",
|
||||
"core.chat.quote.Quote Tip": "Only the actual quoted content is displayed here. If the data is updated, it will not be updated in real-time here.",
|
||||
"core.chat.quote.Read Quote": "View Quote",
|
||||
"core.chat.quote.afterUpdate": "After update",
|
||||
"core.chat.quote.beforeUpdate": "Before update",
|
||||
"core.chat.response.Complete Response": "Complete Response",
|
||||
"core.chat.response.Extension model": "Question Optimization Model",
|
||||
"core.chat.response.Read complete response": "View Details",
|
||||
@ -495,9 +498,11 @@
|
||||
"core.dataset.Dataset": "Dataset",
|
||||
"core.dataset.Dataset ID": "Dataset ID",
|
||||
"core.dataset.Delete Confirm": "Confirm to Delete This Dataset? Data Cannot Be Recovered After Deletion, Please Confirm!",
|
||||
"core.dataset.Download the parsed content": "Download the parsed content",
|
||||
"core.dataset.Empty Dataset": "Empty Dataset",
|
||||
"core.dataset.Empty Dataset Tips": "No Dataset Yet, Create One Now!",
|
||||
"core.dataset.Folder placeholder": "This is a Directory",
|
||||
"core.dataset.Get the raw data": "Get the raw data",
|
||||
"core.dataset.Go Dataset": "Go to Dataset",
|
||||
"core.dataset.Intro Placeholder": "This Dataset Has No Introduction Yet",
|
||||
"core.dataset.Manual collection": "Manual Dataset",
|
||||
@ -540,6 +545,7 @@
|
||||
"core.dataset.data.Empty Tip": "This collection has no data yet",
|
||||
"core.dataset.data.Search data placeholder": "Search Related Data",
|
||||
"core.dataset.data.Too Long": "Total Length Exceeded",
|
||||
"core.dataset.data.Updated": "Updated",
|
||||
"core.dataset.data.group": "Group",
|
||||
"core.dataset.data.unit": "Items",
|
||||
"core.dataset.embedding model tip": "The index model can convert natural language into vectors for semantic search.\nNote that different index models cannot be used together. Once an index model is selected, it cannot be changed.",
|
||||
@ -1027,6 +1033,8 @@
|
||||
"support.outlink.Max usage points": "Points Limit",
|
||||
"support.outlink.Max usage points tip": "The maximum number of points allowed for this link. It cannot be used after exceeding the limit. -1 means unlimited.",
|
||||
"support.outlink.Usage points": "Points Consumption",
|
||||
"support.outlink.share.Chat_quote_reader": "Full text reader",
|
||||
"support.outlink.share.Full_text tips": "Allows reading of the complete dataset from which the referenced fragment is derived",
|
||||
"support.outlink.share.Response Quote": "Return Quote",
|
||||
"support.outlink.share.Response Quote tips": "Return quoted content in the share link, but do not allow users to download the original document",
|
||||
"support.outlink.share.running_node": "Running node",
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
"Delete_all": "清空词库",
|
||||
"LLM_model_response_empty": "模型流响应为空,请检查模型流输出是否正常",
|
||||
"ai_reasoning": "思考过程",
|
||||
"chat.quote.No Data": "找不到该文件",
|
||||
"chat.quote.deleted": "该数据已被删除~",
|
||||
"chat_history": "聊天记录",
|
||||
"chat_input_guide_lexicon_is_empty": "还没有配置词库",
|
||||
"chat_test_app": "调试-{{name}}",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"App": "应用",
|
||||
"Download": "下载",
|
||||
"Export": "导出",
|
||||
"FAQ.ai_point_a": "每次调用AI模型时,都会消耗一定的AI积分。具体的计算标准可参考上方的“AI 积分计算标准”。\nToken计算采用GPT3.5相同公式,1Token≈0.7中文字符≈0.9英文单词,连续出现的字符可能被认为是1个Tokens。",
|
||||
"FAQ.ai_point_expire_a": "会过期。当前套餐过期后,AI积分将会清空,并更新为新套餐的AI积分。年度套餐的AI积分时长为1年,而不是每个月。",
|
||||
@ -453,6 +454,8 @@
|
||||
"core.chat.module_unexist": "运行失败:应用缺失组件",
|
||||
"core.chat.quote.Quote Tip": "此处仅显示实际引用内容,若数据有更新,此处不会实时更新",
|
||||
"core.chat.quote.Read Quote": "查看引用",
|
||||
"core.chat.quote.afterUpdate": "更新后",
|
||||
"core.chat.quote.beforeUpdate": "更新前",
|
||||
"core.chat.response.Complete Response": "完整响应",
|
||||
"core.chat.response.Extension model": "问题优化模型",
|
||||
"core.chat.response.Read complete response": "查看详情",
|
||||
@ -498,9 +501,11 @@
|
||||
"core.dataset.Dataset": "知识库",
|
||||
"core.dataset.Dataset ID": "知识库 ID",
|
||||
"core.dataset.Delete Confirm": "确认删除该知识库?删除后数据无法恢复,请确认!",
|
||||
"core.dataset.Download the parsed content": "下载解析内容",
|
||||
"core.dataset.Empty Dataset": "空数据集",
|
||||
"core.dataset.Empty Dataset Tips": "还没有知识库,快去创建一个吧!",
|
||||
"core.dataset.Folder placeholder": "这是一个目录",
|
||||
"core.dataset.Get the raw data": "获取源数据",
|
||||
"core.dataset.Go Dataset": "前往知识库",
|
||||
"core.dataset.Intro Placeholder": "这个知识库还没有介绍~",
|
||||
"core.dataset.Manual collection": "手动数据集",
|
||||
@ -543,6 +548,7 @@
|
||||
"core.dataset.data.Empty Tip": "这个集合还没有数据~",
|
||||
"core.dataset.data.Search data placeholder": "搜索相关数据",
|
||||
"core.dataset.data.Too Long": "总长度超长了",
|
||||
"core.dataset.data.Updated": "已更新",
|
||||
"core.dataset.data.group": "组",
|
||||
"core.dataset.data.unit": "条",
|
||||
"core.dataset.embedding model tip": "索引模型可以将自然语言转成向量,用于进行语义检索。\n注意,不同索引模型无法一起使用,选择完索引模型后将无法修改。",
|
||||
@ -1031,6 +1037,8 @@
|
||||
"support.outlink.Max usage points": "积分上限",
|
||||
"support.outlink.Max usage points tip": "该链接最多允许使用多少积分,超出后将无法使用。-1 代表无限制。",
|
||||
"support.outlink.Usage points": "积分消耗",
|
||||
"support.outlink.share.Chat_quote_reader": "全文阅读器",
|
||||
"support.outlink.share.Full_text tips": "允许阅读该引用片段来源的完整数据集",
|
||||
"support.outlink.share.Response Quote": "引用内容",
|
||||
"support.outlink.share.Response Quote tips": "查看知识库搜索的引用内容,不可查看完整引用文档或跳转引用网站",
|
||||
"support.outlink.share.running_node": "运行节点",
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
"Delete_all": "清除所有詞彙",
|
||||
"LLM_model_response_empty": "模型流程回應為空,請檢查模型流程輸出是否正常",
|
||||
"ai_reasoning": "思考過程",
|
||||
"chat.quote.No Data": "找不到該文件",
|
||||
"chat.quote.deleted": "該數據已被刪除~",
|
||||
"chat_history": "對話紀錄",
|
||||
"chat_input_guide_lexicon_is_empty": "尚未設定詞彙庫",
|
||||
"chat_test_app": "調試-{{name}}",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"App": "應用程式",
|
||||
"Download": "下載",
|
||||
"Export": "匯出",
|
||||
"FAQ.ai_point_a": "每次呼叫 AI 模型時,都會消耗一定數量的 AI 點數。詳細的計算標準請參考上方的「AI 點數計算標準」。\nToken 計算採用與 GPT3.5 相同的公式,1 Token ≈ 0.7 個中文字 ≈ 0.9 個英文單字,連續出現的字元可能會被視為 1 個 Token。",
|
||||
"FAQ.ai_point_expire_a": "會過期。目前方案過期後,AI 點數將會清空並更新為新方案的 AI 點數。年度方案的 AI 點數有效期為一年,而不是每個月重置。",
|
||||
@ -449,6 +450,8 @@
|
||||
"core.chat.module_unexist": "運行失敗:應用缺失組件",
|
||||
"core.chat.quote.Quote Tip": "此處僅顯示實際引用內容,若資料有更新,此處不會即時更新",
|
||||
"core.chat.quote.Read Quote": "檢視引用",
|
||||
"core.chat.quote.afterUpdate": "更新後",
|
||||
"core.chat.quote.beforeUpdate": "更新前",
|
||||
"core.chat.response.Complete Response": "完整回應",
|
||||
"core.chat.response.Extension model": "問題最佳化模型",
|
||||
"core.chat.response.Read complete response": "檢視詳細資料",
|
||||
@ -494,9 +497,11 @@
|
||||
"core.dataset.Dataset": "知識庫",
|
||||
"core.dataset.Dataset ID": "知識庫 ID",
|
||||
"core.dataset.Delete Confirm": "確認刪除此知識庫?刪除後資料無法復原,請確認!",
|
||||
"core.dataset.Download the parsed content": "下載解析內容",
|
||||
"core.dataset.Empty Dataset": "空資料集",
|
||||
"core.dataset.Empty Dataset Tips": "還沒有知識庫,快來建立一個吧!",
|
||||
"core.dataset.Folder placeholder": "這是一個目錄",
|
||||
"core.dataset.Get the raw data": "獲取源數據",
|
||||
"core.dataset.Go Dataset": "前往知識庫",
|
||||
"core.dataset.Intro Placeholder": "這個知識庫還沒有介紹",
|
||||
"core.dataset.Manual collection": "手動資料集",
|
||||
@ -539,6 +544,7 @@
|
||||
"core.dataset.data.Empty Tip": "此集合還沒有資料",
|
||||
"core.dataset.data.Search data placeholder": "搜尋相關資料",
|
||||
"core.dataset.data.Too Long": "總長度超出上限",
|
||||
"core.dataset.data.Updated": "已更新",
|
||||
"core.dataset.data.group": "組",
|
||||
"core.dataset.data.unit": "筆",
|
||||
"core.dataset.embedding model tip": "索引模型可以將自然語言轉換成向量,用於進行語意搜尋。\n注意,不同索引模型無法一起使用。選擇索引模型後就無法修改。",
|
||||
@ -1026,6 +1032,8 @@
|
||||
"support.outlink.Max usage points": "點數上限",
|
||||
"support.outlink.Max usage points tip": "此連結最多允許使用多少點數,超出後將無法使用。-1 代表無限制。",
|
||||
"support.outlink.Usage points": "點數消耗",
|
||||
"support.outlink.share.Chat_quote_reader": "全文閱讀器",
|
||||
"support.outlink.share.Full_text tips": "允許閱讀該引用片段來源的完整數據集",
|
||||
"support.outlink.share.Response Quote": "回傳引用",
|
||||
"support.outlink.share.Response Quote tips": "在分享連結中回傳引用內容,但不允許使用者下載原始文件",
|
||||
"support.outlink.share.running_node": "執行節點",
|
||||
|
||||
@ -1,22 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
interface Props extends BoxProps {}
|
||||
interface Props extends BoxProps {
|
||||
externalTrigger?: Boolean;
|
||||
}
|
||||
|
||||
const SideBar = (e?: Props) => {
|
||||
const {
|
||||
w = ['100%', '0 0 250px', '0 0 270px', '0 0 290px', '0 0 310px'],
|
||||
children,
|
||||
externalTrigger,
|
||||
...props
|
||||
} = e || {};
|
||||
|
||||
const [foldSideBar, setFoldSideBar] = useState(false);
|
||||
const [isFolded, setIsFolded] = useState(false);
|
||||
|
||||
const prevExternalTriggerRef = useRef<Boolean | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (externalTrigger && !prevExternalTriggerRef.current && !isFolded) {
|
||||
setIsFolded(true);
|
||||
}
|
||||
|
||||
prevExternalTriggerRef.current = externalTrigger;
|
||||
}, [externalTrigger, isFolded]);
|
||||
|
||||
const handleToggle = () => {
|
||||
const newFolded = !isFolded;
|
||||
setIsFolded(newFolded);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={'relative'}
|
||||
flex={foldSideBar ? '0 0 0' : w}
|
||||
flex={isFolded ? '0 0 0' : w}
|
||||
w={['100%', 0]}
|
||||
h={'100%'}
|
||||
zIndex={1}
|
||||
@ -40,7 +59,7 @@ const SideBar = (e?: Props) => {
|
||||
bg={'rgba(0,0,0,0.5)'}
|
||||
cursor={'pointer'}
|
||||
transition={'0.2s'}
|
||||
{...(foldSideBar
|
||||
{...(isFolded
|
||||
? {
|
||||
opacity: 0.6
|
||||
}
|
||||
@ -48,16 +67,16 @@ const SideBar = (e?: Props) => {
|
||||
visibility: 'hidden',
|
||||
opacity: 0
|
||||
})}
|
||||
onClick={() => setFoldSideBar(!foldSideBar)}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<MyIcon
|
||||
name={'common/backLight'}
|
||||
transform={foldSideBar ? 'rotate(180deg)' : ''}
|
||||
transform={isFolded ? 'rotate(180deg)' : ''}
|
||||
w={'14px'}
|
||||
color={'white'}
|
||||
/>
|
||||
</Flex>
|
||||
<Box position={'relative'} h={'100%'} overflow={foldSideBar ? 'hidden' : 'visible'}>
|
||||
<Box position={'relative'} h={'100%'} overflow={isFolded ? 'hidden' : 'visible'}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
|
||||
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import QuoteItem, { formatScore } from '@/components/core/dataset/QuoteItem';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { getQuoteDataList } from '@/web/core/chat/api';
|
||||
|
||||
const QuoteList = React.memo(function QuoteList({
|
||||
chatItemId,
|
||||
rawSearch = [],
|
||||
chatTime
|
||||
}: {
|
||||
chatItemId?: string;
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
chatTime: Date;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const { chatId, appId, outLinkAuthData } = useChatStore();
|
||||
|
||||
const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({
|
||||
chatItemId,
|
||||
appId: v.appId,
|
||||
chatId: v.chatId,
|
||||
...(v.outLinkAuthData || {})
|
||||
}));
|
||||
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
|
||||
const showRouteToDatasetDetail = useContextSelector(
|
||||
ChatItemContext,
|
||||
(v) => v.showRouteToDatasetDetail
|
||||
);
|
||||
|
||||
const { data } = useRequest2(
|
||||
async () =>
|
||||
await getQuoteDataList({
|
||||
datasetDataIdList: rawSearch.map((item) => item.id),
|
||||
chatTime,
|
||||
collectionIdList: [...new Set(rawSearch.map((item) => item.collectionId))],
|
||||
chatItemId: chatItemId || '',
|
||||
appId,
|
||||
chatId,
|
||||
...outLinkAuthData
|
||||
}),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const formatedDataList = useMemo(() => {
|
||||
return rawSearch
|
||||
.map((item) => {
|
||||
const currentFilterItem = data?.quoteList.find((res) => res._id === item.id);
|
||||
|
||||
return {
|
||||
...item,
|
||||
q: currentFilterItem?.q || '',
|
||||
a: currentFilterItem?.a || ''
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aScore = formatScore(a.score);
|
||||
const bScore = formatScore(b.score);
|
||||
return (bScore.primaryScore?.value || 0) - (aScore.primaryScore?.value || 0);
|
||||
});
|
||||
}, [data?.quoteList, rawSearch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{formatedDataList.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
flex={'1 0 0'}
|
||||
p={2}
|
||||
borderRadius={'sm'}
|
||||
border={theme.borders.base}
|
||||
_notLast={{ mb: 2 }}
|
||||
_hover={{ '& .hover-data': { display: 'flex' } }}
|
||||
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
|
||||
>
|
||||
<QuoteItem
|
||||
quoteItem={item}
|
||||
canViewSource={showRawSource}
|
||||
canEditDataset={showRouteToDatasetDetail}
|
||||
{...RawSourceBoxProps}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default QuoteList;
|
||||
@ -1,129 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { ModalBody, Box, useTheme } from '@chakra-ui/react';
|
||||
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import QuoteItem from '@/components/core/dataset/QuoteItem';
|
||||
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
|
||||
const QuoteModal = ({
|
||||
rawSearch = [],
|
||||
onClose,
|
||||
chatItemId,
|
||||
metadata
|
||||
}: {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
onClose: () => void;
|
||||
chatItemId: string;
|
||||
metadata?: {
|
||||
collectionId: string;
|
||||
sourceId?: string;
|
||||
sourceName: string;
|
||||
};
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const filterResults = useMemo(
|
||||
() =>
|
||||
metadata
|
||||
? rawSearch.filter(
|
||||
(item) =>
|
||||
item.collectionId === metadata.collectionId && item.sourceId === metadata.sourceId
|
||||
)
|
||||
: rawSearch,
|
||||
[metadata, rawSearch]
|
||||
);
|
||||
|
||||
const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({
|
||||
appId: v.appId,
|
||||
chatId: v.chatId,
|
||||
chatItemId,
|
||||
...(v.outLinkAuthData || {})
|
||||
}));
|
||||
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
|
||||
const showRouteToDatasetDetail = useContextSelector(
|
||||
ChatItemContext,
|
||||
(v) => v.showRouteToDatasetDetail
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
h={['90vh', '80vh']}
|
||||
isCentered
|
||||
minW={['90vw', '600px']}
|
||||
iconSrc={!!metadata ? undefined : getWebReqUrl('/imgs/modal/quote.svg')}
|
||||
title={
|
||||
<Box>
|
||||
{metadata ? (
|
||||
<RawSourceBox {...metadata} {...RawSourceBoxProps} canView={showRawSource} />
|
||||
) : (
|
||||
<>{t('common:core.chat.Quote Amount', { amount: rawSearch.length })}</>
|
||||
)}
|
||||
<Box fontSize={'xs'} color={'myGray.500'} fontWeight={'normal'}>
|
||||
{t('common:core.chat.quote.Quote Tip')}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ModalBody>
|
||||
<QuoteList rawSearch={filterResults} chatItemId={chatItemId} />
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteModal;
|
||||
|
||||
export const QuoteList = React.memo(function QuoteList({
|
||||
chatItemId,
|
||||
rawSearch = []
|
||||
}: {
|
||||
chatItemId?: string;
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({
|
||||
chatItemId,
|
||||
appId: v.appId,
|
||||
chatId: v.chatId,
|
||||
...(v.outLinkAuthData || {})
|
||||
}));
|
||||
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
|
||||
const showRouteToDatasetDetail = useContextSelector(
|
||||
ChatItemContext,
|
||||
(v) => v.showRouteToDatasetDetail
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{rawSearch.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
flex={'1 0 0'}
|
||||
p={2}
|
||||
borderRadius={'sm'}
|
||||
border={theme.borders.base}
|
||||
_notLast={{ mb: 2 }}
|
||||
_hover={{ '& .hover-data': { display: 'flex' } }}
|
||||
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
|
||||
>
|
||||
<QuoteItem
|
||||
quoteItem={item}
|
||||
canViewSource={showRawSource}
|
||||
canEditDataset={showRouteToDatasetDetail}
|
||||
{...RawSourceBoxProps}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@ -14,8 +14,8 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
|
||||
import { useSize } from 'ahooks';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
|
||||
const QuoteModal = dynamic(() => import('./QuoteModal'));
|
||||
const ContextModal = dynamic(() => import('./ContextModal'));
|
||||
const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal'));
|
||||
|
||||
@ -30,6 +30,7 @@ const ResponseTags = ({
|
||||
const { t } = useTranslation();
|
||||
const quoteListRef = React.useRef<HTMLDivElement>(null);
|
||||
const dataId = historyItem.dataId;
|
||||
const chatTime = historyItem.time || new Date();
|
||||
|
||||
const {
|
||||
totalQuoteList: quoteList = [],
|
||||
@ -38,17 +39,12 @@ const ResponseTags = ({
|
||||
historyPreviewLength = 0
|
||||
} = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]);
|
||||
|
||||
const [quoteModalData, setQuoteModalData] = useState<{
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata?: {
|
||||
collectionId: string;
|
||||
sourceId?: string;
|
||||
sourceName: string;
|
||||
};
|
||||
}>();
|
||||
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
|
||||
|
||||
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
|
||||
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const notSharePage = useMemo(() => chatType !== 'share', [chatType]);
|
||||
|
||||
const {
|
||||
@ -81,7 +77,8 @@ const ResponseTags = ({
|
||||
sourceName: item.sourceName,
|
||||
sourceId: item.sourceId,
|
||||
icon: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
|
||||
collectionId: item.collectionId
|
||||
collectionId: item.collectionId,
|
||||
datasetId: item.datasetId
|
||||
}));
|
||||
}, [quoteList]);
|
||||
|
||||
@ -99,7 +96,11 @@ const ResponseTags = ({
|
||||
<>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<Box width={'100%'}>
|
||||
<ChatBoxDivider icon="core/chat/quoteFill" text={t('common:core.chat.Quote')} />
|
||||
<ChatBoxDivider
|
||||
icon="core/chat/quoteFill"
|
||||
text={t('common:core.chat.Quote')}
|
||||
iconColor="#E82F72"
|
||||
/>
|
||||
</Box>
|
||||
{quoteFolded && quoteIsOverflow && (
|
||||
<MyIcon
|
||||
@ -135,15 +136,13 @@ const ResponseTags = ({
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{sourceList.map((item) => {
|
||||
{sourceList.map((item, index) => {
|
||||
return (
|
||||
<MyTooltip key={item.collectionId} label={t('common:core.chat.quote.Read Quote')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'xs'}
|
||||
border={'sm'}
|
||||
py={1.5}
|
||||
px={2}
|
||||
borderRadius={'sm'}
|
||||
_hover={{
|
||||
'.controller': {
|
||||
@ -155,20 +154,46 @@ const ResponseTags = ({
|
||||
cursor={'pointer'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setQuoteModalData({
|
||||
|
||||
setQuoteData({
|
||||
chatTime,
|
||||
rawSearch: quoteList,
|
||||
metadata: {
|
||||
collectionId: item.collectionId,
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName
|
||||
collectionIdList: [
|
||||
...new Set(quoteList.map((item) => item.collectionId))
|
||||
],
|
||||
sourceId: item.sourceId || '',
|
||||
sourceName: item.sourceName,
|
||||
datasetId: item.datasetId,
|
||||
chatItemId: historyItem.dataId
|
||||
}
|
||||
});
|
||||
}}
|
||||
height={6}
|
||||
>
|
||||
<MyIcon name={item.icon as any} mr={1} flexShrink={0} w={'12px'} />
|
||||
<Box className="textEllipsis3" wordBreak={'break-all'} flex={'1 0 0'}>
|
||||
{item.sourceName}
|
||||
</Box>
|
||||
<Flex
|
||||
color={'myGray.500'}
|
||||
bg={'myGray.150'}
|
||||
w={4}
|
||||
justifyContent={'center'}
|
||||
fontSize={'10px'}
|
||||
h={'full'}
|
||||
alignItems={'center'}
|
||||
>
|
||||
{index + 1}
|
||||
</Flex>
|
||||
<Flex px={1.5}>
|
||||
<MyIcon name={item.icon as any} mr={1} flexShrink={0} w={'12px'} />
|
||||
<Box
|
||||
className="textEllipsis3"
|
||||
wordBreak={'break-all'}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{item.sourceName}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
);
|
||||
@ -196,7 +221,22 @@ const ResponseTags = ({
|
||||
colorSchema="blue"
|
||||
type="borderSolid"
|
||||
cursor={'pointer'}
|
||||
onClick={() => setQuoteModalData({ rawSearch: quoteList })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
setQuoteData({
|
||||
chatTime,
|
||||
rawSearch: quoteList,
|
||||
metadata: {
|
||||
collectionId: '',
|
||||
collectionIdList: [...new Set(quoteList.map((item) => item.collectionId))],
|
||||
chatItemId: historyItem.dataId,
|
||||
sourceId: '',
|
||||
sourceName: '',
|
||||
datasetId: ''
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('chat:citations', { num: quoteList.length })}
|
||||
</MyTag>
|
||||
@ -246,15 +286,10 @@ const ResponseTags = ({
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!!quoteModalData && (
|
||||
<QuoteModal
|
||||
{...quoteModalData}
|
||||
chatItemId={historyItem.dataId}
|
||||
onClose={() => setQuoteModalData(undefined)}
|
||||
/>
|
||||
)}
|
||||
{isOpenContextModal && <ContextModal dataId={dataId} onClose={onCloseContextModal} />}
|
||||
{isOpenWholeModal && <WholeResponseModal dataId={dataId} onClose={onCloseWholeModal} />}
|
||||
{isOpenWholeModal && (
|
||||
<WholeResponseModal dataId={dataId} chatTime={chatTime} onClose={onCloseWholeModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,12 +12,18 @@ const RenderResponseDetail = () => {
|
||||
const isChatting = useContextSelector(PluginRunContext, (v) => v.isChatting);
|
||||
|
||||
const responseData = chatRecords?.[1]?.responseData || [];
|
||||
const chatTime = new Date();
|
||||
|
||||
return isChatting ? (
|
||||
<>{t('chat:in_progress')}</>
|
||||
) : (
|
||||
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
|
||||
<ResponseBox useMobile={true} response={responseData} dataId={chatRecords?.[1]?.dataId} />
|
||||
<ResponseBox
|
||||
useMobile={true}
|
||||
response={responseData}
|
||||
dataId={chatRecords?.[1]?.dataId}
|
||||
chatTime={chatTime}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,11 +3,19 @@ import React from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type.d';
|
||||
|
||||
const ChatBoxDivider = ({ icon, text }: { icon: IconNameType; text: string }) => {
|
||||
const ChatBoxDivider = ({
|
||||
icon,
|
||||
text,
|
||||
iconColor
|
||||
}: {
|
||||
icon: IconNameType;
|
||||
text: string;
|
||||
iconColor?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Box>
|
||||
<Flex alignItems={'center'} py={2} gap={2}>
|
||||
<MyIcon name={icon} w={'14px'} color={'myGray.900'} />
|
||||
<MyIcon name={icon} w={'14px'} color={iconColor || 'myGray.900'} />
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{text}
|
||||
</Box>
|
||||
|
||||
@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { QuoteList } from '../ChatContainer/ChatBox/components/QuoteModal';
|
||||
import QuoteList from '../ChatContainer/ChatBox/components/QuoteList';
|
||||
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
|
||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
@ -32,11 +32,13 @@ type sideTabItemType = {
|
||||
export const WholeResponseContent = ({
|
||||
activeModule,
|
||||
hideTabs,
|
||||
dataId
|
||||
dataId,
|
||||
chatTime
|
||||
}: {
|
||||
activeModule: ChatHistoryItemResType;
|
||||
hideTabs?: boolean;
|
||||
dataId?: string;
|
||||
chatTime?: Date;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -263,7 +265,13 @@ export const WholeResponseContent = ({
|
||||
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
|
||||
<Row
|
||||
label={t('common:core.chat.response.module quoteList')}
|
||||
rawDom={<QuoteList chatItemId={dataId} rawSearch={activeModule.quoteList} />}
|
||||
rawDom={
|
||||
<QuoteList
|
||||
chatItemId={dataId}
|
||||
chatTime={chatTime || new Date()}
|
||||
rawSearch={activeModule.quoteList}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@ -562,11 +570,13 @@ const SideTabItem = ({
|
||||
export const ResponseBox = React.memo(function ResponseBox({
|
||||
response,
|
||||
dataId,
|
||||
chatTime,
|
||||
hideTabs = false,
|
||||
useMobile = false
|
||||
}: {
|
||||
response: ChatHistoryItemResType[];
|
||||
dataId?: string;
|
||||
chatTime: Date;
|
||||
hideTabs?: boolean;
|
||||
useMobile?: boolean;
|
||||
}) {
|
||||
@ -689,7 +699,12 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={'5 0 0'} w={0} height={'100%'}>
|
||||
<WholeResponseContent dataId={dataId} activeModule={activeModule} hideTabs={hideTabs} />
|
||||
<WholeResponseContent
|
||||
dataId={dataId}
|
||||
activeModule={activeModule}
|
||||
hideTabs={hideTabs}
|
||||
chatTime={chatTime}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
) : (
|
||||
@ -753,6 +768,7 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
dataId={dataId}
|
||||
activeModule={activeModule}
|
||||
hideTabs={hideTabs}
|
||||
chatTime={chatTime}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
@ -763,7 +779,15 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
);
|
||||
});
|
||||
|
||||
const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId: string }) => {
|
||||
const WholeResponseModal = ({
|
||||
onClose,
|
||||
dataId,
|
||||
chatTime
|
||||
}: {
|
||||
onClose: () => void;
|
||||
dataId: string;
|
||||
chatTime: Date;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v);
|
||||
@ -792,7 +816,7 @@ const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId:
|
||||
}
|
||||
>
|
||||
{!!response?.length ? (
|
||||
<ResponseBox response={response} dataId={dataId} />
|
||||
<ResponseBox response={response} dataId={dataId} chatTime={chatTime} />
|
||||
) : (
|
||||
<EmptyTip text={t('chat:no_workflow_response')} />
|
||||
)}
|
||||
|
||||
@ -14,8 +14,8 @@ import Markdown from '@/components/Markdown';
|
||||
|
||||
const InputDataModal = dynamic(() => import('@/pageComponents/dataset/detail/InputDataModal'));
|
||||
|
||||
type ScoreItemType = SearchDataResponseItemType['score'][0];
|
||||
const scoreTheme: Record<
|
||||
export type ScoreItemType = SearchDataResponseItemType['score'][0];
|
||||
export const scoreTheme: Record<
|
||||
string,
|
||||
{
|
||||
color: string;
|
||||
@ -44,6 +44,47 @@ const scoreTheme: Record<
|
||||
}
|
||||
};
|
||||
|
||||
export const formatScore = (score: ScoreItemType[]) => {
|
||||
if (!Array.isArray(score)) {
|
||||
return {
|
||||
primaryScore: undefined,
|
||||
secondaryScore: []
|
||||
};
|
||||
}
|
||||
|
||||
// rrf -> rerank -> embedding -> fullText 优先级
|
||||
let rrfScore: ScoreItemType | undefined = undefined;
|
||||
let reRankScore: ScoreItemType | undefined = undefined;
|
||||
let embeddingScore: ScoreItemType | undefined = undefined;
|
||||
let fullTextScore: ScoreItemType | undefined = undefined;
|
||||
|
||||
score.forEach((item) => {
|
||||
if (item.type === SearchScoreTypeEnum.rrf) {
|
||||
rrfScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.reRank) {
|
||||
reRankScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.embedding) {
|
||||
embeddingScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.fullText) {
|
||||
fullTextScore = item;
|
||||
}
|
||||
});
|
||||
|
||||
const primaryScore = (rrfScore ||
|
||||
reRankScore ||
|
||||
embeddingScore ||
|
||||
fullTextScore) as unknown as ScoreItemType;
|
||||
const secondaryScore = [rrfScore, reRankScore, embeddingScore, fullTextScore].filter(
|
||||
// @ts-ignore
|
||||
(item) => item && primaryScore && item.type !== primaryScore.type
|
||||
) as unknown as ScoreItemType[];
|
||||
|
||||
return {
|
||||
primaryScore,
|
||||
secondaryScore
|
||||
};
|
||||
};
|
||||
|
||||
const QuoteItem = ({
|
||||
quoteItem,
|
||||
canViewSource,
|
||||
@ -58,44 +99,7 @@ const QuoteItem = ({
|
||||
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
|
||||
|
||||
const score = useMemo(() => {
|
||||
if (!Array.isArray(quoteItem.score)) {
|
||||
return {
|
||||
primaryScore: undefined,
|
||||
secondaryScore: []
|
||||
};
|
||||
}
|
||||
|
||||
// rrf -> rerank -> embedding -> fullText 优先级
|
||||
let rrfScore: ScoreItemType | undefined = undefined;
|
||||
let reRankScore: ScoreItemType | undefined = undefined;
|
||||
let embeddingScore: ScoreItemType | undefined = undefined;
|
||||
let fullTextScore: ScoreItemType | undefined = undefined;
|
||||
|
||||
quoteItem.score.forEach((item) => {
|
||||
if (item.type === SearchScoreTypeEnum.rrf) {
|
||||
rrfScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.reRank) {
|
||||
reRankScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.embedding) {
|
||||
embeddingScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.fullText) {
|
||||
fullTextScore = item;
|
||||
}
|
||||
});
|
||||
|
||||
const primaryScore = (rrfScore ||
|
||||
reRankScore ||
|
||||
embeddingScore ||
|
||||
fullTextScore) as unknown as ScoreItemType;
|
||||
const secondaryScore = [rrfScore, reRankScore, embeddingScore, fullTextScore].filter(
|
||||
// @ts-ignore
|
||||
(item) => item && primaryScore && item.type !== primaryScore.type
|
||||
) as unknown as ScoreItemType[];
|
||||
|
||||
return {
|
||||
primaryScore,
|
||||
secondaryScore
|
||||
};
|
||||
return formatScore(quoteItem.score);
|
||||
}, [quoteItem.score]);
|
||||
|
||||
return (
|
||||
|
||||
@ -39,5 +39,6 @@ export type DatasetDataListItemType = {
|
||||
q: string; // embedding content
|
||||
a: string; // bonus content
|
||||
chunkIndex?: number;
|
||||
updated?: boolean;
|
||||
// indexes: DatasetDataSchemaType['indexes'];
|
||||
};
|
||||
|
||||
@ -19,6 +19,7 @@ import ChatRecordContextProvider, {
|
||||
} from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
|
||||
const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatBox'));
|
||||
@ -37,6 +38,8 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
|
||||
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
|
||||
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
|
||||
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
|
||||
@ -76,7 +79,7 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
|
||||
zIndex={3}
|
||||
position={['fixed', 'absolute']}
|
||||
top={[0, '2%']}
|
||||
right={0}
|
||||
right={quoteData ? 600 : 0}
|
||||
h={['100%', '96%']}
|
||||
w={'100%'}
|
||||
maxW={['100%', '600px']}
|
||||
@ -168,6 +171,26 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
|
||||
)}
|
||||
</Box>
|
||||
</MyBox>
|
||||
{quoteData && (
|
||||
<Box
|
||||
w={['full', '588px']}
|
||||
zIndex={300}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
h={'95%'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box zIndex={2} position={'fixed'} top={0} left={0} bottom={0} right={0} onClick={onClose} />
|
||||
</>
|
||||
);
|
||||
@ -189,6 +212,7 @@ const Render = (props: Props) => {
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={params}>
|
||||
|
||||
@ -42,7 +42,6 @@ import { getDocPath } from '@/web/common/system/doc';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
@ -185,6 +184,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
|
||||
name: item.name,
|
||||
responseDetail: item.responseDetail ?? false,
|
||||
showRawSource: item.showRawSource ?? false,
|
||||
// showFullText: item.showFullText ?? false,
|
||||
showNodeStatus: item.showNodeStatus ?? false,
|
||||
limit: item.limit
|
||||
})
|
||||
@ -270,7 +270,6 @@ function EditLinkModal({
|
||||
}) {
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { t } = useTranslation();
|
||||
const { publishT } = useI18n();
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
@ -281,6 +280,7 @@ function EditLinkModal({
|
||||
});
|
||||
|
||||
const responseDetail = watch('responseDetail');
|
||||
// const showFullText = watch('showFullText');
|
||||
const showRawSource = watch('showRawSource');
|
||||
|
||||
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
|
||||
@ -306,7 +306,7 @@ function EditLinkModal({
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
iconSrc="/imgs/modal/shareFill.svg"
|
||||
title={isEdit ? publishT('edit_link') : publishT('create_link')}
|
||||
title={isEdit ? t('publish:edit_link') : t('publish:create_link')}
|
||||
maxW={['90vw', '700px']}
|
||||
w={'100%'}
|
||||
h={['90vh', 'auto']}
|
||||
@ -325,10 +325,10 @@ function EditLinkModal({
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
|
||||
<Input
|
||||
placeholder={publishT('link_name')}
|
||||
placeholder={t('publish:link_name')}
|
||||
maxLength={20}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty') || 'name_is_empty'
|
||||
required: t('common:common.name_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
@ -353,7 +353,7 @@ function EditLinkModal({
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>QPM</FormLabel>
|
||||
<QuestionTip ml={1} label={publishT('qpm_tips')}></QuestionTip>
|
||||
<QuestionTip ml={1} label={t('publish:qpm_tips')}></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
max={1000}
|
||||
@ -361,7 +361,7 @@ function EditLinkModal({
|
||||
min: 0,
|
||||
max: 1000,
|
||||
valueAsNumber: true,
|
||||
required: publishT('qpm_is_empty') || ''
|
||||
required: t('publish:qpm_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
@ -385,11 +385,11 @@ function EditLinkModal({
|
||||
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>{publishT('token_auth')}</FormLabel>
|
||||
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
|
||||
<FormLabel>{t('publish:token_auth')}</FormLabel>
|
||||
<QuestionTip ml={1} label={t('publish:token_auth_tips')}></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
placeholder={publishT('token_auth_tips') || ''}
|
||||
placeholder={t('publish:token_auth_tips')}
|
||||
fontSize={'sm'}
|
||||
{...register('limit.hookUrl')}
|
||||
/>
|
||||
@ -400,7 +400,7 @@ function EditLinkModal({
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
{publishT('token_auth_use_cases')}
|
||||
{t('publish:token_auth_use_cases')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
@ -421,8 +421,39 @@ function EditLinkModal({
|
||||
label={t('common:support.outlink.share.Response Quote tips')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Switch {...register('responseDetail')} isChecked={responseDetail} />
|
||||
<Switch
|
||||
{...register('responseDetail', {
|
||||
onChange(e) {
|
||||
if (!e.target.checked) {
|
||||
// setValue('showFullText', false);
|
||||
setValue('showRawSource', false);
|
||||
}
|
||||
}
|
||||
})}
|
||||
isChecked={responseDetail}
|
||||
/>
|
||||
</Flex>
|
||||
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.share.Chat_quote_reader')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.outlink.share.Full_text tips')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Switch
|
||||
{...register('showFullText', {
|
||||
onChange(e) {
|
||||
if (e.target.checked) {
|
||||
setValue('responseDetail', true);
|
||||
} else {
|
||||
setValue('showRawSource', false);
|
||||
}
|
||||
}
|
||||
})}
|
||||
isChecked={showFullText}
|
||||
/>
|
||||
</Flex> */}
|
||||
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
|
||||
@ -436,6 +467,7 @@ function EditLinkModal({
|
||||
onChange(e) {
|
||||
if (e.target.checked) {
|
||||
setValue('responseDetail', true);
|
||||
// setValue('showFullText', true);
|
||||
}
|
||||
}
|
||||
})}
|
||||
|
||||
@ -7,21 +7,26 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSafeState } from 'ahooks';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { useChatTest } from '../useChatTest';
|
||||
import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { cardStyles } from '../constants';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
type Props = { appForm: AppSimpleEditFormType };
|
||||
const ChatTest = ({ appForm }: Props) => {
|
||||
type Props = {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setRenderEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
const ChatTest = ({ appForm, setRenderEdit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const [workflowData, setWorkflowData] = useSafeState({
|
||||
nodes: appDetail.modules || [],
|
||||
@ -32,6 +37,11 @@ const ChatTest = ({ appForm }: Props) => {
|
||||
const { nodes, edges } = form2AppWorkflow(appForm, t);
|
||||
setWorkflowData({ nodes, edges });
|
||||
}, [appForm, setWorkflowData, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderEdit(!quoteData);
|
||||
}, [quoteData, setRenderEdit]);
|
||||
|
||||
const { ChatContainer, restartChat, loading } = useChatTest({
|
||||
...workflowData,
|
||||
chatConfig: appForm.chatConfig,
|
||||
@ -39,41 +49,56 @@ const ChatTest = ({ appForm }: Props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
isLoading={loading}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
py={4}
|
||||
>
|
||||
<Flex px={[2, 5]}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
|
||||
{appT('chat_debug')}
|
||||
<Flex h={'full'} gap={2}>
|
||||
<MyBox
|
||||
isLoading={loading}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'full'}
|
||||
w={quoteData ? '' : 'full'}
|
||||
py={4}
|
||||
{...cardStyles}
|
||||
boxShadow={'3'}
|
||||
>
|
||||
<Flex px={[2, 5]}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
|
||||
{t('app:chat_debug')}
|
||||
</Box>
|
||||
<MyTooltip label={t('common:core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
restartChat();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatContainer />
|
||||
</Box>
|
||||
<MyTooltip label={t('common:core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
restartChat();
|
||||
}}
|
||||
</MyBox>
|
||||
{quoteData && (
|
||||
<Box w={['full', '588px']} {...cardStyles} boxShadow={'3'}>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatContainer />
|
||||
</Box>
|
||||
</MyBox>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const Render = ({ appForm }: Props) => {
|
||||
const Render = ({ appForm, setRenderEdit }: Props) => {
|
||||
const { chatId } = useChatStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
@ -90,10 +115,11 @@ const Render = ({ appForm }: Props) => {
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<ChatTest appForm={appForm} />
|
||||
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />
|
||||
</ChatRecordContextProvider>
|
||||
</ChatItemContextProvider>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import ChatTest from './ChatTest';
|
||||
@ -21,6 +21,7 @@ const Edit = ({
|
||||
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
const [renderEdit, setRenderEdit] = useState(true);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -32,24 +33,26 @@ const Edit = ({
|
||||
borderRadius={'lg'}
|
||||
overflowY={['auto', 'unset']}
|
||||
>
|
||||
<Box
|
||||
className={styles.EditAppBox}
|
||||
pr={[0, 1]}
|
||||
overflowY={'auto'}
|
||||
minW={['auto', '580px']}
|
||||
flex={'1'}
|
||||
>
|
||||
<Box {...cardStyles} boxShadow={'2'}>
|
||||
<AppCard appForm={appForm} setPast={setPast} />
|
||||
</Box>
|
||||
{renderEdit && (
|
||||
<Box
|
||||
className={styles.EditAppBox}
|
||||
pr={[0, 1]}
|
||||
overflowY={'auto'}
|
||||
minW={['auto', '580px']}
|
||||
flex={'1'}
|
||||
>
|
||||
<Box {...cardStyles} boxShadow={'2'}>
|
||||
<AppCard appForm={appForm} setPast={setPast} />
|
||||
</Box>
|
||||
|
||||
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
|
||||
<EditForm appForm={appForm} setAppForm={setAppForm} />
|
||||
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
|
||||
<EditForm appForm={appForm} setAppForm={setAppForm} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{isPc && (
|
||||
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0} mb={3}>
|
||||
<ChatTest appForm={appForm} />
|
||||
<Box flex={'2 0 0'} w={0} mb={3}>
|
||||
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@ -20,6 +20,7 @@ import ChatRecordContextProvider, {
|
||||
} from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@ -41,10 +42,13 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
});
|
||||
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
|
||||
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex h={'full'}>
|
||||
<Box
|
||||
zIndex={300}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
@ -53,7 +57,10 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
onClick={onClose}
|
||||
onClick={() => {
|
||||
setQuoteData(undefined);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<MyBox
|
||||
isLoading={loading}
|
||||
@ -62,7 +69,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
right={quoteData ? 600 : 0}
|
||||
h={isOpen ? '95%' : '0'}
|
||||
w={isOpen ? ['100%', '460px'] : '0'}
|
||||
bg={'white'}
|
||||
@ -141,7 +148,27 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
<ChatContainer />
|
||||
</Box>
|
||||
</MyBox>
|
||||
</>
|
||||
{quoteData && (
|
||||
<Box
|
||||
w={['full', '588px']}
|
||||
zIndex={300}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
h={'95%'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@ -162,6 +189,7 @@ const Render = (Props: Props) => {
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
|
||||
@ -46,6 +46,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name);
|
||||
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar);
|
||||
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const concatHistory = useMemo(() => {
|
||||
const formatHistories: HistoryItemType[] = histories.map((item) => {
|
||||
@ -144,7 +145,10 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
borderRadius={'xl'}
|
||||
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
|
||||
overflow={'hidden'}
|
||||
onClick={() => onChangeChatId()}
|
||||
onClick={() => {
|
||||
onChangeChatId();
|
||||
setQuoteData(undefined);
|
||||
}}
|
||||
>
|
||||
{t('common:core.chat.New Chat')}
|
||||
</Button>
|
||||
@ -199,6 +203,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
: {
|
||||
onClick: () => {
|
||||
onChangeChatId(item.id);
|
||||
setQuoteData(undefined);
|
||||
}
|
||||
})}
|
||||
{...(i !== concatHistory.length - 1 && {
|
||||
@ -270,6 +275,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
onDelHistory(item.id);
|
||||
if (item.id === activeChatId) {
|
||||
onChangeChatId();
|
||||
setQuoteData(undefined);
|
||||
}
|
||||
},
|
||||
type: 'danger'
|
||||
|
||||
@ -0,0 +1,192 @@
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { Dispatch, MutableRefObject, SetStateAction, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
import InputDataModal from '@/pageComponents/dataset/detail/InputDataModal';
|
||||
|
||||
const CollectionQuoteItem = ({
|
||||
index,
|
||||
quoteRefs,
|
||||
quoteIndex,
|
||||
setQuoteIndex,
|
||||
refreshList,
|
||||
canEdit,
|
||||
|
||||
updated,
|
||||
isCurrentSelected,
|
||||
q,
|
||||
a,
|
||||
dataId,
|
||||
collectionId
|
||||
}: {
|
||||
index: number;
|
||||
quoteRefs: MutableRefObject<(HTMLDivElement | null)[]>;
|
||||
quoteIndex: number;
|
||||
setQuoteIndex: Dispatch<SetStateAction<number>>;
|
||||
refreshList: () => void;
|
||||
canEdit: boolean;
|
||||
|
||||
updated?: boolean;
|
||||
isCurrentSelected: boolean;
|
||||
q: string;
|
||||
a?: string;
|
||||
dataId: string;
|
||||
collectionId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const hasBeenSearched = quoteIndex !== undefined && quoteIndex > -1;
|
||||
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={(el: HTMLDivElement | null) => {
|
||||
quoteRefs.current[index] = el;
|
||||
}}
|
||||
p={2}
|
||||
py={2}
|
||||
cursor={hasBeenSearched ? 'pointer' : 'default'}
|
||||
bg={isCurrentSelected ? '#FFF9E7' : hasBeenSearched ? '#FFFCF2' : ''}
|
||||
position={'relative'}
|
||||
overflow={'hidden'}
|
||||
border={'1px solid '}
|
||||
borderColor={isCurrentSelected ? 'yellow.200' : 'transparent'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'sm'}
|
||||
_hover={
|
||||
hasBeenSearched
|
||||
? {
|
||||
'& .hover-data': { visibility: 'visible' }
|
||||
}
|
||||
: {
|
||||
bg: 'linear-gradient(180deg, #FBFBFC 7.61%, #F0F1F6 100%)',
|
||||
borderTopColor: 'myGray.50',
|
||||
'& .hover-data': { visibility: 'visible' }
|
||||
}
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasBeenSearched) {
|
||||
setQuoteIndex(quoteIndex);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{updated && (
|
||||
<Flex
|
||||
position={'absolute'}
|
||||
top={2}
|
||||
right={5}
|
||||
gap={1}
|
||||
bg={'yellow.50'}
|
||||
color={'yellow.500'}
|
||||
px={2}
|
||||
py={1}
|
||||
rounded={'md'}
|
||||
fontSize={'12px'}
|
||||
>
|
||||
<MyIcon name="common/info" w={'14px'} color={'yellow.500'} />
|
||||
{t('common:core.dataset.data.Updated')}
|
||||
</Flex>
|
||||
)}
|
||||
<Markdown source={q} />
|
||||
{!!a && (
|
||||
<Box>
|
||||
<Markdown source={a} />
|
||||
</Box>
|
||||
)}
|
||||
<Flex
|
||||
className="hover-data"
|
||||
position={'absolute'}
|
||||
bottom={2}
|
||||
right={5}
|
||||
gap={1.5}
|
||||
visibility={'hidden'}
|
||||
>
|
||||
<MyTooltip label={t('common:core.dataset.Quote Length')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={2}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
>
|
||||
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
|
||||
{q.length + (a?.length || 0)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
{canEdit && (
|
||||
<MyTooltip label={t('common:core.dataset.data.Edit')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={1}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
setEditInputData({
|
||||
dataId,
|
||||
collectionId
|
||||
})
|
||||
}
|
||||
>
|
||||
<MyIcon name="common/edit" w={'14px'} color={'myGray.500'} />
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<MyTooltip label={t('common:common.Copy')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={1}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
copyData(q + '\n' + a);
|
||||
}}
|
||||
>
|
||||
<MyIcon name="copy" w={'14px'} color={'myGray.500'} />
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
</Box>
|
||||
{editInputData && (
|
||||
<InputDataModal
|
||||
onClose={() => setEditInputData(undefined)}
|
||||
onSuccess={() => {
|
||||
console.log('onSuccess');
|
||||
refreshList();
|
||||
}}
|
||||
dataId={editInputData.dataId}
|
||||
collectionId={editInputData.collectionId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionQuoteItem;
|
||||
@ -0,0 +1,345 @@
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadButton from './DownloadButton';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { downloadFetch } from '@/web/common/system/utils';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { getCollectionSource, getDatasetDataPermission } from '@/web/core/dataset/api';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import ScoreTag from './ScoreTag';
|
||||
import { formatScore } from '@/components/core/dataset/QuoteItem';
|
||||
import NavButton from './NavButton';
|
||||
import { useLinkedScroll } from '@fastgpt/web/hooks/useLinkedScroll';
|
||||
import CollectionQuoteItem from './CollectionQuoteItem';
|
||||
import { DatasetDataListItemType } from '@/global/core/dataset/type';
|
||||
import { metadataType } from '@/web/core/chat/context/chatItemContext';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { getCollectionQuote } from '@/web/core/chat/api';
|
||||
|
||||
const CollectionReader = ({
|
||||
rawSearch,
|
||||
metadata,
|
||||
chatTime,
|
||||
onClose
|
||||
}: {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata: metadataType;
|
||||
chatTime: Date;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { chatId, appId, outLinkAuthData } = useChatStore();
|
||||
const { userInfo } = useUserStore();
|
||||
const { collectionId, datasetId, chatItemId, sourceId, sourceName } = metadata;
|
||||
const [quoteIndex, setQuoteIndex] = useState(0);
|
||||
|
||||
const { data: permissionData, loading: isPermissionLoading } = useRequest2(
|
||||
async () => await getDatasetDataPermission(datasetId),
|
||||
{
|
||||
manual: !userInfo && !datasetId,
|
||||
refreshDeps: [datasetId, userInfo]
|
||||
}
|
||||
);
|
||||
|
||||
const filterResults = useMemo(() => {
|
||||
const results = rawSearch.filter(
|
||||
(item) => item.collectionId === metadata.collectionId && item.sourceId === metadata.sourceId
|
||||
);
|
||||
|
||||
return results.sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
|
||||
}, [metadata, rawSearch]);
|
||||
|
||||
const currentQuoteItem = filterResults[quoteIndex];
|
||||
|
||||
const {
|
||||
dataList: datasetDataList,
|
||||
setDataList: setDatasetDataList,
|
||||
isLoading,
|
||||
loadData,
|
||||
ScrollData,
|
||||
itemRefs,
|
||||
scrollToItem
|
||||
} = useLinkedScroll(getCollectionQuote, {
|
||||
refreshDeps: [collectionId],
|
||||
params: {
|
||||
collectionId,
|
||||
chatTime,
|
||||
chatItemId,
|
||||
chatId,
|
||||
appId,
|
||||
...outLinkAuthData
|
||||
},
|
||||
initialId: currentQuoteItem?.id,
|
||||
initialIndex: currentQuoteItem?.chunkIndex,
|
||||
canLoadData: !!currentQuoteItem?.id && !isPermissionLoading
|
||||
});
|
||||
|
||||
const loading = isLoading || isPermissionLoading;
|
||||
const isDeleted = !datasetDataList.find((item) => item._id === currentQuoteItem?.id);
|
||||
|
||||
const formatedDataList = useMemo(
|
||||
() =>
|
||||
datasetDataList.map((item: DatasetDataListItemType) => {
|
||||
const isCurrentSelected = currentQuoteItem?.id === item._id;
|
||||
const quoteIndex = filterResults.findIndex((res) => res.id === item._id);
|
||||
|
||||
return {
|
||||
...item,
|
||||
isCurrentSelected,
|
||||
quoteIndex
|
||||
};
|
||||
}),
|
||||
[currentQuoteItem?.id, datasetDataList, filterResults]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setQuoteIndex(0);
|
||||
setDatasetDataList([]);
|
||||
}, [collectionId, setDatasetDataList]);
|
||||
|
||||
const { runAsync: handleDownload, loading: downloadLoading } = useRequest2(async () => {
|
||||
await downloadFetch({
|
||||
url: '/api/core/dataset/collection/export',
|
||||
filename: 'parsed_content.md',
|
||||
body: {
|
||||
collectionId: collectionId,
|
||||
chatTime: chatTime,
|
||||
chatItemId: chatItemId
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const { runAsync: handleRead, loading: readLoading } = useRequest2(
|
||||
async () => await getCollectionSource({ ...metadata, appId, chatId }),
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
if (!res.value) {
|
||||
throw new Error('No file found');
|
||||
}
|
||||
if (res.value.startsWith('/')) {
|
||||
window.open(`${location.origin}${res.value}`, '_blank');
|
||||
} else {
|
||||
window.open(res.value, '_blank');
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({
|
||||
title: t(getErrText(err, t('common:error.fileNotFound'))),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleNavigate = useCallback(
|
||||
async (targetIndex: number) => {
|
||||
if (targetIndex < 0 || targetIndex >= filterResults.length) return;
|
||||
const targetItemId = filterResults[targetIndex].id;
|
||||
const targetItemIndex = filterResults[targetIndex].chunkIndex;
|
||||
|
||||
setQuoteIndex(targetIndex);
|
||||
const dataIndex = datasetDataList.findIndex((item) => item._id === targetItemId);
|
||||
|
||||
if (dataIndex !== -1) {
|
||||
setTimeout(() => {
|
||||
scrollToItem(dataIndex);
|
||||
}, 50);
|
||||
} else {
|
||||
try {
|
||||
await loadData({ id: targetItemId, index: targetItemIndex });
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
[filterResults, datasetDataList, scrollToItem, loadData]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'full'}>
|
||||
{/* title */}
|
||||
<Flex
|
||||
w={'full'}
|
||||
alignItems={'center'}
|
||||
px={5}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.150'}
|
||||
>
|
||||
<Box flex={1} py={4}>
|
||||
<Flex mb={1} alignItems={['flex-start', 'center']} flexDirection={['column', 'row']}>
|
||||
<Flex gap={2} mr={2}>
|
||||
<MyIcon
|
||||
name={getSourceNameIcon({ sourceId, sourceName }) as any}
|
||||
w={['1rem', '1.25rem']}
|
||||
color={'primary.600'}
|
||||
/>
|
||||
<Box
|
||||
maxW={['200px', '300px']}
|
||||
className={'textEllipsis'}
|
||||
wordBreak={'break-all'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{sourceName || t('common:common.UnKnow Source')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex gap={3} mt={[2, 0]} alignItems={'center'}>
|
||||
{!!userInfo && permissionData?.permission?.hasReadPer && (
|
||||
<Button
|
||||
variant={'primaryGhost'}
|
||||
size={'xs'}
|
||||
fontSize={'mini'}
|
||||
border={'none'}
|
||||
_hover={{
|
||||
bg: 'primary.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/dataset/detail?datasetId=${datasetId}¤tTab=dataCard&collectionId=${collectionId}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('common:core.dataset.Go Dataset')}
|
||||
<MyIcon name="common/upperRight" w={4} ml={1} />
|
||||
</Button>
|
||||
)}
|
||||
<DownloadButton
|
||||
canAccessRawData={true}
|
||||
onDownload={handleDownload}
|
||||
onRead={handleRead}
|
||||
isLoading={downloadLoading || readLoading}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box fontSize={'mini'} color={'myGray.500'}>
|
||||
{t('common:core.chat.quote.Quote Tip')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
borderRadius={'sm'}
|
||||
p={1}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<MyIcon name="common/closeLight" color={'myGray.900'} w={6} />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* header control */}
|
||||
{datasetDataList.length > 0 && (
|
||||
<Flex
|
||||
w={'full'}
|
||||
px={4}
|
||||
py={2}
|
||||
alignItems={'center'}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.150'}
|
||||
>
|
||||
{/* 引用序号 */}
|
||||
<Flex fontSize={'mini'} mr={3} alignItems={'center'} gap={1}>
|
||||
<Box as={'span'} color={'myGray.900'}>
|
||||
{t('common:core.chat.Quote')} {quoteIndex + 1}
|
||||
</Box>
|
||||
<Box as={'span'} color={'myGray.500'}>
|
||||
/
|
||||
</Box>
|
||||
<Box as={'span'} color={'myGray.500'}>
|
||||
{filterResults.length}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* 检索分数 */}
|
||||
{!loading &&
|
||||
(!isDeleted ? (
|
||||
<ScoreTag {...formatScore(currentQuoteItem?.score)} />
|
||||
) : (
|
||||
<Flex
|
||||
borderRadius={'sm'}
|
||||
py={1}
|
||||
px={2}
|
||||
color={'red.600'}
|
||||
bg={'red.50'}
|
||||
alignItems={'center'}
|
||||
fontSize={'11px'}
|
||||
>
|
||||
<MyIcon name="common/info" w={'14px'} mr={1} color={'red.600'} />
|
||||
{t('chat:chat.quote.deleted')}
|
||||
</Flex>
|
||||
))}
|
||||
|
||||
<Box flex={1} />
|
||||
|
||||
{/* 检索按钮 */}
|
||||
<Flex gap={1}>
|
||||
<NavButton
|
||||
direction="up"
|
||||
isDisabled={quoteIndex === 0}
|
||||
onClick={() => handleNavigate(quoteIndex - 1)}
|
||||
/>
|
||||
<NavButton
|
||||
direction="down"
|
||||
isDisabled={quoteIndex === filterResults.length - 1}
|
||||
onClick={() => handleNavigate(quoteIndex + 1)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* quote list */}
|
||||
{loading || datasetDataList.length > 0 ? (
|
||||
<ScrollData flex={'1 0 0'} mt={2} px={5} py={1} isLoading={loading}>
|
||||
<Flex flexDir={'column'} gap={3}>
|
||||
{formatedDataList.map((item, index) => (
|
||||
<CollectionQuoteItem
|
||||
key={item._id}
|
||||
index={index}
|
||||
quoteRefs={itemRefs as React.MutableRefObject<(HTMLDivElement | null)[]>}
|
||||
quoteIndex={item.quoteIndex}
|
||||
setQuoteIndex={setQuoteIndex}
|
||||
refreshList={() =>
|
||||
currentQuoteItem?.id &&
|
||||
loadData({ id: currentQuoteItem.id, index: currentQuoteItem.chunkIndex })
|
||||
}
|
||||
updated={item.updated}
|
||||
isCurrentSelected={item.isCurrentSelected}
|
||||
q={item.q}
|
||||
a={item.a}
|
||||
dataId={item._id}
|
||||
collectionId={collectionId}
|
||||
canEdit={!!userInfo && !!permissionData?.permission?.hasWritePer}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollData>
|
||||
) : (
|
||||
<Flex
|
||||
flex={'1 0 0'}
|
||||
flexDirection={'column'}
|
||||
gap={1}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
>
|
||||
<Box border={'1px dashed'} borderColor={'myGray.400'} p={2} borderRadius={'full'}>
|
||||
<MyIcon name="common/fileNotFound" />
|
||||
</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
{t('chat:chat.quote.No Data')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionReader;
|
||||
@ -0,0 +1,68 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const DownloadButton = ({
|
||||
canAccessRawData,
|
||||
onDownload,
|
||||
onRead,
|
||||
isLoading
|
||||
}: {
|
||||
canAccessRawData: boolean;
|
||||
onDownload: () => void;
|
||||
onRead: () => void;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (canAccessRawData) {
|
||||
return (
|
||||
<MyMenu
|
||||
size={'xs'}
|
||||
Button={
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size={'xs'}
|
||||
fontSize={'mini'}
|
||||
leftIcon={<MyIcon name={'common/download'} w={'4'} />}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t('common:Download')}
|
||||
</Button>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: t('common:core.dataset.Download the parsed content'),
|
||||
type: 'grayBg',
|
||||
onClick: onDownload
|
||||
},
|
||||
{
|
||||
label: t('common:core.dataset.Get the raw data'),
|
||||
type: 'grayBg',
|
||||
onClick: onRead
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size={'xs'}
|
||||
fontSize={'mini'}
|
||||
leftIcon={<MyIcon name={'common/download'} w={'4'} />}
|
||||
onClick={onDownload}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t('common:Download')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadButton;
|
||||
@ -0,0 +1,47 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const NavButton = ({
|
||||
direction,
|
||||
isDisabled,
|
||||
onClick
|
||||
}: {
|
||||
direction: 'up' | 'down';
|
||||
isDisabled: boolean;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const isUp = direction === 'up';
|
||||
|
||||
const baseStyles = {
|
||||
color: 'myGray.500',
|
||||
border: '1px solid',
|
||||
borderColor: 'myGray.150',
|
||||
borderRadius: 'sm',
|
||||
w: 6,
|
||||
h: 6,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s'
|
||||
};
|
||||
|
||||
const stateStyles = isDisabled
|
||||
? {
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.5,
|
||||
_hover: {}
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
opacity: 1,
|
||||
_hover: { bg: 'myGray.100' },
|
||||
onClick
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex {...baseStyles} {...stateStyles}>
|
||||
<MyIcon name={isUp ? `common/solidChevronUp` : `common/solidChevronDown`} w={'18px'} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavButton;
|
||||
163
projects/app/src/pageComponents/chat/ChatQuoteList/QuoteItem.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import { ScoreItemType } from '@/components/core/dataset/QuoteItem';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import ScoreTag from './ScoreTag';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
|
||||
const QuoteItem = ({
|
||||
index,
|
||||
icon,
|
||||
sourceName,
|
||||
score,
|
||||
q,
|
||||
a
|
||||
}: {
|
||||
index: number;
|
||||
icon: string;
|
||||
sourceName: string;
|
||||
score: { primaryScore?: ScoreItemType; secondaryScore: ScoreItemType[] };
|
||||
q: string;
|
||||
a?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const isDeleted = !q;
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={2}
|
||||
position={'relative'}
|
||||
overflow={'hidden'}
|
||||
border={'1px solid transparent'}
|
||||
borderBottomColor={'myGray.150'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'sm'}
|
||||
_hover={{
|
||||
bg: 'linear-gradient(180deg, #FBFBFC 7.61%, #F0F1F6 100%)',
|
||||
borderTopColor: 'myGray.50',
|
||||
'& .hover-data': { visibility: 'visible' }
|
||||
}}
|
||||
>
|
||||
<Flex gap={2} alignItems={'center'} mb={2}>
|
||||
<Box
|
||||
alignItems={'center'}
|
||||
fontSize={'xs'}
|
||||
border={'sm'}
|
||||
borderRadius={'sm'}
|
||||
_hover={{
|
||||
'.controller': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
overflow={'hidden'}
|
||||
display={'inline-flex'}
|
||||
height={6}
|
||||
>
|
||||
<Flex
|
||||
color={'myGray.500'}
|
||||
bg={'myGray.150'}
|
||||
w={4}
|
||||
justifyContent={'center'}
|
||||
fontSize={'10px'}
|
||||
h={'full'}
|
||||
alignItems={'center'}
|
||||
>
|
||||
{index + 1}
|
||||
</Flex>
|
||||
<Flex px={1.5}>
|
||||
<MyIcon name={icon as any} mr={1} flexShrink={0} w={'12px'} />
|
||||
<Box
|
||||
className="textEllipsis3"
|
||||
wordBreak={'break-all'}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'mini'}
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{sourceName}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
{score && !isDeleted && (
|
||||
<Box className="hover-data" visibility={'hidden'}>
|
||||
<ScoreTag {...score} />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
{!isDeleted ? (
|
||||
<>
|
||||
<Markdown source={q} />
|
||||
{!!a && (
|
||||
<Box>
|
||||
<Markdown source={a} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Flex
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
h={'full'}
|
||||
py={2}
|
||||
bg={'#FAFAFA'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<MyIcon name="common/info" w={'14px'} mr={1} color={'myGray.500'} />
|
||||
{t('chat:chat.quote.deleted')}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
className="hover-data"
|
||||
position={'absolute'}
|
||||
bottom={2}
|
||||
right={5}
|
||||
gap={1.5}
|
||||
visibility={'hidden'}
|
||||
>
|
||||
<MyTooltip label={t('common:core.dataset.Quote Length')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={2}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
>
|
||||
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
|
||||
{q.length + (a?.length || 0)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('common:common.Copy')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={1}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
copyData(q + '\n' + a);
|
||||
}}
|
||||
>
|
||||
<MyIcon name="copy" w={'14px'} color={'myGray.500'} />
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteItem;
|
||||
@ -0,0 +1,152 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import QuoteItem from './QuoteItem';
|
||||
import { useMemo } from 'react';
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import { formatScore } from '@/components/core/dataset/QuoteItem';
|
||||
import { metadataType } from '@/web/core/chat/context/chatItemContext';
|
||||
import { getQuoteDataList } from '@/web/core/chat/api';
|
||||
|
||||
const QuoteReader = ({
|
||||
rawSearch,
|
||||
metadata,
|
||||
chatTime,
|
||||
onClose
|
||||
}: {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata: metadataType;
|
||||
chatTime: Date;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { chatId, appId, outLinkAuthData } = useChatStore();
|
||||
|
||||
const { data, loading } = useRequest2(
|
||||
async () =>
|
||||
await getQuoteDataList({
|
||||
datasetDataIdList: rawSearch.map((item) => item.id),
|
||||
chatTime,
|
||||
collectionIdList: metadata.collectionIdList,
|
||||
chatItemId: metadata.chatItemId,
|
||||
appId,
|
||||
chatId,
|
||||
...outLinkAuthData
|
||||
}),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const filterResults = useMemo(() => {
|
||||
if (!metadata.collectionId) {
|
||||
return rawSearch;
|
||||
}
|
||||
|
||||
return rawSearch.filter(
|
||||
(item) => item.collectionId === metadata.collectionId && item.sourceId === metadata.sourceId
|
||||
);
|
||||
}, [metadata, rawSearch]);
|
||||
|
||||
const formatedDataList = useMemo(() => {
|
||||
return filterResults
|
||||
.map((item) => {
|
||||
const currentFilterItem = data?.quoteList.find((res) => res._id === item.id);
|
||||
|
||||
return {
|
||||
...item,
|
||||
q: currentFilterItem?.q || '',
|
||||
a: currentFilterItem?.a || '',
|
||||
score: formatScore(item.score),
|
||||
icon: getSourceNameIcon({
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName
|
||||
})
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return (b.score.primaryScore?.value || 0) - (a.score.primaryScore?.value || 0);
|
||||
});
|
||||
}, [data?.quoteList, filterResults]);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'full'}>
|
||||
{/* title */}
|
||||
<Flex
|
||||
w={'full'}
|
||||
alignItems={'center'}
|
||||
px={5}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.150'}
|
||||
>
|
||||
<Box flex={1} py={4}>
|
||||
<Flex gap={2} mr={2} mb={1}>
|
||||
<MyIcon
|
||||
name={
|
||||
metadata.sourceId && metadata.sourceName
|
||||
? (getSourceNameIcon({
|
||||
sourceId: metadata.sourceId,
|
||||
sourceName: metadata.sourceName
|
||||
}) as any)
|
||||
: 'core/chat/quoteFill'
|
||||
}
|
||||
w={['1rem', '1.25rem']}
|
||||
color={'primary.600'}
|
||||
/>
|
||||
<Box
|
||||
maxW={['200px', '300px']}
|
||||
className={'textEllipsis'}
|
||||
wordBreak={'break-all'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{metadata.sourceName
|
||||
? metadata.sourceName
|
||||
: t('common:core.chat.Quote Amount', { amount: rawSearch.length })}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize={'mini'} color={'myGray.500'}>
|
||||
{t('common:core.chat.quote.Quote Tip')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
borderRadius={'sm'}
|
||||
p={1}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<MyIcon name="common/closeLight" color={'myGray.900'} w={6} />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* quote list */}
|
||||
<MyBox flex={'1 0 0'} mt={2} px={5} py={1} overflow={'auto'} isLoading={loading}>
|
||||
{!loading && (
|
||||
<Flex flexDir={'column'} gap={3}>
|
||||
{formatedDataList?.map((item, index) => (
|
||||
<QuoteItem
|
||||
key={item.id}
|
||||
index={index}
|
||||
icon={item.icon}
|
||||
sourceName={item.sourceName}
|
||||
score={item.score}
|
||||
q={item.q}
|
||||
a={item.a}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</MyBox>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteReader;
|
||||
@ -0,0 +1,78 @@
|
||||
import { ScoreItemType, scoreTheme } from '@/components/core/dataset/QuoteItem';
|
||||
import { Box, Flex, Progress } from '@chakra-ui/react';
|
||||
import { SearchScoreTypeMap } from '@fastgpt/global/core/dataset/constants';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ScoreTag = (score: { primaryScore?: ScoreItemType; secondaryScore: ScoreItemType[] }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex alignItems={'center'} flexWrap={'wrap'} gap={3}>
|
||||
{score?.primaryScore && (
|
||||
<MyTooltip
|
||||
label={
|
||||
score.secondaryScore.length ? (
|
||||
<Box>
|
||||
{score.secondaryScore.map((item, i) => (
|
||||
<Box fontSize={'xs'} key={i}>
|
||||
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
|
||||
<Box
|
||||
px={'5px'}
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'sm'}
|
||||
mr={'2px'}
|
||||
{...(scoreTheme[i] && scoreTheme[i])}
|
||||
>
|
||||
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
|
||||
</Box>
|
||||
<Box transform={'scale(0.9)'}>
|
||||
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box h={'4px'}>
|
||||
{SearchScoreTypeMap[item.type]?.showScore && (
|
||||
<Progress
|
||||
value={item.value * 100}
|
||||
h={'4px'}
|
||||
w={'100%'}
|
||||
size="sm"
|
||||
borderRadius={'20px'}
|
||||
{...(scoreTheme[i] && {
|
||||
colorScheme: scoreTheme[i].colorScheme
|
||||
})}
|
||||
bg="#E8EBF0"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
borderRadius={'sm'}
|
||||
py={1}
|
||||
px={2}
|
||||
color={'green.600'}
|
||||
bg={'green.50'}
|
||||
alignItems={'center'}
|
||||
fontSize={'11px'}
|
||||
>
|
||||
<Box>
|
||||
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
|
||||
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
|
||||
? ` ${score.primaryScore.value.toFixed(4)}`
|
||||
: `: ${score.primaryScore.index + 1}`}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScoreTag;
|
||||
42
projects/app/src/pageComponents/chat/ChatQuoteList/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatItemContext, metadataType } from '@/web/core/chat/context/chatItemContext';
|
||||
import CollectionQuoteReader from './CollectionQuoteReader';
|
||||
import QuoteReader from './QuoteReader';
|
||||
|
||||
const ChatQuoteList = ({
|
||||
chatTime,
|
||||
rawSearch = [],
|
||||
metadata,
|
||||
onClose
|
||||
}: {
|
||||
chatTime: Date;
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata: metadataType;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const isShowReadRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
|
||||
|
||||
return (
|
||||
<>
|
||||
{metadata.collectionId && isShowReadRawSource ? (
|
||||
<CollectionQuoteReader
|
||||
rawSearch={rawSearch}
|
||||
metadata={metadata}
|
||||
chatTime={chatTime}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<QuoteReader
|
||||
rawSearch={rawSearch}
|
||||
metadata={metadata}
|
||||
chatTime={chatTime}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatQuoteList;
|
||||
@ -5,7 +5,6 @@ import {
|
||||
delOneDatasetDataById,
|
||||
getDatasetCollectionById
|
||||
} from '@/web/core/dataset/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
|
||||
@ -178,7 +178,6 @@ const InputDataModal = ({
|
||||
async (e: InputDataType) => {
|
||||
if (!dataId) return Promise.reject(t('common:common.error.unKnow'));
|
||||
|
||||
// not exactly same
|
||||
await putDatasetDataById({
|
||||
dataId,
|
||||
q: e.q,
|
||||
|
||||
250
projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
|
||||
import { DatasetDataSchemaType } from '@fastgpt/global/core/dataset/type';
|
||||
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { LinkedListResponse, LinkedPaginationProps } from '@fastgpt/web/common/fetch/type';
|
||||
import { FilterQuery, Types } from 'mongoose';
|
||||
import { dataFieldSelector, processChatTimeFilter } from './getQuote';
|
||||
|
||||
export type GetCollectionQuoteProps = LinkedPaginationProps & {
|
||||
chatTime: Date;
|
||||
|
||||
isInitialLoad: boolean;
|
||||
collectionId: string;
|
||||
chatItemId: string;
|
||||
appId: string;
|
||||
chatId: string;
|
||||
shareId?: string;
|
||||
outLinkUid?: string;
|
||||
teamId?: string;
|
||||
teamToken?: string;
|
||||
};
|
||||
|
||||
export type GetCollectionQuoteRes = LinkedListResponse<DatasetDataSchemaType>;
|
||||
|
||||
type BaseMatchType = FilterQuery<DatasetDataSchemaType>;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<GetCollectionQuoteProps>
|
||||
): Promise<GetCollectionQuoteRes> {
|
||||
const {
|
||||
initialId,
|
||||
initialIndex,
|
||||
prevId,
|
||||
prevIndex,
|
||||
nextId,
|
||||
nextIndex,
|
||||
chatTime,
|
||||
|
||||
isInitialLoad,
|
||||
collectionId,
|
||||
chatItemId,
|
||||
appId,
|
||||
chatId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken,
|
||||
pageSize = 15
|
||||
} = req.body;
|
||||
const limitedPageSize = Math.min(pageSize, 30);
|
||||
|
||||
await Promise.all([
|
||||
authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
appId,
|
||||
chatId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
}),
|
||||
authCollectionInChat({ appId, chatId, chatItemId, collectionId })
|
||||
]);
|
||||
|
||||
const baseMatch: BaseMatchType = {
|
||||
collectionId,
|
||||
$or: [
|
||||
{ updateTime: { $lt: new Date(chatTime) } },
|
||||
{ history: { $elemMatch: { updateTime: { $lt: new Date(chatTime) } } } }
|
||||
]
|
||||
};
|
||||
|
||||
if (initialId && initialIndex !== undefined) {
|
||||
return await handleInitialLoad(
|
||||
initialId,
|
||||
initialIndex,
|
||||
limitedPageSize,
|
||||
chatTime,
|
||||
chatItemId,
|
||||
isInitialLoad,
|
||||
baseMatch
|
||||
);
|
||||
}
|
||||
|
||||
if ((prevId && prevIndex !== undefined) || (nextId && nextIndex !== undefined)) {
|
||||
return await handlePaginatedLoad(
|
||||
prevId,
|
||||
prevIndex,
|
||||
nextId,
|
||||
nextIndex,
|
||||
limitedPageSize,
|
||||
chatTime,
|
||||
chatItemId,
|
||||
baseMatch
|
||||
);
|
||||
}
|
||||
|
||||
return { list: [], hasMorePrev: false, hasMoreNext: false };
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
async function handleInitialLoad(
|
||||
initialId: string,
|
||||
initialIndex: number,
|
||||
pageSize: number,
|
||||
chatTime: Date,
|
||||
chatItemId: string,
|
||||
isInitialLoad: boolean,
|
||||
baseMatch: BaseMatchType
|
||||
): Promise<GetCollectionQuoteRes> {
|
||||
const centerNode = await MongoDatasetData.findOne(
|
||||
{
|
||||
_id: new Types.ObjectId(initialId)
|
||||
},
|
||||
dataFieldSelector
|
||||
).lean();
|
||||
|
||||
if (!centerNode) {
|
||||
if (isInitialLoad) {
|
||||
const list = await MongoDatasetData.find(baseMatch, dataFieldSelector)
|
||||
.sort({ chunkIndex: 1, _id: -1 })
|
||||
.limit(pageSize)
|
||||
.lean();
|
||||
|
||||
const listRes = list.map((item, index) => ({
|
||||
...item,
|
||||
index: item.chunkIndex
|
||||
}));
|
||||
|
||||
const hasMoreNext = list.length === pageSize;
|
||||
|
||||
return {
|
||||
list: listRes,
|
||||
hasMorePrev: false,
|
||||
hasMoreNext
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject('centerNode not found');
|
||||
}
|
||||
|
||||
const prevHalfSize = Math.floor(pageSize / 2);
|
||||
const nextHalfSize = pageSize - prevHalfSize - 1;
|
||||
|
||||
const { list: prevList, hasMore: hasMorePrev } = await getPrevNodes(
|
||||
initialId,
|
||||
initialIndex,
|
||||
prevHalfSize,
|
||||
baseMatch
|
||||
);
|
||||
const { list: nextList, hasMore: hasMoreNext } = await getNextNodes(
|
||||
initialId,
|
||||
initialIndex,
|
||||
nextHalfSize,
|
||||
baseMatch
|
||||
);
|
||||
|
||||
const resultList = [...prevList, centerNode, ...nextList];
|
||||
|
||||
const list = processChatTimeFilter(resultList, chatTime);
|
||||
|
||||
return {
|
||||
list: list.map((item) => ({
|
||||
...item,
|
||||
index: item.chunkIndex
|
||||
})),
|
||||
hasMorePrev,
|
||||
hasMoreNext
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePaginatedLoad(
|
||||
prevId: string | undefined,
|
||||
prevIndex: number | undefined,
|
||||
nextId: string | undefined,
|
||||
nextIndex: number | undefined,
|
||||
pageSize: number,
|
||||
chatTime: Date,
|
||||
chatItemId: string,
|
||||
baseMatch: BaseMatchType
|
||||
): Promise<GetCollectionQuoteRes> {
|
||||
const { list, hasMore } =
|
||||
prevId && prevIndex !== undefined
|
||||
? await getPrevNodes(prevId, prevIndex, pageSize, baseMatch)
|
||||
: await getNextNodes(nextId!, nextIndex!, pageSize, baseMatch);
|
||||
|
||||
const processedList = processChatTimeFilter(list, chatTime);
|
||||
|
||||
return {
|
||||
list: processedList.map((item) => ({
|
||||
...item,
|
||||
index: item.chunkIndex
|
||||
})),
|
||||
hasMorePrev: !!prevId && hasMore,
|
||||
hasMoreNext: !!nextId && hasMore
|
||||
};
|
||||
}
|
||||
|
||||
async function getPrevNodes(
|
||||
initialId: string,
|
||||
initialIndex: number,
|
||||
limit: number,
|
||||
baseMatch: BaseMatchType
|
||||
) {
|
||||
const match: BaseMatchType = {
|
||||
...baseMatch,
|
||||
$or: [
|
||||
{ chunkIndex: { $lte: initialIndex } },
|
||||
{ chunkIndex: initialIndex, _id: { $lte: new Types.ObjectId(initialId) } }
|
||||
]
|
||||
};
|
||||
|
||||
const list = await MongoDatasetData.find(match, dataFieldSelector)
|
||||
.sort({ chunkIndex: -1, _id: 1 })
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
return {
|
||||
list: list.filter((item) => String(item._id) !== initialId).reverse(),
|
||||
hasMore: list.length === limit
|
||||
};
|
||||
}
|
||||
|
||||
async function getNextNodes(
|
||||
initialId: string,
|
||||
initialIndex: number,
|
||||
limit: number,
|
||||
baseMatch: BaseMatchType
|
||||
) {
|
||||
const match: BaseMatchType = {
|
||||
...baseMatch,
|
||||
$or: [
|
||||
{ chunkIndex: { $gte: initialIndex } },
|
||||
{ chunkIndex: initialIndex, _id: { $gte: new Types.ObjectId(initialId) } }
|
||||
]
|
||||
};
|
||||
|
||||
const list = await MongoDatasetData.find(match, dataFieldSelector)
|
||||
.sort({ chunkIndex: 1, _id: -1 })
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
return {
|
||||
list: list.filter((item) => String(item._id) !== initialId),
|
||||
hasMore: list.length === limit
|
||||
};
|
||||
}
|
||||
102
projects/app/src/pages/api/core/chat/quote/getQuote.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
|
||||
import { DatasetDataSchemaType } from '@fastgpt/global/core/dataset/type';
|
||||
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
|
||||
export type GetQuoteDataProps = {
|
||||
datasetDataIdList: string[];
|
||||
chatTime: Date;
|
||||
|
||||
collectionIdList: string[];
|
||||
chatItemId: string;
|
||||
appId: string;
|
||||
chatId: string;
|
||||
shareId?: string;
|
||||
outLinkUid?: string;
|
||||
teamId?: string;
|
||||
teamToken?: string;
|
||||
};
|
||||
|
||||
export type GetQuoteDataRes = {
|
||||
quoteList: DatasetDataSchemaType[];
|
||||
};
|
||||
|
||||
export const dataFieldSelector =
|
||||
'_id datasetId collectionId q a chunkIndex history updateTime currentChatItemId prevId';
|
||||
|
||||
async function handler(req: ApiRequestProps<GetQuoteDataProps>): Promise<GetQuoteDataRes> {
|
||||
const {
|
||||
datasetDataIdList,
|
||||
chatTime,
|
||||
|
||||
collectionIdList,
|
||||
chatItemId,
|
||||
chatId,
|
||||
appId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
} = req.body;
|
||||
|
||||
await authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
appId,
|
||||
chatId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
collectionIdList.map(async (collectionId) => {
|
||||
await authCollectionInChat({ appId, chatId, chatItemId, collectionId });
|
||||
})
|
||||
);
|
||||
|
||||
const list = await MongoDatasetData.find(
|
||||
{ _id: { $in: datasetDataIdList } },
|
||||
dataFieldSelector
|
||||
).lean();
|
||||
|
||||
const quoteList = processChatTimeFilter(list, chatTime);
|
||||
|
||||
return {
|
||||
quoteList
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
export function processChatTimeFilter(list: DatasetDataSchemaType[], chatTime?: Date) {
|
||||
if (!chatTime) return list;
|
||||
|
||||
return list.map((item) => {
|
||||
if (!item.history) return item;
|
||||
|
||||
const { history, ...rest } = item;
|
||||
const formatedChatTime = new Date(chatTime);
|
||||
|
||||
if (item.updateTime <= formatedChatTime) {
|
||||
return rest;
|
||||
}
|
||||
|
||||
const latestHistoryIndex = history.findIndex(
|
||||
(historyItem: any) => historyItem.updateTime <= formatedChatTime
|
||||
);
|
||||
|
||||
if (latestHistoryIndex === -1) return rest;
|
||||
|
||||
const latestHistory = history[latestHistoryIndex];
|
||||
|
||||
return {
|
||||
...rest,
|
||||
q: latestHistory?.q || item.q,
|
||||
a: latestHistory?.a || item.a,
|
||||
updated: true
|
||||
};
|
||||
});
|
||||
}
|
||||
78
projects/app/src/pages/api/core/dataset/collection/export.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
|
||||
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
|
||||
import { responseWriteController } from '@fastgpt/service/common/response';
|
||||
import { addLog } from '@fastgpt/service/common/system/log';
|
||||
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { NextApiResponse } from 'next';
|
||||
|
||||
export type ExportCollectionBody = {
|
||||
collectionId: string;
|
||||
chatTime: Date;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<ExportCollectionBody, {}>, res: NextApiResponse) {
|
||||
let { collectionId, chatTime } = req.body;
|
||||
|
||||
const { teamId, collection } = await authDatasetCollection({
|
||||
req,
|
||||
authToken: true,
|
||||
collectionId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
const where = {
|
||||
teamId,
|
||||
datasetId: collection.datasetId,
|
||||
collectionId,
|
||||
...(chatTime
|
||||
? {
|
||||
$or: [
|
||||
{ updateTime: { $lt: new Date(chatTime) } },
|
||||
{ history: { $elemMatch: { updateTime: { $lt: new Date(chatTime) } } } }
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8;');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=usage.csv; ');
|
||||
|
||||
const cursor = MongoDatasetData.find(where, 'q a', {
|
||||
...readFromSecondary,
|
||||
batchSize: 1000
|
||||
})
|
||||
.sort({ chunkIndex: 1 })
|
||||
.limit(50000)
|
||||
.cursor();
|
||||
|
||||
const write = responseWriteController({
|
||||
res,
|
||||
readStream: cursor
|
||||
});
|
||||
|
||||
cursor.on('data', (doc) => {
|
||||
const res = doc.a ? `\n${doc.q}\n${doc.a}` : `\n${doc.q}`;
|
||||
|
||||
write(res);
|
||||
});
|
||||
|
||||
cursor.on('end', () => {
|
||||
cursor.close();
|
||||
res.end();
|
||||
});
|
||||
|
||||
cursor.on('error', (err) => {
|
||||
addLog.error(`export usage error`, err);
|
||||
res.status(500);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(
|
||||
useIPFrequencyLimit({ id: 'export-usage', seconds: 60, limit: 1, force: true }),
|
||||
handler
|
||||
);
|
||||
@ -7,9 +7,7 @@ import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/con
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { AIChatItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
|
||||
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
|
||||
import { useApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset/api';
|
||||
import { POST } from '@fastgpt/service/common/api/plusRequest';
|
||||
@ -29,57 +27,6 @@ export type readCollectionSourceResponse = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const authCollectionInChat = async ({
|
||||
collectionId,
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId
|
||||
}: {
|
||||
collectionId: string;
|
||||
appId: string;
|
||||
chatId: string;
|
||||
chatItemId: string;
|
||||
}) => {
|
||||
try {
|
||||
const chatItem = (await MongoChatItem.findOne(
|
||||
{
|
||||
appId,
|
||||
chatId,
|
||||
dataId: chatItemId
|
||||
},
|
||||
'responseData'
|
||||
).lean()) as AIChatItemType;
|
||||
|
||||
if (!chatItem) return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
|
||||
// 找 responseData 里,是否有该文档 id
|
||||
const responseData = chatItem.responseData || [];
|
||||
const flatResData: ChatHistoryItemResType[] =
|
||||
responseData
|
||||
?.map((item) => {
|
||||
return [
|
||||
item,
|
||||
...(item.pluginDetail || []),
|
||||
...(item.toolDetail || []),
|
||||
...(item.loopDetail || [])
|
||||
];
|
||||
})
|
||||
.flat() || [];
|
||||
|
||||
if (
|
||||
flatResData.some((item) => {
|
||||
if (item.quoteList) {
|
||||
return item.quoteList.some((quote) => quote.collectionId === collectionId);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {}
|
||||
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<readCollectionSourceBody, readCollectionSourceQuery>
|
||||
): Promise<readCollectionSourceResponse> {
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
|
||||
|
||||
export type GetQuotePermissionResponse =
|
||||
| {
|
||||
permission: {
|
||||
hasWritePer: boolean;
|
||||
hasReadPer: boolean;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
async function handler(req: NextApiRequest): Promise<GetQuotePermissionResponse> {
|
||||
const { id: datasetId } = req.query as {
|
||||
id?: string;
|
||||
};
|
||||
if (!datasetId) {
|
||||
return Promise.reject('datasetId is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const { permission } = await authDataset({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
datasetId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
return {
|
||||
permission: {
|
||||
hasReadPer: permission.hasReadPer,
|
||||
hasWritePer: permission.hasWritePer
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error === DatasetErrEnum.unAuthDataset) {
|
||||
return {
|
||||
permission: {
|
||||
hasWritePer: false,
|
||||
hasReadPer: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@ -45,7 +45,7 @@ async function handler(
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
MongoDatasetData.find(match, '_id datasetId collectionId q a chunkIndex')
|
||||
.sort({ chunkIndex: 1, updateTime: -1 })
|
||||
.sort({ chunkIndex: 1, _id: -1 })
|
||||
.skip(offset)
|
||||
.limit(pageSize)
|
||||
.lean(),
|
||||
|
||||
@ -37,6 +37,7 @@ async function handler(
|
||||
responseDetail,
|
||||
showRawSource,
|
||||
showNodeStatus,
|
||||
// showFullText,
|
||||
limit,
|
||||
app
|
||||
});
|
||||
|
||||
@ -59,6 +59,7 @@ import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatc
|
||||
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
|
||||
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
|
||||
import { ExternalProviderType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
type FastGptWebChatProps = {
|
||||
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import NextHead from '@/components/common/NextHead';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getInitChatInfo } from '@/web/core/chat/api';
|
||||
@ -36,6 +36,7 @@ import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/contex
|
||||
import ChatRecordContextProvider, {
|
||||
ChatRecordContext
|
||||
} from '@/web/core/chat/context/chatRecordContext';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox'));
|
||||
|
||||
@ -58,6 +59,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
|
||||
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
|
||||
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
|
||||
@ -138,13 +141,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
},
|
||||
[appId, chatId, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat]
|
||||
);
|
||||
|
||||
const RenderHistorySlider = useMemo(() => {
|
||||
const Children = (
|
||||
<ChatHistorySlider confirmClearText={t('common:core.chat.Confirm to clear history')} />
|
||||
);
|
||||
|
||||
return isPc || !appId ? (
|
||||
<SideBar>{Children}</SideBar>
|
||||
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
|
||||
) : (
|
||||
<Drawer
|
||||
isOpen={isOpenSlider}
|
||||
@ -157,7 +161,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
<DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}, [appId, isOpenSlider, isPc, onCloseSlider, t]);
|
||||
}, [t, isPc, appId, isOpenSlider, onCloseSlider, quoteData]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'}>
|
||||
@ -169,7 +173,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<PageContainer isLoading={loading} flex={'1 0 0'} w={0} p={[0, '16px']} position={'relative'}>
|
||||
<PageContainer
|
||||
isLoading={loading}
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
p={[0, '16px']}
|
||||
pr={quoteData ? '8px !important' : '16px'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Flex h={'100%'} flexDirection={['column', 'row']}>
|
||||
{/* pc always show history. */}
|
||||
{RenderHistorySlider}
|
||||
@ -215,6 +226,16 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageContainer>
|
||||
{quoteData && (
|
||||
<PageContainer w={['full', '588px']} insertProps={{ bg: 'white' }}>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</PageContainer>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@ -278,6 +299,7 @@ const Render = (props: { appId: string; isStandalone?: string }) => {
|
||||
showRouteToAppDetail={isStandalone !== '1'}
|
||||
showRouteToDatasetDetail={isStandalone !== '1'}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
|
||||
@ -37,6 +37,7 @@ import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { useI18nLng } from '@fastgpt/web/hooks/useI18n';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox'));
|
||||
|
||||
@ -49,6 +50,7 @@ type Props = {
|
||||
authToken: string;
|
||||
customUid: string;
|
||||
showRawSource: boolean;
|
||||
// showFullText: boolean;
|
||||
showNodeStatus: boolean;
|
||||
};
|
||||
|
||||
@ -81,6 +83,8 @@ const OutLink = (props: Props) => {
|
||||
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
|
||||
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
|
||||
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
|
||||
@ -217,7 +221,7 @@ const OutLink = (props: Props) => {
|
||||
if (showHistory !== '1') return null;
|
||||
|
||||
return isPc ? (
|
||||
<SideBar>{Children}</SideBar>
|
||||
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
|
||||
) : (
|
||||
<Drawer
|
||||
isOpen={isOpenSlider}
|
||||
@ -232,10 +236,10 @@ const OutLink = (props: Props) => {
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}, [isOpenSlider, isPc, onCloseSlider, showHistory, t]);
|
||||
}, [isOpenSlider, isPc, onCloseSlider, quoteData, showHistory, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box h={'full'} display={quoteData ? 'flex' : ''}>
|
||||
<NextHead
|
||||
title={props.appName || data?.app?.name || 'AI'}
|
||||
desc={props.appIntro || data?.app?.intro}
|
||||
@ -291,7 +295,17 @@ const OutLink = (props: Props) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageContainer>
|
||||
</>
|
||||
{quoteData && (
|
||||
<PageContainer w={['full', '800px']} py={5}>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</PageContainer>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -340,6 +354,7 @@ const Render = (props: Props) => {
|
||||
showRouteToAppDetail={false}
|
||||
showRouteToDatasetDetail={false}
|
||||
isShowReadRawSource={props.showRawSource}
|
||||
// isShowFullText={props.showFullText}
|
||||
showNodeStatus={props.showNodeStatus}
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
@ -383,6 +398,7 @@ export async function getServerSideProps(context: any) {
|
||||
appAvatar: app?.associatedApp?.avatar ?? '',
|
||||
appIntro: app?.associatedApp?.intro ?? 'AI',
|
||||
showRawSource: app?.showRawSource ?? false,
|
||||
// showFullText: app?.showFullText ?? false,
|
||||
showNodeStatus: app?.showNodeStatus ?? false,
|
||||
shareId: shareId ?? '',
|
||||
authToken: authToken ?? '',
|
||||
|
||||
@ -33,6 +33,7 @@ import ChatRecordContextProvider, {
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { useMount } from 'ahooks';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox'));
|
||||
|
||||
type Props = { appId: string; chatId: string; teamId: string; teamToken: string };
|
||||
@ -63,6 +64,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
|
||||
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
|
||||
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
|
||||
@ -163,7 +166,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
);
|
||||
|
||||
return isPc || !appId ? (
|
||||
<SideBar>{Children}</SideBar>
|
||||
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
|
||||
) : (
|
||||
<Drawer
|
||||
isOpen={isOpenSlider}
|
||||
@ -176,7 +179,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
<DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}, [appId, isOpenSlider, isPc, onCloseSlider, t]);
|
||||
}, [appId, isOpenSlider, isPc, onCloseSlider, quoteData, t]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'}>
|
||||
@ -231,6 +234,17 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageContainer>
|
||||
|
||||
{quoteData && (
|
||||
<PageContainer w={['full', '800px']} py={5}>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</PageContainer>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@ -300,6 +314,7 @@ const Render = (props: Props) => {
|
||||
showRouteToAppDetail={false}
|
||||
showRouteToDatasetDetail={false}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
|
||||
@ -220,11 +220,7 @@ export async function updateData2Dataset({
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update mongo updateTime(便于脏数据检查器识别)
|
||||
mongoData.updateTime = new Date();
|
||||
await mongoData.save();
|
||||
|
||||
// 5. Insert vector
|
||||
// insert vector
|
||||
const insertResult = await Promise.all(
|
||||
patchResult
|
||||
.filter((item) => item.type === 'create' || item.type === 'update')
|
||||
@ -249,9 +245,19 @@ export async function updateData2Dataset({
|
||||
.filter((item) => item.type !== 'delete')
|
||||
.map((item) => item.index) as DatasetDataIndexItemType[];
|
||||
|
||||
// console.log(clonePatchResult2Insert);
|
||||
await mongoSessionRun(async (session) => {
|
||||
// Update MongoData
|
||||
// update mongo data
|
||||
mongoData.history =
|
||||
q !== mongoData.q || a !== mongoData.a
|
||||
? [
|
||||
{
|
||||
q: mongoData.q,
|
||||
a: mongoData.a,
|
||||
updateTime: mongoData.updateTime
|
||||
},
|
||||
...(mongoData.history?.slice(0, 9) || [])
|
||||
]
|
||||
: mongoData.history;
|
||||
mongoData.q = q || mongoData.q;
|
||||
mongoData.a = a ?? mongoData.a;
|
||||
mongoData.indexes = newIndexes;
|
||||
@ -277,6 +283,10 @@ export async function updateData2Dataset({
|
||||
}
|
||||
});
|
||||
|
||||
// Update mongo updateTime(便于脏数据检查器识别)
|
||||
mongoData.updateTime = new Date();
|
||||
await mongoData.save();
|
||||
|
||||
return {
|
||||
tokens
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ChatSchema } from '@fastgpt/global/core/chat/type';
|
||||
import { AIChatItemType, ChatHistoryItemResType, ChatSchema } from '@fastgpt/global/core/chat/type';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { AuthModeType } from '@fastgpt/service/support/permission/type';
|
||||
import { authOutLink } from './outLink';
|
||||
@ -6,6 +6,8 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
|
||||
import { authTeamSpaceToken } from './team';
|
||||
import { AuthUserTypeEnum, ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
|
||||
|
||||
/*
|
||||
检查chat的权限:
|
||||
@ -184,3 +186,54 @@ export async function authChatCrud({
|
||||
|
||||
return Promise.reject(ChatErrEnum.unAuthChat);
|
||||
}
|
||||
|
||||
export const authCollectionInChat = async ({
|
||||
collectionId,
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId
|
||||
}: {
|
||||
collectionId: string;
|
||||
appId: string;
|
||||
chatId: string;
|
||||
chatItemId: string;
|
||||
}) => {
|
||||
try {
|
||||
const chatItem = (await MongoChatItem.findOne(
|
||||
{
|
||||
appId,
|
||||
chatId,
|
||||
dataId: chatItemId
|
||||
},
|
||||
'responseData'
|
||||
).lean()) as AIChatItemType;
|
||||
|
||||
if (!chatItem) return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
|
||||
// 找 responseData 里,是否有该文档 id
|
||||
const responseData = chatItem.responseData || [];
|
||||
const flatResData: ChatHistoryItemResType[] =
|
||||
responseData
|
||||
?.map((item) => {
|
||||
return [
|
||||
item,
|
||||
...(item.pluginDetail || []),
|
||||
...(item.toolDetail || []),
|
||||
...(item.loopDetail || [])
|
||||
];
|
||||
})
|
||||
.flat() || [];
|
||||
|
||||
if (
|
||||
flatResData.some((item) => {
|
||||
if (item.quoteList) {
|
||||
return item.quoteList.some((quote) => quote.collectionId === collectionId);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {}
|
||||
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
};
|
||||
|
||||
@ -28,6 +28,7 @@ export const defaultOutLinkForm: OutLinkEditType = {
|
||||
name: '',
|
||||
showNodeStatus: true,
|
||||
responseDetail: false,
|
||||
// showFullText: false,
|
||||
showRawSource: false,
|
||||
limit: {
|
||||
QPM: 100,
|
||||
|
||||
@ -30,6 +30,11 @@ import type {
|
||||
getPaginationRecordsBody,
|
||||
getPaginationRecordsResponse
|
||||
} from '@/pages/api/core/chat/getPaginationRecords';
|
||||
import { GetQuoteDataProps, GetQuoteDataRes } from '@/pages/api/core/chat/quote/getQuote';
|
||||
import {
|
||||
GetCollectionQuoteProps,
|
||||
GetCollectionQuoteRes
|
||||
} from '@/pages/api/core/chat/quote/getCollectionQuote';
|
||||
|
||||
/**
|
||||
* 获取初始化聊天内容
|
||||
@ -100,3 +105,9 @@ export const getMyTokensApps = (data: AuthTeamTagTokenProps) =>
|
||||
*/
|
||||
export const getinitTeamChat = (data: { teamId: string; authToken: string; appId: string }) =>
|
||||
GET(`/proApi/core/chat/initTeamChat`, data);
|
||||
|
||||
export const getQuoteDataList = (data: GetQuoteDataProps) =>
|
||||
POST<GetQuoteDataRes>(`/core/chat/quote/getQuote`, data);
|
||||
|
||||
export const getCollectionQuote = (data: GetCollectionQuoteProps) =>
|
||||
POST<GetCollectionQuoteRes>(`/core/chat/quote/getCollectionQuote`, data);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ChatBoxInputFormType } from '@/components/core/chat/ChatContainer/ChatBox/type';
|
||||
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
|
||||
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { ComponentRef as ChatComponentRef } from '@/components/core/chat/ChatContainer/ChatBox/type';
|
||||
import { useForm, UseFormReturn } from 'react-hook-form';
|
||||
@ -8,11 +8,13 @@ import { defaultChatData } from '@/global/core/chat/constants';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppChatConfigType, VariableItemType } from '@fastgpt/global/core/app/type';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
|
||||
type ContextProps = {
|
||||
showRouteToAppDetail: boolean;
|
||||
showRouteToDatasetDetail: boolean;
|
||||
isShowReadRawSource: boolean;
|
||||
// isShowFullText: boolean;
|
||||
showNodeStatus: boolean;
|
||||
};
|
||||
type ChatBoxDataType = {
|
||||
@ -30,6 +32,21 @@ type ChatBoxDataType = {
|
||||
};
|
||||
};
|
||||
|
||||
export type metadataType = {
|
||||
collectionId: string;
|
||||
collectionIdList: string[];
|
||||
chatItemId: string;
|
||||
sourceId: string;
|
||||
sourceName: string;
|
||||
datasetId: string;
|
||||
};
|
||||
|
||||
export type QuoteDataType = {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata: metadataType;
|
||||
chatTime: Date;
|
||||
};
|
||||
|
||||
type ChatItemContextType = {
|
||||
ChatBoxRef: React.RefObject<ChatComponentRef> | null;
|
||||
variablesForm: UseFormReturn<ChatBoxInputFormType, any>;
|
||||
@ -43,6 +60,9 @@ type ChatItemContextType = {
|
||||
chatBoxData: ChatBoxDataType;
|
||||
setChatBoxData: React.Dispatch<React.SetStateAction<ChatBoxDataType>>;
|
||||
isPlugin: boolean;
|
||||
|
||||
quoteData?: QuoteDataType;
|
||||
setQuoteData: React.Dispatch<React.SetStateAction<QuoteDataType | undefined>>;
|
||||
} & ContextProps;
|
||||
|
||||
export const ChatItemContext = createContext<ChatItemContextType>({
|
||||
@ -61,6 +81,11 @@ export const ChatItemContext = createContext<ChatItemContextType>({
|
||||
},
|
||||
clearChatRecords: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
|
||||
quoteData: undefined,
|
||||
setQuoteData: function (value: React.SetStateAction<QuoteDataType | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
@ -72,13 +97,14 @@ const ChatItemContextProvider = ({
|
||||
showRouteToAppDetail,
|
||||
showRouteToDatasetDetail,
|
||||
isShowReadRawSource,
|
||||
// isShowFullText,
|
||||
showNodeStatus
|
||||
}: {
|
||||
children: ReactNode;
|
||||
} & ContextProps) => {
|
||||
const ChatBoxRef = useRef<ChatComponentRef>(null);
|
||||
const variablesForm = useForm<ChatBoxInputFormType>();
|
||||
|
||||
const [quoteData, setQuoteData] = useState<QuoteDataType>();
|
||||
const [chatBoxData, setChatBoxData] = useState<ChatBoxDataType>({
|
||||
...defaultChatData
|
||||
});
|
||||
@ -131,7 +157,11 @@ const ChatItemContextProvider = ({
|
||||
showRouteToAppDetail,
|
||||
showRouteToDatasetDetail,
|
||||
isShowReadRawSource,
|
||||
showNodeStatus
|
||||
// isShowFullText,
|
||||
showNodeStatus,
|
||||
|
||||
quoteData,
|
||||
setQuoteData
|
||||
};
|
||||
}, [
|
||||
chatBoxData,
|
||||
@ -143,7 +173,10 @@ const ChatItemContextProvider = ({
|
||||
showRouteToAppDetail,
|
||||
showRouteToDatasetDetail,
|
||||
isShowReadRawSource,
|
||||
showNodeStatus
|
||||
// isShowFullText,
|
||||
showNodeStatus,
|
||||
quoteData,
|
||||
setQuoteData
|
||||
]);
|
||||
|
||||
return <ChatItemContext.Provider value={contextValue}>{children}</ChatItemContext.Provider>;
|
||||
|
||||
@ -65,6 +65,7 @@ import type {
|
||||
listExistIdResponse
|
||||
} from '@/pages/api/core/dataset/apiDataset/listExistId';
|
||||
import { GetQuoteDataResponse } from '@/pages/api/core/dataset/data/getQuoteData';
|
||||
import { GetQuotePermissionResponse } from '@/pages/api/core/dataset/data/getPermission';
|
||||
|
||||
/* ======================== dataset ======================= */
|
||||
export const getDatasets = (data: GetDatasetListBody) =>
|
||||
@ -179,6 +180,9 @@ export const getAllTags = (datasetId: string) =>
|
||||
export const getDatasetDataList = (data: GetDatasetDataListProps) =>
|
||||
POST<GetDatasetDataListRes>(`/core/dataset/data/v2/list`, data);
|
||||
|
||||
export const getDatasetDataPermission = (id?: string) =>
|
||||
GET<GetQuotePermissionResponse>(`/core/dataset/data/getPermission`, { id });
|
||||
|
||||
export const getDatasetDataItemById = (id: string) =>
|
||||
GET<DatasetDataItemType>(`/core/dataset/data/detail`, { id });
|
||||
|
||||
|
||||