Compare commits

..

7 Commits

Author SHA1 Message Date
ccb1c269e8 fix: Handling Null Values in SQL Execution Results (#10332)
### What problem does this PR solve?

Close #10324
The agent reported an error "undefined" after using the ExeSql tool.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)

now:
<img width="641" height="687" alt="img"
src="https://github.com/user-attachments/assets/eb3bbd27-4cf8-42ce-939c-fc012a93efa0"
/>
2025-09-28 15:40:06 +08:00
6dfb0c245c Fix: The dataset uses the new id to obtain the knowledge graph #10333 (#10339)
### What problem does this PR solve?

Fix: The dataset uses the new id to obtain the knowledge graph #10333

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-09-28 15:39:49 +08:00
72d1047a8f Fix: update Japanese translations in ja.ts (#10338)
### What problem does this PR solve?

This PR addresses [issue
#9962](https://github.com/infiniflow/ragflow/issues/9962).
It updates the Japanese translations in `web/src/locales/ja.ts`.  

For this contribution, the scope is intentionally limited to **Chat**
and **Knowledge Base** related UI texts, ensuring focused and
incremental improvement without affecting other modules.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-09-28 15:39:05 +08:00
bece37e6c8 Fix: floating widget match style with original one (#10317)
### What problem does this PR solve?

These changes are intended to implement the remaining functionalities of
the fullscreen widget.

The question arises: how to display document prieview of PDFs in this
floating widget?
- simply enlarge the widget window
- implement zoom in/out
- render outside the iframe?

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-09-28 13:58:10 +08:00
59cb0eb8bc fix: remove ibm-db dependency and refactor import order (#10330)
### What problem does this PR solve?
issue: 
#10326
change:
 remove ibm-db dependency and refactor import order

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-09-28 12:19:32 +08:00
fc56217eb3 Fix: The enterprise version of the knowledge graph cannot be displayed. #10333 (#10334)
### What problem does this PR solve?
Fix: The enterprise version of the knowledge graph cannot be displayed.
#10333
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-28 12:18:58 +08:00
723cf9443e Fix:After setting user's is_active to 0, the user can still log in to RAGFlow. (#10325)
### What problem does this PR solve?

https://github.com/infiniflow/ragflow/issues/10293

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-09-28 12:18:01 +08:00
12 changed files with 517 additions and 162 deletions

View File

@ -21,7 +21,6 @@ import pandas as pd
import pymysql
import psycopg2
import pyodbc
import ibm_db
from agent.tools.base import ToolParamBase, ToolBase, ToolMeta
from api.utils.api_utils import timeout
@ -125,6 +124,7 @@ class ExeSQL(ToolBase, ABC):
)
db = pyodbc.connect(conn_str)
elif self._param.db_type == 'IBM DB2':
import ibm_db
conn_str = (
f"DATABASE={self._param.database};"
f"HOSTNAME={self._param.host};"
@ -162,6 +162,8 @@ class ExeSQL(ToolBase, ABC):
if pd.api.types.is_datetime64_any_dtype(df[col]):
df[col] = df[col].dt.strftime("%Y-%m-%d")
df = df.where(pd.notnull(df), None)
sql_res.append(convert_decimals(df.to_dict(orient="records")))
formalized_content.append(df.to_markdown(index=False, floatfmt=".6f"))
@ -197,6 +199,8 @@ class ExeSQL(ToolBase, ABC):
if pd.api.types.is_datetime64_any_dtype(single_res[col]):
single_res[col] = single_res[col].dt.strftime('%Y-%m-%d')
single_res = single_res.where(pd.notnull(single_res), None)
sql_res.append(convert_decimals(single_res.to_dict(orient='records')))
formalized_content.append(single_res.to_markdown(index=False, floatfmt=".6f"))

View File

@ -98,6 +98,15 @@ def login():
return get_json_result(data=False, code=settings.RetCode.SERVER_ERROR, message="Fail to crypt password")
user = UserService.query_user(email, password)
if user and hasattr(user, 'is_active') and user.is_active == "0":
return get_json_result(
data=False,
code=settings.RetCode.FORBIDDEN,
message="This account has been disabled, please contact the administrator!",
)
if user:
response_data = user.to_json()
user.access_token = get_uuid()

View File

@ -132,7 +132,6 @@ dependencies = [
"litellm>=1.74.15.post1",
"flask-mail>=0.10.0",
"lark>=1.2.2",
"ibm-db>=3.2.7",
]
[project.optional-dependencies]

28
uv.lock generated
View File

@ -2504,32 +2504,6 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/34/87/7940713f929d0280cff1bde207479cb588a0d3a4dd49a0e2e69bfff46363/hyppo-0.4.0-py3-none-any.whl", hash = "sha256:4e75565b8deb601485cd7bc1b5c3f44e6ddf329136fc81e65d011f9b4e95132f" },
]
[[package]]
name = "ibm-db"
version = "3.2.7"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/dd/a5/182413f7fe28ee7a67d8be5afa1139ccc60cfb5c13c0e9be81e2205bddbb/ibm_db-3.2.7.tar.gz", hash = "sha256:b3c3b4550364a43bf1daa4519b668e6e00e7c3935291f8c444c4ec989417e861" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/50/cd/a6549ed35424875f07ea89dbd44093b7d9dc4a03d9e29e56c5167bd7d568/ibm_db-3.2.7-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:7c2b451ffe67be602e93d94b2d2042dd051ec0757cfd6e4d7344cb594f2d3508" },
{ url = "https://mirrors.aliyun.com/pypi/packages/50/5a/be83503ec6ef9b2a47175b38b1099595a8d06237ac3fcc82d967b834672a/ibm_db-3.2.7-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:9a1b139a9c21ff7216aac83ba29dceb6c8a9df3d6aee44ff1fe845cb60d3caed" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cc/fa/379405785d27d10110992ee17f150c8ef1ee0c3eadca2d1451c8c03ff075/ibm_db-3.2.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5e60297b4680cc566caa67f513aa68883ef48b0c612028a38883620807b09c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/52/e1/c18aee07888666b3249b4f54d3cb67ae5041600b5fc3ed281817e868eaa8/ibm_db-3.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf1c30e67e9e573e33524c393a1426e0dffa2da34ba42a0ec510e0f75766976f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/26/01/f80a407192351aa304206504d6b15ccbb061f45dc9fc3e37c8c00c7e222b/ibm_db-3.2.7-cp310-cp310-win32.whl", hash = "sha256:171014c2caa0419055943ff3badae5118cc3a191360f03b80c8366ef374d5c28" },
{ url = "https://mirrors.aliyun.com/pypi/packages/98/1b/29f98a0d4d9896635d7b7fa53a51f8753214f0deed23ac7987d726299b12/ibm_db-3.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:3425c158a65dd43e4b09dc968c18042a656ed6ef2e1db0164f032e97681823b7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c8/04/18e42549f498569db7a437453db51ae3d61105ad4da7b1fe64e9e59aedee/ibm_db-3.2.7-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:ba493d07e1845b8b1169ad27ace92f0ff540cc9a623f2753b8c68dc66c59d7df" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c5/f3/85c8963ee8047183c435059abaf40771d40c6e9268ca32d66be0b66b7a6c/ibm_db-3.2.7-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:abed0a7c644b9ddf2c49bf5c0938f936f0b2dffd1703c9819440021be141716e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6d/fb/8d2ff4b6bcd6b05d13af855bedc47e597430a6c3372e0ab7579659cad9bb/ibm_db-3.2.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cabd3d3e8c879ef60d91e1fe1356cf8603f8b4b69cc7dda39d4a8698a055044" },
{ url = "https://mirrors.aliyun.com/pypi/packages/77/26/c43e02bb4cf62b1e93bf617440f5da6d3888935ef09d4fbd86fe07f3f920/ibm_db-3.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aab5dceec45d69b0bbd333be66597dbaedf663c6c56a0fbd6196ecd1836e4095" },
{ url = "https://mirrors.aliyun.com/pypi/packages/05/d0/787d7a9864d3782238ca3b14a4484c642931e1363a760b0828c9ee26ad58/ibm_db-3.2.7-cp311-cp311-win32.whl", hash = "sha256:16272ad07912051d9ab5cbe3a9e2d3d888365d071334f9620d8e0b2ed69ee4f9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1e/7c/998c663d0a65984c76c2c2c8d01cdbdd174bd817359e0e4024b9b316a9cc/ibm_db-3.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:4b479e92b6954ab7f65c9d247a65fb0cde6a48899f71a8881b58023c0ace1f49" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4d/0f/048679ca8516b73f3547c64f968b9654181d79f6cd2706914f37c2486da3/ibm_db-3.2.7-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:24e8a538475997f20569f221247808507b63349df51119fe9b2f8e48a0bf6f9b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f0/36/4cc64c70ebc74b2765005cd5357df18512c358cc6f5e87c6b0e70cff4053/ibm_db-3.2.7-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:24a53fb8e3c200bf2a55095f1ae4c065f2136f8be87ca1db89a874bd82d88ea5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/09/d7/ae63f1257736c5e6a06cd2be133b4bc38f68893504f046b4c850144b65cd/ibm_db-3.2.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91f68be7bd0d2940023da43d0a94f196fe267ca825df7874b8174583c8678ea0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d9/24/af465d93f549b0dbc944f256cdb2b470574018285e1478b6c50305a609ac/ibm_db-3.2.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d39fe5001c078f2b824d1805ca9737060203a00a9dd9a8fe4b6f6b32b271cb5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/48/04/c512eed2c701e8762e90f000fd1a768dd749ffb070a62b8cd92722c16327/ibm_db-3.2.7-cp312-cp312-win32.whl", hash = "sha256:20388753f52050e07e845b74146dbbe3f892dcfdfb015638e8f57c2fb2e056b8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/07/cc/ae978e6d020f3b17f2e68cc2c60fe8381fffd1271608e871b5b4ee6434f2/ibm_db-3.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:6e507ddf93b8406b0b88ff6bf07658a3100ce98cb1e735e5ec8e0a56e30ea856" },
]
[[package]]
name = "idna"
version = "3.10"
@ -5350,7 +5324,6 @@ dependencies = [
{ name = "html-text" },
{ name = "httpx", extra = ["socks"] },
{ name = "huggingface-hub" },
{ name = "ibm-db" },
{ name = "infinity-emb" },
{ name = "infinity-sdk" },
{ name = "itsdangerous" },
@ -5507,7 +5480,6 @@ requires-dist = [
{ name = "html-text", specifier = "==0.6.2" },
{ name = "httpx", extras = ["socks"], specifier = "==0.27.2" },
{ name = "huggingface-hub", specifier = ">=0.25.0,<0.26.0" },
{ name = "ibm-db", specifier = ">=3.2.7" },
{ name = "infinity-emb", specifier = ">=0.0.66,<0.0.67" },
{ name = "infinity-sdk", specifier = "==0.6.0.dev5" },
{ name = "itsdangerous", specifier = "==2.1.2" },

View File

@ -0,0 +1,58 @@
/* floating-chat-widget-markdown.less */
.widget-citation-popover {
max-width: 90vw;
/* Use viewport width for better responsiveness */
width: max-content;
.ant-popover-inner {
max-height: 400px;
overflow-y: auto;
}
.ant-popover-inner-content {
padding: 12px;
}
}
/* Responsive breakpoints for popover width */
@media (min-width: 480px) {
.widget-citation-popover {
max-width: 360px;
}
}
.widget-citation-content {
p,
div,
span,
button {
word-break: break-word;
overflow-wrap: break-word;
white-space: normal;
}
}
.floating-chat-widget {
/* General styles for markdown content within the widget */
p,
div,
ul,
ol,
blockquote {
line-height: 1.6;
}
/* Enhanced image styles */
img,
.ant-image,
.ant-image-img {
max-width: 100% !important;
height: auto !important;
border-radius: 8px;
margin: 8px 0 !important;
display: inline-block !important;
}
}

View File

@ -0,0 +1,199 @@
import Image from '@/components/image';
import SvgIcon from '@/components/svg-icon';
import { useFetchDocumentThumbnailsByIds, useGetDocumentUrl } from '@/hooks/document-hooks';
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
import { preprocessLaTeX, replaceThinkToSection, showImage } from '@/utils/chat';
import { getExtension } from '@/utils/document-util';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Button, Flex, Popover, Tooltip } from 'antd';
import classNames from 'classnames';
import DOMPurify from 'dompurify';
import { omit } from 'lodash';
import { pipe } from 'lodash/fp';
import 'katex/dist/katex.min.css';
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Markdown from 'react-markdown';
import reactStringReplace from 'react-string-replace';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import rehypeKatex from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { visitParents } from 'unist-util-visit-parents';
import { currentReg, replaceTextByOldReg } from '../pages/next-chats/utils';
import styles from './floating-chat-widget-markdown.less';
import { useIsDarkTheme } from './theme-provider';
const getChunkIndex = (match: string) => Number(match.replace(/\[|\]/g, ''));
const FloatingChatWidgetMarkdown = ({
reference,
clickDocumentButton,
content,
}: {
content: string;
loading: boolean;
reference: IReference;
clickDocumentButton?: (documentId: string, chunk: IReferenceChunk) => void;
}) => {
const { t } = useTranslation();
const { setDocumentIds, data: fileThumbnails } = useFetchDocumentThumbnailsByIds();
const getDocumentUrl = useGetDocumentUrl();
const isDarkTheme = useIsDarkTheme();
const contentWithCursor = useMemo(() => {
let text = content === '' ? t('chat.searching') : content;
const nextText = replaceTextByOldReg(text);
return pipe(replaceThinkToSection, preprocessLaTeX)(nextText);
}, [content, t]);
useEffect(() => {
const docAggs = reference?.doc_aggs;
const docList = Array.isArray(docAggs) ? docAggs : Object.values(docAggs ?? {});
setDocumentIds(docList.map((x: any) => x.doc_id).filter(Boolean));
}, [reference, setDocumentIds]);
const handleDocumentButtonClick = useCallback((documentId: string, chunk: IReferenceChunk, isPdf: boolean, documentUrl?: string) => () => {
if (!documentId) return;
if (!isPdf && documentUrl) {
window.open(documentUrl, '_blank');
} else if (clickDocumentButton) {
clickDocumentButton(documentId, chunk);
}
}, [clickDocumentButton]);
const rehypeWrapReference = () => (tree: any) => {
visitParents(tree, 'text', (node, ancestors) => {
const latestAncestor = ancestors[ancestors.length - 1];
if (latestAncestor.tagName !== 'custom-typography' && latestAncestor.tagName !== 'code') {
node.type = 'element';
node.tagName = 'custom-typography';
node.properties = {};
node.children = [{ type: 'text', value: node.value }];
}
});
};
const getReferenceInfo = useCallback((chunkIndex: number) => {
const chunkItem = reference?.chunks?.[chunkIndex];
if (!chunkItem) return null;
const docAggsArray = Array.isArray(reference?.doc_aggs) ? reference.doc_aggs : Object.values(reference?.doc_aggs ?? {});
const document = docAggsArray.find((x: any) => x?.doc_id === chunkItem?.document_id) as any;
const documentId = document?.doc_id;
const documentUrl = document?.url ?? (documentId ? getDocumentUrl(documentId) : undefined);
const fileThumbnail = documentId ? fileThumbnails[documentId] : '';
const fileExtension = documentId ? getExtension(document?.doc_name ?? '') : '';
return { documentUrl, fileThumbnail, fileExtension, imageId: chunkItem.image_id, chunkItem, documentId, document };
}, [fileThumbnails, reference, getDocumentUrl]);
const getPopoverContent = useCallback((chunkIndex: number) => {
const info = getReferenceInfo(chunkIndex);
if (!info) {
return <div className="p-2 text-xs text-red-500">Error: Missing document information.</div>;
}
const { documentUrl, fileThumbnail, fileExtension, imageId, chunkItem, documentId, document } = info;
return (
<div key={`popover-content-${chunkItem.id}`} className="flex gap-2 widget-citation-content">
{imageId && (
<Popover placement="left" content={<Image id={imageId} className="max-w-[80vw] max-h-[60vh] rounded" />}>
<Image id={imageId} className="w-24 h-24 object-contain rounded m-1 cursor-pointer" />
</Popover>
)}
<div className="space-y-2 flex-1 min-w-0">
<div
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(chunkItem?.content ?? '') }}
className="max-h-[250px] overflow-y-auto text-xs leading-relaxed p-2 bg-gray-50 dark:bg-gray-800 rounded prose-sm"
></div>
{documentId && (
<Flex gap={'small'} align="center">
{fileThumbnail ? (
<img src={fileThumbnail} alt={document?.doc_name} className="w-6 h-6 rounded" />
) : (
<SvgIcon name={`file-icon/${fileExtension}`} width={20} />
)}
<Tooltip title={!documentUrl && fileExtension !== 'pdf' ? 'Document link unavailable' : document.doc_name}>
<Button
type="link"
size="small"
className="p-0 text-xs break-words h-auto text-left flex-1"
onClick={handleDocumentButtonClick(documentId, chunkItem, fileExtension === 'pdf', documentUrl)}
disabled={!documentUrl && fileExtension !== 'pdf'}
style={{ whiteSpace: 'normal' }}
>
<span className="truncate">{document?.doc_name ?? 'Unnamed Document'}</span>
</Button>
</Tooltip>
</Flex>
)}
</div>
</div>
);
}, [getReferenceInfo, handleDocumentButtonClick]);
const renderReference = useCallback((text: string) => {
return reactStringReplace(text, currentReg, (match, i) => {
const chunkIndex = getChunkIndex(match);
const info = getReferenceInfo(chunkIndex);
if (!info) {
return <Tooltip key={`err-tooltip-${i}`} title="Reference unavailable"><InfoCircleOutlined className={styles.referenceIcon} /></Tooltip>;
}
const { imageId, chunkItem, documentId, fileExtension, documentUrl } = info;
if (showImage(chunkItem?.doc_type)) {
return <Image key={`img-${i}`} id={imageId} className="block object-contain max-w-full max-h-48 rounded my-2 cursor-pointer" onClick={handleDocumentButtonClick(documentId, chunkItem, fileExtension === 'pdf', documentUrl)} />;
}
return (
<Popover
content={getPopoverContent(chunkIndex)}
key={`popover-${i}`}
>
<InfoCircleOutlined className={styles.referenceIcon} />
</Popover>
);
});
}, [getPopoverContent, getReferenceInfo, handleDocumentButtonClick]);
return (
<div className="floating-chat-widget">
<Markdown
rehypePlugins={[rehypeWrapReference, rehypeKatex, rehypeRaw]}
remarkPlugins={[remarkGfm, remarkMath]}
className="text-sm leading-relaxed space-y-2 prose-sm max-w-full"
components={{
'custom-typography': ({ children }: { children: string }) => renderReference(children),
code(props: any) {
const { children, className, node, ...rest } = props;
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter
{...omit(rest, 'inline')}
PreTag="div"
language={match[1]}
style={isDarkTheme ? oneDark : oneLight}
wrapLongLines
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code {...rest} className={classNames(className, 'text-wrap text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded')}>
{children}
</code>
);
},
} as any}
>
{contentWithCursor}
</Markdown>
</div>
);
};
export default FloatingChatWidgetMarkdown;

View File

@ -2,8 +2,10 @@ import { MessageType, SharedFrom } from '@/constants/chat';
import { useFetchNextConversationSSE } from '@/hooks/chat-hooks';
import { useFetchFlowSSE } from '@/hooks/flow-hooks';
import { useFetchExternalChatInfo } from '@/hooks/use-chat-request';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import i18n from '@/locales/config';
import { MessageCircle, Minimize2, Send, X } from 'lucide-react';
import PdfDrawer from '@/components/pdf-drawer';
import React, {
useCallback,
useEffect,
@ -15,6 +17,7 @@ import {
useGetSharedChatSearchParams,
useSendSharedMessage,
} from '../pages/next-chats/hooks/use-send-shared-message';
import FloatingChatWidgetMarkdown from './floating-chat-widget-markdown';
const FloatingChatWidget = () => {
const [isOpen, setIsOpen] = useState(false);
@ -63,6 +66,14 @@ const FloatingChatWidget = () => {
const { data: avatarData } = useFetchAvatar();
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
// PDF drawer state tracking
useEffect(() => {
// Drawer state management
}, [visible, documentId, selectedChunk]);
// Play sound when opening
const playNotificationSound = useCallback(() => {
try {
@ -223,7 +234,7 @@ const FloatingChatWidget = () => {
const syntheticEvent = {
target: { value: inputValue },
currentTarget: { value: inputValue },
preventDefault: () => {},
preventDefault: () => { },
} as any;
handleInputChange(syntheticEvent);
@ -314,9 +325,8 @@ const FloatingChatWidget = () => {
'*',
);
}}
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
}`}
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
}`}
>
<div
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
@ -343,9 +353,8 @@ const FloatingChatWidget = () => {
>
<button
onClick={toggleChat}
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
}`}
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
}`}
>
<div
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
@ -367,127 +376,143 @@ const FloatingChatWidget = () => {
if (mode === 'window') {
// Only render the chat window (always open)
return (
<div
className={`fixed top-0 left-0 z-50 bg-blue-600 rounded-2xl transition-all duration-300 ease-out h-[500px] w-[380px] overflow-hidden ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
>
{/* Header */}
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-t-2xl">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
<MessageCircle size={18} />
</div>
<div>
<h3 className="font-semibold text-sm">
{chatInfo?.title || 'Chat Support'}
</h3>
<p className="text-xs text-blue-100">
We typically reply instantly
</p>
<>
<div
className={`fixed top-0 left-0 z-50 bg-blue-600 rounded-2xl transition-all duration-300 ease-out h-[500px] w-[380px] overflow-hidden ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
>
{/* Header */}
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-t-2xl">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
<MessageCircle size={18} />
</div>
<div>
<h3 className="font-semibold text-sm">
{chatInfo?.title || 'Chat Support'}
</h3>
<p className="text-xs text-blue-100">
We typically reply instantly
</p>
</div>
</div>
</div>
</div>
{/* Messages and Input */}
<div
className="flex flex-col h-[436px] bg-white"
style={{ borderRadius: '0 0 16px 16px' }}
>
{/* Messages and Input */}
<div
className="flex-1 overflow-y-auto p-4 space-y-4"
onWheel={(e) => {
const element = e.currentTarget;
const isAtTop = element.scrollTop === 0;
const isAtBottom =
element.scrollTop + element.clientHeight >=
element.scrollHeight - 1;
// Allow scroll to pass through to parent when at boundaries
if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) {
e.preventDefault();
// Let the parent handle the scroll
window.parent.postMessage(
{
type: 'SCROLL_PASSTHROUGH',
deltaY: e.deltaY,
},
'*',
);
}
}}
className="flex flex-col h-[436px] bg-white"
style={{ borderRadius: '0 0 16px 16px' }}
>
{displayMessages?.map((message, index) => (
<div
key={index}
className={`flex ${message.role === MessageType.User ? 'justify-end' : 'justify-start'}`}
>
<div
className="flex-1 overflow-y-auto p-4 space-y-4"
onWheel={(e) => {
const element = e.currentTarget;
const isAtTop = element.scrollTop === 0;
const isAtBottom =
element.scrollTop + element.clientHeight >=
element.scrollHeight - 1;
// Allow scroll to pass through to parent when at boundaries
if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) {
e.preventDefault();
// Let the parent handle the scroll
window.parent.postMessage(
{
type: 'SCROLL_PASSTHROUGH',
deltaY: e.deltaY,
},
'*',
);
}
}}
>
{displayMessages?.map((message, index) => (
<div
className={`max-w-[280px] px-4 py-2 rounded-2xl ${
message.role === MessageType.User
key={index}
className={`flex ${message.role === MessageType.User ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[280px] px-4 py-2 rounded-2xl ${message.role === MessageType.User
? 'bg-blue-600 text-white rounded-br-md'
: 'bg-gray-100 text-gray-800 rounded-bl-md'
}`}
}`}
>
{message.role === MessageType.User ? (
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{message.content}
</p>
) : (
<FloatingChatWidgetMarkdown
loading={false}
content={message.content}
reference={message.reference || { doc_aggs: [], chunks: [], total: 0 }}
clickDocumentButton={clickDocumentButton}
/>
)}
</div>
</div>
))}
{/* Clean Typing Indicator */}
{sendLoading && !enableStreaming && (
<div className="flex justify-start pl-4">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: '0.1s' }}
></div>
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: '0.2s' }}
></div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="border-t border-gray-200 p-4">
<div className="flex items-end space-x-3">
<div className="flex-1">
<textarea
value={inputValue}
onChange={(e) => {
const newValue = e.target.value;
setInputValue(newValue);
handleInputChange(e);
}}
onKeyPress={handleKeyPress}
placeholder="Type your message..."
rows={1}
className="w-full resize-none border border-gray-300 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
style={{ minHeight: '44px', maxHeight: '120px' }}
disabled={hasError || sendLoading}
/>
</div>
<button
onClick={handleSendMessage}
disabled={!inputValue.trim() || sendLoading}
className="p-3 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{message.content}
</p>
</div>
<Send size={18} />
</button>
</div>
))}
{/* Clean Typing Indicator */}
{sendLoading && !enableStreaming && (
<div className="flex justify-start pl-4">
<div className="flex space-x-1">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: '0.1s' }}
></div>
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: '0.2s' }}
></div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="border-t border-gray-200 p-4">
<div className="flex items-end space-x-3">
<div className="flex-1">
<textarea
value={inputValue}
onChange={(e) => {
const newValue = e.target.value;
setInputValue(newValue);
handleInputChange(e);
}}
onKeyPress={handleKeyPress}
placeholder="Type your message..."
rows={1}
className="w-full resize-none border border-gray-300 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
style={{ minHeight: '44px', maxHeight: '120px' }}
disabled={hasError || sendLoading}
/>
</div>
<button
onClick={handleSendMessage}
disabled={!inputValue.trim() || sendLoading}
className="p-3 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Send size={18} />
</button>
</div>
</div>
</div>
</div>
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
width={'100vw'}
height={'100vh'}
/>
</>
);
}
// Full mode - render everything together (original behavior)
} // Full mode - render everything together (original behavior)
return (
<div
className={`transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
@ -495,9 +520,8 @@ const FloatingChatWidget = () => {
{/* Chat Widget Container */}
{isOpen && (
<div
className={`fixed bottom-24 right-6 z-50 bg-blue-600 rounded-2xl transition-all duration-300 ease-out ${
isMinimized ? 'h-16' : 'h-[500px]'
} w-[380px] overflow-hidden`}
className={`fixed bottom-24 right-6 z-50 bg-blue-600 rounded-2xl transition-all duration-300 ease-out ${isMinimized ? 'h-16' : 'h-[500px]'
} w-[380px] overflow-hidden`}
>
{/* Header */}
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-t-2xl">
@ -568,15 +592,23 @@ const FloatingChatWidget = () => {
className={`flex ${message.role === MessageType.User ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[280px] px-4 py-2 rounded-2xl ${
message.role === MessageType.User
? 'bg-blue-600 text-white rounded-br-md'
: 'bg-gray-100 text-gray-800 rounded-bl-md'
}`}
className={`max-w-[280px] px-4 py-2 rounded-2xl ${message.role === MessageType.User
? 'bg-blue-600 text-white rounded-br-md'
: 'bg-gray-100 text-gray-800 rounded-bl-md'
}`}
>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{message.content}
</p>
{message.role === MessageType.User ? (
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{message.content}
</p>
) : (
<FloatingChatWidgetMarkdown
loading={false}
content={message.content}
reference={message.reference || { doc_aggs: [], chunks: [], total: 0 }}
clickDocumentButton={clickDocumentButton}
/>
)}
</div>
</div>
))}
@ -641,9 +673,8 @@ const FloatingChatWidget = () => {
<div className="fixed bottom-6 right-6 z-50">
<button
onClick={toggleChat}
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
}`}
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
}`}
>
<div
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
@ -659,6 +690,12 @@ const FloatingChatWidget = () => {
</div>
)}
</div>
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
/>
</div>
);
};

View File

@ -7,6 +7,8 @@ import DocumentPreviewer from '../pdf-previewer';
interface IProps extends IModalProps<any> {
documentId: string;
chunk: IChunk | IReferenceChunk;
width?: string | number;
height?: string | number;
}
export const PdfDrawer = ({
@ -14,13 +16,16 @@ export const PdfDrawer = ({
hideModal,
documentId,
chunk,
width = '50vw',
height,
}: IProps) => {
return (
<Drawer
title="Document Previewer"
onClose={hideModal}
open={visible}
width={'50vw'}
width={width}
height={height}
>
<DocumentPreviewer
documentId={documentId}
@ -31,4 +36,4 @@ export const PdfDrawer = ({
);
};
export default PdfDrawer;
export default PdfDrawer;

View File

@ -9,6 +9,7 @@ import {
import { ITestRetrievalRequestBody } from '@/interfaces/request/knowledge';
import i18n from '@/locales/config';
import kbService, {
deleteKnowledgeGraph,
getKnowledgeGraph,
listDataset,
} from '@/services/knowledge-service';
@ -30,6 +31,7 @@ export const enum KnowledgeApiAction {
FetchKnowledgeDetail = 'fetchKnowledgeDetail',
FetchKnowledgeGraph = 'fetchKnowledgeGraph',
FetchMetadata = 'fetchMetadata',
RemoveKnowledgeGraph = 'removeKnowledgeGraph',
}
export const useKnowledgeBaseId = (): string => {
@ -296,3 +298,28 @@ export function useFetchKnowledgeMetadata(kbIds: string[] = []) {
return { data, loading };
}
export const useRemoveKnowledgeGraph = () => {
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [KnowledgeApiAction.RemoveKnowledgeGraph],
mutationFn: async () => {
const { data } = await deleteKnowledgeGraph(knowledgeBaseId);
if (data.code === 0) {
message.success(i18n.t(`message.deleted`));
queryClient.invalidateQueries({
queryKey: ['fetchKnowledgeGraph'],
});
}
return data?.code;
},
});
return { data, loading, removeKnowledgeGraph: mutateAsync };
};

View File

@ -1,6 +1,7 @@
export default {
translation: {
common: {
selectPlaceholder: '選択してください',
delete: '削除',
deleteModalTitle: 'この項目を削除してよろしいですか?',
ok: 'はい',
@ -174,6 +175,7 @@ export default {
'ナレッジベースの設定、特にチャンク方法をここで更新してください。',
name: 'ナレッジベース名',
photo: 'ナレッジベース写真',
photoTip: '4MBのファイルをアップロードできます',
description: '説明',
language: '言語',
languageMessage: '言語を入力してください',
@ -333,6 +335,11 @@ export default {
},
chat: {
messagePlaceholder: 'メッセージを入力してください...',
exit: '終了',
multipleModels: '複数モデル',
applyModelConfigs: 'モデル設定を適用',
conversations: '会話一覧',
chatApps: 'チャットアプリ',
newConversation: '新しい会話',
createAssistant: 'アシスタントを作成',
assistantSetting: 'アシスタント設定',
@ -352,6 +359,7 @@ export default {
language: '言語',
emptyResponse: '空の応答',
emptyResponseTip: `ナレッジベースに該当する内容がない場合、この応答が使用されます。`,
emptyResponseMessage: `ナレッジベースから関連する情報が取得できなかった場合に発動します。ナレッジベースが選択されていない場合、このフィールドをクリアしてください。`,
setAnOpener: 'オープニングメッセージを設定',
setAnOpenerInitial: `こんにちは! 私はあなたのアシスタントです。何をお手伝いしましょうか?`,
setAnOpenerTip: 'お客様をどのように歓迎しますか?',
@ -378,10 +386,14 @@ export default {
model: 'モデル',
modelTip: '大規模言語チャットモデル',
modelMessage: '選択してください!',
modelEnabledTools: '有効化されたツール',
modelEnabledToolsTip:
'モデルで使用するツールを1つ以上選択してください。ツール呼び出しをサポートしていないモデルでは効果がありません。',
freedom: '自由度',
improvise: '自由に',
precise: '正確に',
balance: 'バランス',
custom: 'カスタム',
freedomTip: `'正確に'は、LLMが慎重に質問に答えることを意味します。'自由に'は、LLMが多く話し、自由に答えることを望むことを意味します。'バランス'は、慎重さと自由さの間のバランスを取ることを意味します。`,
temperature: '温度',
temperatureMessage: '温度は必須です',
@ -435,6 +447,7 @@ export default {
partialTitle: '部分埋め込み',
extensionTitle: 'Chrome拡張機能',
tokenError: 'まずAPIトークンを作成してください',
betaError: 'システム設定ページからRAGFlow APIキーを取得してください。',
searching: '検索中...',
parsing: '解析中',
uploading: 'アップロード中',
@ -451,6 +464,38 @@ export default {
'マルチラウンドの会話では、ナレッジベースへのクエリが最適化されます。大規模モデルが呼び出され、追加のトークンが消費されます。',
howUseId: 'チャットIDの使い方',
description: 'アシスタントの説明',
descriptionPlaceholder: '例: 履歴書用のチャットアシスタント',
useKnowledgeGraph: 'ナレッジグラフを使用',
useKnowledgeGraphTip:
'ナレッジグラフを利用してエンティティや関係をまたいだ検索を行います。マルチホップ質問応答が可能になりますが、検索時間が大幅に増加します。',
keyword: 'キーワード解析',
keywordTip: `LLMでユーザーの質問を解析し、重要キーワードを抽出して関連性計算で強調します。長文クエリに有効ですが応答速度が遅くなります。`,
languageTip:
'選択した言語で文をリライトできます。未選択の場合は直近の質問の言語が使われます。',
avatarHidden: 'アバターを非表示',
locale: 'ロケール',
selectLanguage: '言語を選択',
reasoning: '推論',
reasoningTip: `Deepseek-R1 や OpenAI o1 のように、推論ワークフローを有効にできます。ステップごとの論理展開で複雑な質問への正確さが向上します。`,
tavilyApiKeyTip:
'ここにTavilyのAPIキーを設定すると、ナレッジベース検索に加えてウェブ検索も利用できます。',
tavilyApiKeyMessage: 'Tavily APIキーを入力してください',
tavilyApiKeyHelp: '取得方法はこちら',
crossLanguage: 'クロス言語検索',
crossLanguageTip: `1つ以上の言語を選択すると、その言語でも検索します。未選択の場合は元の言語で検索します。`,
createChat: 'チャットを作成',
metadata: 'メタデータ',
metadataTip:
'メタデータ(タグ、カテゴリ、アクセス権限など)を使って、関連情報の検索を制御・絞り込みます。',
conditions: '条件',
addCondition: '条件を追加',
meta: {
disabled: '無効',
automatic: '自動',
manual: '手動',
},
cancel: 'キャンセル',
chatSetting: 'チャット設定',
},
setting: {
profile: 'プロファイル',

View File

@ -1,6 +1,6 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { Button } from '@/components/ui/button';
import { useFetchKnowledgeGraph } from '@/hooks/knowledge-hooks';
import { useFetchKnowledgeGraph } from '@/hooks/use-knowledge-request';
import { Trash2 } from 'lucide-react';
import React from 'react';
import { useTranslation } from 'react-i18next';

View File

@ -1,5 +1,5 @@
import { useRemoveKnowledgeGraph } from '@/hooks/knowledge-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useRemoveKnowledgeGraph } from '@/hooks/use-knowledge-request';
import { useCallback } from 'react';
import { useParams } from 'umi';