Compare commits

...

4 Commits

Author SHA1 Message Date
5b5f19cbc1 Fix: Newly added models to OpenAI-API-Compatible are not displayed in the LLM dropdown menu in a timely manner. #11774 (#11775)
### What problem does this PR solve?

Fix: Newly added models to OpenAI-API-Compatible are not displayed in
the LLM dropdown menu in a timely manner. #11774

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-12-05 18:04:49 +08:00
ea38e12d42 Feat: Users can chat directly without first creating a conversation. #11768 (#11769)
### What problem does this PR solve?

Feat: Users can chat directly without first creating a conversation.
#11768
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-12-05 17:34:41 +08:00
885eb2eab9 Add ut test into CI (#11753)
### What problem does this PR solve?

As title

### Type of change

- [x] Other (please describe):

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
2025-12-05 11:40:16 +08:00
6587acef88 Feat: use filepath for files with the same name (#11752)
### What problem does this PR solve?

When there are multiple files with the same name the file would just
duplicate, making it hard to distinguish between the different files.
Now if there are multiple files with the same name, they will be named
after their folder path in the webdav storage unit.

The same could be done for the other connectors, too, since most of them
will have similars issues, when iterating through the folder paths.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Contribution by RAGcon GmbH, visit us [here](https://www.ragcon.ai/)
2025-12-05 10:10:26 +08:00
26 changed files with 955 additions and 349 deletions

View File

@ -127,6 +127,14 @@ jobs:
fi
fi
- name: Run unit test
run: |
uv sync --python 3.10 --group test --frozen
source .venv/bin/activate
which pytest || echo "pytest not in PATH"
echo "Start to run unit test"
python3 run_tests.py
- name: Build ragflow:nightly
run: |
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-${HOME}}

View File

@ -190,6 +190,11 @@ class WebDAVConnector(LoadConnector, PollConnector):
files = self._list_files_recursive(self.remote_path, start, end)
logging.info(f"Found {len(files)} files matching time criteria")
filename_counts: dict[str, int] = {}
for file_path, _ in files:
file_name = os.path.basename(file_path)
filename_counts[file_name] = filename_counts.get(file_name, 0) + 1
batch: list[Document] = []
for file_path, file_info in files:
file_name = os.path.basename(file_path)
@ -237,12 +242,22 @@ class WebDAVConnector(LoadConnector, PollConnector):
else:
modified = datetime.now(timezone.utc)
if filename_counts.get(file_name, 0) > 1:
relative_path = file_path
if file_path.startswith(self.remote_path):
relative_path = file_path[len(self.remote_path):]
if relative_path.startswith('/'):
relative_path = relative_path[1:]
semantic_id = relative_path.replace('/', ' / ') if relative_path else file_name
else:
semantic_id = file_name
batch.append(
Document(
id=f"webdav:{self.base_url}:{file_path}",
blob=blob,
source=DocumentSource.WEBDAV,
semantic_identifier=file_name,
semantic_identifier=semantic_id,
extension=get_file_ext(file_name),
doc_updated_at=modified,
size_bytes=size_bytes if size_bytes else 0

View File

@ -131,7 +131,6 @@ dependencies = [
"graspologic @ git+https://github.com/yuzhichang/graspologic.git@38e680cab72bc9fb68a7992c3bcc2d53b24e42fd",
"mini-racer>=0.12.4,<0.13.0",
"pyodbc>=5.2.0,<6.0.0",
"pyicu>=2.15.3,<3.0.0",
"flasgger>=0.9.7.1,<0.10.0",
"xxhash>=3.5.0,<4.0.0",
"trio>=0.17.0,<0.29.0",
@ -163,6 +162,9 @@ test = [
"openpyxl>=3.1.5",
"pillow>=10.4.0",
"pytest>=8.3.5",
"pytest-asyncio>=1.3.0",
"pytest-xdist>=3.8.0",
"pytest-cov>=7.0.0",
"python-docx>=1.1.2",
"python-pptx>=1.0.2",
"reportlab>=4.4.1",
@ -195,8 +197,83 @@ extend-select = ["ASYNC", "ASYNC1"]
ignore = ["E402"]
[tool.pytest.ini_options]
pythonpath = [
"."
]
testpaths = ["test"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"p1: high priority test cases",
"p2: medium priority test cases",
"p3: low priority test cases",
]
# Test collection and runtime configuration
filterwarnings = [
"error", # Treat warnings as errors
"ignore::DeprecationWarning", # Ignore specific warnings
]
# Command line options
addopts = [
"-v", # Verbose output
"--strict-markers", # Enforce marker definitions
"--tb=short", # Simplified traceback
"--disable-warnings", # Disable warnings
"--color=yes" # Colored output
]
# Coverage configuration
[tool.coverage.run]
# Source paths - adjust according to your project structure
source = [
# "../../api/db/services",
# Add more directories if needed:
"../../common",
# "../../utils",
]
# Files/directories to exclude
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/.pytest_cache/*",
"*/venv/*",
"*/.venv/*",
"*/env/*",
"*/site-packages/*",
"*/dist/*",
"*/build/*",
"*/migrations/*",
"setup.py"
]
[tool.coverage.report]
# Report configuration
precision = 2
show_missing = true
skip_covered = false
fail_under = 0 # Minimum coverage requirement (0-100)
# Lines to exclude (optional)
exclude_lines = [
# "pragma: no cover",
# "def __repr__",
# "raise AssertionError",
# "raise NotImplementedError",
# "if __name__ == .__main__.:",
# "if TYPE_CHECKING:",
"pass"
]
[tool.coverage.html]
# HTML report configuration
directory = "htmlcov"
title = "Test Coverage Report"
# extra_css = "custom.css" # Optional custom CSS

275
run_tests.py Executable file
View File

@ -0,0 +1,275 @@
#!/usr/bin/env python3
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
import os
import argparse
import subprocess
from pathlib import Path
from typing import List
class Colors:
"""ANSI color codes for terminal output"""
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
class TestRunner:
"""RAGFlow Unit Test Runner"""
def __init__(self):
self.project_root = Path(__file__).parent.resolve()
self.ut_dir = Path(self.project_root / 'test' / 'unit_test')
# Default options
self.coverage = False
self.parallel = False
self.verbose = False
self.markers = ""
# Python interpreter path
self.python = sys.executable
@staticmethod
def print_info(message: str) -> None:
"""Print informational message"""
print(f"{Colors.BLUE}[INFO]{Colors.NC} {message}")
@staticmethod
def print_error(message: str) -> None:
"""Print error message"""
print(f"{Colors.RED}[ERROR]{Colors.NC} {message}")
@staticmethod
def show_usage() -> None:
"""Display usage information"""
usage = """
RAGFlow Unit Test Runner
Usage: python run_tests.py [OPTIONS]
OPTIONS:
-h, --help Show this help message
-c, --coverage Run tests with coverage report
-p, --parallel Run tests in parallel (requires pytest-xdist)
-v, --verbose Verbose output
-t, --test FILE Run specific test file or directory
-m, --markers MARKERS Run tests with specific markers (e.g., "unit", "integration")
EXAMPLES:
# Run all tests
python run_tests.py
# Run with coverage
python run_tests.py --coverage
# Run in parallel
python run_tests.py --parallel
# Run specific test file
python run_tests.py --test services/test_dialog_service.py
# Run only unit tests
python run_tests.py --markers "unit"
# Run tests with coverage and parallel execution
python run_tests.py --coverage --parallel
"""
print(usage)
def build_pytest_command(self) -> List[str]:
"""Build the pytest command arguments"""
cmd = ["pytest", str(self.ut_dir)]
# Add test path
# Add markers
if self.markers:
cmd.extend(["-m", self.markers])
# Add verbose flag
if self.verbose:
cmd.extend(["-vv"])
else:
cmd.append("-v")
# Add coverage
if self.coverage:
# Relative path from test directory to source code
source_path = str(self.project_root / "common")
cmd.extend([
"--cov", source_path,
"--cov-report", "html",
"--cov-report", "term"
])
# Add parallel execution
if self.parallel:
# Try to get number of CPU cores
try:
import multiprocessing
cpu_count = multiprocessing.cpu_count()
cmd.extend(["-n", str(cpu_count)])
except ImportError:
# Fallback to auto if multiprocessing not available
cmd.extend(["-n", "auto"])
# Add default options from pyproject.toml if it exists
pyproject_path = self.project_root / "pyproject.toml"
if pyproject_path.exists():
cmd.extend(["--config-file", str(pyproject_path)])
return cmd
def run_tests(self) -> bool:
"""Execute the pytest command"""
# Change to test directory
os.chdir(self.project_root)
# Build command
cmd = self.build_pytest_command()
# Print test configuration
self.print_info("Running RAGFlow Unit Tests")
self.print_info("=" * 40)
self.print_info(f"Test Directory: {self.ut_dir}")
self.print_info(f"Coverage: {self.coverage}")
self.print_info(f"Parallel: {self.parallel}")
self.print_info(f"Verbose: {self.verbose}")
if self.markers:
self.print_info(f"Markers: {self.markers}")
print(f"\n{Colors.BLUE}[EXECUTING]{Colors.NC} {' '.join(cmd)}\n")
# Run pytest
try:
result = subprocess.run(cmd, check=False)
if result.returncode == 0:
print(f"\n{Colors.GREEN}[SUCCESS]{Colors.NC} All tests passed!")
if self.coverage:
coverage_dir = self.ut_dir / "htmlcov"
if coverage_dir.exists():
index_file = coverage_dir / "index.html"
print(f"\n{Colors.BLUE}[INFO]{Colors.NC} Coverage report generated:")
print(f" {index_file}")
print("\nOpen with:")
print(f" - Windows: start {index_file}")
print(f" - macOS: open {index_file}")
print(f" - Linux: xdg-open {index_file}")
return True
else:
print(f"\n{Colors.RED}[FAILURE]{Colors.NC} Some tests failed!")
return False
except KeyboardInterrupt:
print(f"\n{Colors.YELLOW}[INTERRUPTED]{Colors.NC} Test execution interrupted by user")
return False
except Exception as e:
self.print_error(f"Failed to execute tests: {e}")
return False
def parse_arguments(self) -> bool:
"""Parse command line arguments"""
parser = argparse.ArgumentParser(
description="RAGFlow Unit Test Runner",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python run_tests.py # Run all tests
python run_tests.py --coverage # Run with coverage
python run_tests.py --parallel # Run in parallel
python run_tests.py --test services/test_dialog_service.py # Run specific test
python run_tests.py --markers "unit" # Run only unit tests
"""
)
parser.add_argument(
"-c", "--coverage",
action="store_true",
help="Run tests with coverage report"
)
parser.add_argument(
"-p", "--parallel",
action="store_true",
help="Run tests in parallel (requires pytest-xdist)"
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Verbose output"
)
parser.add_argument(
"-t", "--test",
type=str,
default="",
help="Run specific test file or directory"
)
parser.add_argument(
"-m", "--markers",
type=str,
default="",
help="Run tests with specific markers (e.g., 'unit', 'integration')"
)
try:
args = parser.parse_args()
# Set options
self.coverage = args.coverage
self.parallel = args.parallel
self.verbose = args.verbose
self.markers = args.markers
return True
except SystemExit:
# argparse already printed help, just exit
return False
except Exception as e:
self.print_error(f"Error parsing arguments: {e}")
return False
def run(self) -> int:
"""Main execution method"""
# Parse command line arguments
if not self.parse_arguments():
return 1
# Run tests
success = self.run_tests()
return 0 if success else 1
def main():
"""Entry point"""
runner = TestRunner()
return runner.run()
if __name__ == "__main__":
sys.exit(main())

137
uv.lock generated
View File

@ -443,6 +443,15 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
]
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
]
[[package]]
name = "bce-python-sdk"
version = "0.9.55"
@ -1025,6 +1034,58 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
]
[[package]]
name = "coverage"
version = "7.12.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/4a/0dc3de1c172d35abe512332cfdcc43211b6ebce629e4cc42e6cd25ed8f4d/coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b", size = 217409, upload-time = "2025-11-18T13:31:53.122Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/c3/086198b98db0109ad4f84241e8e9ea7e5fb2db8c8ffb787162d40c26cc76/coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c", size = 217927, upload-time = "2025-11-18T13:31:54.458Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/5f/34614dbf5ce0420828fc6c6f915126a0fcb01e25d16cf141bf5361e6aea6/coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832", size = 244678, upload-time = "2025-11-18T13:31:55.805Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/7b/6b26fb32e8e4a6989ac1d40c4e132b14556131493b1d06bc0f2be169c357/coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa", size = 246507, upload-time = "2025-11-18T13:31:57.05Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/42/7d70e6603d3260199b90fb48b537ca29ac183d524a65cc31366b2e905fad/coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73", size = 248366, upload-time = "2025-11-18T13:31:58.362Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/4a/d86b837923878424c72458c5b25e899a3c5ca73e663082a915f5b3c4d749/coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb", size = 245366, upload-time = "2025-11-18T13:31:59.572Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/c2/2adec557e0aa9721875f06ced19730fdb7fc58e31b02b5aa56f2ebe4944d/coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e", size = 246408, upload-time = "2025-11-18T13:32:00.784Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/4b/8bd1f1148260df11c618e535fdccd1e5aaf646e55b50759006a4f41d8a26/coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777", size = 244416, upload-time = "2025-11-18T13:32:01.963Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/13/3a248dd6a83df90414c54a4e121fd081fb20602ca43955fbe1d60e2312a9/coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553", size = 244681, upload-time = "2025-11-18T13:32:03.408Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/30/aa833827465a5e8c938935f5d91ba055f70516941078a703740aaf1aa41f/coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d", size = 245300, upload-time = "2025-11-18T13:32:04.686Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/24/f85b3843af1370fb3739fa7571819b71243daa311289b31214fe3e8c9d68/coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef", size = 220008, upload-time = "2025-11-18T13:32:05.806Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/a2/c7da5b9566f7164db9eefa133d17761ecb2c2fde9385d754e5b5c80f710d/coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022", size = 220943, upload-time = "2025-11-18T13:32:07.166Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "cramjam"
version = "2.11.0"
@ -1512,6 +1573,15 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "execnet"
version = "2.1.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
]
[[package]]
name = "extract-msg"
version = "0.41.5"
@ -3925,12 +3995,12 @@ name = "onnxruntime-gpu"
version = "1.19.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "coloredlogs" },
{ name = "flatbuffers" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "sympy" },
{ name = "coloredlogs", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "flatbuffers", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "numpy", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "packaging", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "protobuf", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "sympy", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/9c/3fa310e0730643051eb88e884f19813a6c8b67d0fbafcda610d960e589db/onnxruntime_gpu-1.19.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a49740e079e7c5215830d30cde3df792e903df007aa0b0fd7aa797937061b27a", size = 226178508, upload-time = "2024-09-04T06:43:40.83Z" },
@ -4994,12 +5064,6 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyicu"
version = "2.16"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/c3/8d558b30deb33eb583c0bcae3e64d6db8316b69461a04bb9db5ff63d3f6e/pyicu-2.16.tar.gz", hash = "sha256:42b3a8062e3b23e927ca727e6b5e1730d86c70279834e4887152895d2eb012d9", size = 268126, upload-time = "2025-11-04T23:33:00.006Z" }
[[package]]
name = "pyjwt"
version = "2.8.0"
@ -5200,6 +5264,47 @@ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.3.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
{ name = "typing-extensions" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "pytest-xdist"
version = "3.8.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "execnet" },
{ name = "pytest" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
]
[[package]]
name = "python-calamine"
version = "0.6.1"
@ -5563,7 +5668,6 @@ dependencies = [
{ name = "psycopg2-binary" },
{ name = "pyclipper" },
{ name = "pycryptodomex" },
{ name = "pyicu" },
{ name = "pymysql" },
{ name = "pyobvector" },
{ name = "pyodbc" },
@ -5625,6 +5729,9 @@ test = [
{ name = "openpyxl" },
{ name = "pillow" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "pytest-xdist" },
{ name = "python-docx" },
{ name = "python-pptx" },
{ name = "reportlab" },
@ -5728,7 +5835,6 @@ requires-dist = [
{ name = "psycopg2-binary", specifier = "==2.9.9" },
{ name = "pyclipper", specifier = "==1.3.0.post5" },
{ name = "pycryptodomex", specifier = "==3.20.0" },
{ name = "pyicu", specifier = ">=2.15.3,<3.0.0" },
{ name = "pymysql", specifier = ">=1.1.1,<2.0.0" },
{ name = "pyobvector", specifier = "==0.2.18" },
{ name = "pyodbc", specifier = ">=5.2.0,<6.0.0" },
@ -5790,6 +5896,9 @@ test = [
{ name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pillow", specifier = ">=10.4.0" },
{ name = "pytest", specifier = ">=8.3.5" },
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
{ name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-xdist", specifier = ">=3.8.0" },
{ name = "python-docx", specifier = ">=1.1.2" },
{ name = "python-pptx", specifier = ">=1.0.2" },
{ name = "reportlab", specifier = ">=4.4.1" },

View File

@ -433,7 +433,7 @@ export const useSelectDerivedMessages = () => {
);
const addNewestQuestion = useCallback(
(message: Message, answer: string = '') => {
(message: IMessage, answer: string = '') => {
setDerivedMessages((pre) => {
return [
...pre,
@ -446,6 +446,7 @@ export const useSelectDerivedMessages = () => {
{
role: MessageType.Assistant,
content: answer,
conversationId: message.conversationId,
id: buildMessageUuid({ ...message, role: MessageType.Assistant }),
},
];

View File

@ -16,11 +16,11 @@ import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-
import { isConversationIdExist } from '@/pages/next-chats/utils';
import chatService from '@/services/next-chat-service';
import api from '@/utils/api';
import { buildMessageListWithUuid, getConversationId } from '@/utils/chat';
import { buildMessageListWithUuid, generateConversationId } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { has } from 'lodash';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'umi';
import {
@ -36,6 +36,7 @@ export const enum ChatApiAction {
FetchDialog = 'fetchDialog',
FetchConversationList = 'fetchConversationList',
FetchConversation = 'fetchConversation',
FetchConversationManually = 'fetchConversationManually',
UpdateConversation = 'updateConversation',
RemoveConversation = 'removeConversation',
DeleteMessage = 'deleteMessage',
@ -59,29 +60,6 @@ export const useGetChatSearchParams = () => {
};
};
export const useClickDialogCard = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(() => {
return new URLSearchParams();
}, []);
const handleClickDialog = useCallback(
(dialogId: string) => {
newQueryParameters.set(ChatSearchParams.DialogId, dialogId);
// newQueryParameters.set(
// ChatSearchParams.ConversationId,
// EmptyConversationId,
// );
setSearchParams(newQueryParameters);
},
[newQueryParameters, setSearchParams],
);
return { handleClickDialog };
};
export const useFetchDialogList = () => {
const { searchString, handleInputChange } = useHandleSearchChange();
const { pagination, setPagination } = useGetPaginationWithRouter();
@ -222,28 +200,8 @@ export const useFetchDialog = () => {
//#region Conversation
export const useClickConversationCard = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(
() => new URLSearchParams(currentQueryParameters.toString()),
[currentQueryParameters],
);
const handleClickConversation = useCallback(
(conversationId: string, isNew: string) => {
newQueryParameters.set(ChatSearchParams.ConversationId, conversationId);
newQueryParameters.set(ChatSearchParams.isNew, isNew);
setSearchParams(newQueryParameters);
},
[setSearchParams, newQueryParameters],
);
return { handleClickConversation };
};
export const useFetchConversationList = () => {
const { id } = useParams();
const { handleClickConversation } = useClickConversationCard();
const { searchString, handleInputChange } = useHandleSearchStrChange();
@ -267,13 +225,6 @@ export const useFetchConversationList = () => {
{ params: { dialog_id: id } },
true,
);
if (data.code === 0) {
if (data.data.length > 0) {
handleClickConversation(data.data[0].id, '');
} else {
handleClickConversation('', '');
}
}
return data?.data;
},
});
@ -281,45 +232,33 @@ export const useFetchConversationList = () => {
return { data, loading, refetch, searchString, handleInputChange };
};
export const useFetchConversation = () => {
const { isNew, conversationId } = useGetChatSearchParams();
const { sharedId } = useGetSharedChatSearchParams();
export function useFetchConversationManually() {
const {
data,
isFetching: loading,
refetch,
} = useQuery<IClientConversation>({
queryKey: [ChatApiAction.FetchConversation, conversationId],
initialData: {} as IClientConversation,
// enabled: isConversationIdExist(conversationId),
gcTime: 0,
refetchOnWindowFocus: false,
queryFn: async () => {
if (
isNew !== 'true' &&
isConversationIdExist(sharedId || conversationId)
) {
const { data } = await chatService.getConversation(
{
params: {
conversationId: conversationId || sharedId,
},
isPending: loading,
mutateAsync,
} = useMutation<IClientConversation, unknown, string>({
mutationKey: [ChatApiAction.FetchConversationManually],
mutationFn: async (conversationId) => {
const { data } = await chatService.getConversation(
{
params: {
conversationId,
},
true,
);
},
true,
);
const conversation = data?.data ?? {};
const conversation = data?.data ?? {};
const messageList = buildMessageListWithUuid(conversation?.message);
const messageList = buildMessageListWithUuid(conversation?.message);
return { ...conversation, message: messageList };
}
return { message: [] };
return { ...conversation, message: messageList };
},
});
return { data, loading, refetch };
};
return { data, loading, fetchConversationManually: mutateAsync };
}
export const useUpdateConversation = () => {
const { t } = useTranslation();
@ -335,7 +274,7 @@ export const useUpdateConversation = () => {
...params,
conversation_id: params.conversation_id
? params.conversation_id
: getConversationId(),
: generateConversationId(),
});
if (data.code === 0) {
queryClient.invalidateQueries({

View File

@ -24,6 +24,15 @@ import { buildLlmUuid } from '@/utils/llm-util';
export const enum LLMApiAction {
LlmList = 'llmList',
MyLlmList = 'myLlmList',
MyLlmListDetailed = 'myLlmListDetailed',
FactoryList = 'factoryList',
SaveApiKey = 'saveApiKey',
SaveTenantInfo = 'saveTenantInfo',
AddLlm = 'addLlm',
DeleteLlm = 'deleteLlm',
EnableLlm = 'enableLlm',
DeleteFactory = 'deleteFactory',
}
export const useFetchLlmList = (modelType?: LlmModelType) => {
@ -177,7 +186,7 @@ export const useComposeLlmOptionsByModelTypes = (
export const useFetchLlmFactoryList = (): ResponseGetType<IFactory[]> => {
const { data, isFetching: loading } = useQuery({
queryKey: ['factoryList'],
queryKey: [LLMApiAction.FactoryList],
initialData: [],
gcTime: 0,
queryFn: async () => {
@ -196,7 +205,7 @@ export const useFetchMyLlmList = (): ResponseGetType<
Record<string, IMyLlmValue>
> => {
const { data, isFetching: loading } = useQuery({
queryKey: ['myLlmList'],
queryKey: [LLMApiAction.MyLlmList],
initialData: {},
gcTime: 0,
queryFn: async () => {
@ -213,7 +222,7 @@ export const useFetchMyLlmListDetailed = (): ResponseGetType<
Record<string, any>
> => {
const { data, isFetching: loading } = useQuery({
queryKey: ['myLlmListDetailed'],
queryKey: [LLMApiAction.MyLlmListDetailed],
initialData: {},
gcTime: 0,
queryFn: async () => {
@ -271,14 +280,16 @@ export const useSaveApiKey = () => {
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['saveApiKey'],
mutationKey: [LLMApiAction.SaveApiKey],
mutationFn: async (params: IApiKeySavingParams) => {
const { data } = await userService.set_api_key(params);
if (data.code === 0) {
message.success(t('message.modified'));
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
}
return data.code;
},
@ -303,7 +314,7 @@ export const useSaveTenantInfo = () => {
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['saveTenantInfo'],
mutationKey: [LLMApiAction.SaveTenantInfo],
mutationFn: async (params: ISystemModelSettingSavingParams) => {
const { data } = await userService.set_tenant_info(params);
if (data.code === 0) {
@ -324,13 +335,16 @@ export const useAddLlm = () => {
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['addLlm'],
mutationKey: [LLMApiAction.AddLlm],
mutationFn: async (params: IAddLlmRequestBody) => {
const { data } = await userService.add_llm(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.LlmList] });
message.success(t('message.modified'));
}
return data.code;
@ -348,13 +362,15 @@ export const useDeleteLlm = () => {
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteLlm'],
mutationKey: [LLMApiAction.DeleteLlm],
mutationFn: async (params: IDeleteLlmRequestBody) => {
const { data } = await userService.delete_llm(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
message.success(t('message.deleted'));
}
return data.code;
@ -372,7 +388,7 @@ export const useEnableLlm = () => {
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['enableLlm'],
mutationKey: [LLMApiAction.EnableLlm],
mutationFn: async (params: IDeleteLlmRequestBody & { enable: boolean }) => {
const reqParam: IDeleteLlmRequestBody & {
enable?: boolean;
@ -381,9 +397,11 @@ export const useEnableLlm = () => {
delete reqParam.enable;
const { data } = await userService.enable_llm(reqParam);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
message.success(t('message.modified'));
}
return data.code;
@ -401,14 +419,16 @@ export const useDeleteFactory = () => {
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteFactory'],
mutationKey: [LLMApiAction.DeleteFactory],
mutationFn: async (params: IDeleteLlmRequestBody) => {
const { data } = await userService.deleteFactory(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['myLlmList'] });
queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] });
queryClient.invalidateQueries({ queryKey: ['factoryList'] });
queryClient.invalidateQueries({ queryKey: ['llmList'] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.LlmList] });
message.success(t('message.deleted'));
}
return data.code;

View File

@ -187,6 +187,7 @@ export interface IExternalChatInfo {
export interface IMessage extends Message {
id: string;
reference?: IReference; // the latest news has reference
conversationId?: string; // To distinguish which conversation the message belongs to
}
export interface IClientConversation extends IConversation {

View File

@ -15,12 +15,12 @@ import {
import { MessageType } from '@/constants/chat';
import { useScrollToBottom } from '@/hooks/logic-hooks';
import {
useFetchConversation,
useFetchDialog,
useGetChatSearchParams,
useSetDialog,
} from '@/hooks/use-chat-request';
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
import { IClientConversation, IMessage } from '@/interfaces/database/chat';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
@ -38,13 +38,14 @@ import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-creat
import { useSendMessage } from '../../hooks/use-send-chat-message';
import { useSendMultipleChatMessage } from '../../hooks/use-send-multiple-message';
import { buildMessageItemReference } from '../../utils';
import { IMessage } from '../interface';
import { useAddChatBox } from '../use-add-box';
import { useSetDefaultModel } from './use-set-default-model';
type MultipleChatBoxProps = {
controller: AbortController;
chatBoxIds: string[];
stopOutputMessage(): void;
conversation: IClientConversation;
} & Pick<
ReturnType<typeof useAddChatBox>,
'removeChatBox' | 'addChatBox' | 'chatBoxIds'
@ -55,6 +56,7 @@ type ChatCardProps = {
idx: number;
derivedMessages: IMessage[];
sendLoading: boolean;
conversation: IClientConversation;
} & Pick<
MultipleChatBoxProps,
'controller' | 'removeChatBox' | 'addChatBox' | 'chatBoxIds'
@ -72,6 +74,7 @@ const ChatCard = forwardRef(function ChatCard(
derivedMessages,
sendLoading,
clickDocumentButton,
conversation,
}: ChatCardProps,
ref,
) {
@ -97,7 +100,8 @@ const ChatCard = forwardRef(function ChatCard(
const { data: userInfo } = useFetchUserInfo();
const { data: currentDialog } = useFetchDialog();
const { data: conversation } = useFetchConversation();
useSetDefaultModel(form);
const isLatestChat = idx === chatBoxIds.length - 1;
@ -202,6 +206,7 @@ export function MultipleChatBox({
removeChatBox,
addChatBox,
stopOutputMessage,
conversation,
}: MultipleChatBoxProps) {
const {
value,
@ -237,6 +242,7 @@ export function MultipleChatBox({
ref={setFormRef(id)}
sendLoading={sendLoading}
clickDocumentButton={clickDocumentButton}
conversation={conversation}
></ChatCard>
))}
</div>

View File

@ -4,12 +4,13 @@ import PdfSheet from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { MessageType } from '@/constants/chat';
import {
useFetchConversation,
useFetchDialog,
useGetChatSearchParams,
} from '@/hooks/use-chat-request';
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
import { IClientConversation } from '@/interfaces/database/chat';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { useEffect } from 'react';
import {
useGetSendButtonDisabled,
useSendButtonDisabled,
@ -21,9 +22,14 @@ import { buildMessageItemReference } from '../../utils';
interface IProps {
controller: AbortController;
stopOutputMessage(): void;
conversation: IClientConversation;
}
export function SingleChatBox({ controller, stopOutputMessage }: IProps) {
export function SingleChatBox({
controller,
stopOutputMessage,
conversation,
}: IProps) {
const {
value,
scrollRef,
@ -37,18 +43,32 @@ export function SingleChatBox({ controller, stopOutputMessage }: IProps) {
removeMessageById,
handleUploadFile,
removeFile,
setDerivedMessages,
} = useSendMessage(controller);
const { data: userInfo } = useFetchUserInfo();
const { data: currentDialog } = useFetchDialog();
const { createConversationBeforeUploadDocument } =
useCreateConversationBeforeUploadDocument();
const { conversationId } = useGetChatSearchParams();
const { data: conversation } = useFetchConversation();
const disabled = useGetSendButtonDisabled();
const sendDisabled = useSendButtonDisabled(value);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
useEffect(() => {
const messages = conversation?.message;
if (Array.isArray(messages)) {
setDerivedMessages(messages);
}
}, [conversation?.message, setDerivedMessages]);
useEffect(() => {
// Clear the message list after deleting the conversation.
if (conversationId === '') {
setDerivedMessages([]);
}
}, [conversationId, setDerivedMessages]);
return (
<section className="flex flex-col p-5 h-full">
<div ref={messageContainerRef} className="flex-1 overflow-auto min-h-0">

View File

@ -0,0 +1,18 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import { useMount } from 'ahooks';
import { UseFormReturn } from 'react-hook-form';
export function useSetDefaultModel(form: UseFormReturn<any>) {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
useMount(() => {
const firstModel = modelOptions.at(0)?.options.at(0)?.value;
if (firstModel) {
form.setValue('llm_id', firstModel);
}
});
}

View File

@ -5,11 +5,15 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useRemoveConversation } from '@/hooks/use-chat-request';
import {
useGetChatSearchParams,
useRemoveConversation,
} from '@/hooks/use-chat-request';
import { IConversation } from '@/interfaces/database/chat';
import { Trash2 } from 'lucide-react';
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useChatUrlParams } from '../hooks/use-chat-url';
export function ConversationDropdown({
children,
@ -20,22 +24,27 @@ export function ConversationDropdown({
removeTemporaryConversation?: (conversationId: string) => void;
}) {
const { t } = useTranslation();
const { setConversationBoth } = useChatUrlParams();
const { removeConversation } = useRemoveConversation();
const { isNew } = useGetChatSearchParams();
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
if (conversation.is_new && removeTemporaryConversation) {
removeTemporaryConversation(conversation.id);
removeConversation([]);
} else {
removeConversation([conversation.id]);
}
}, [
conversation.id,
conversation.is_new,
removeConversation,
removeTemporaryConversation,
]);
const handleDelete: MouseEventHandler<HTMLDivElement> =
useCallback(async () => {
if (isNew === 'true' && removeTemporaryConversation) {
removeTemporaryConversation(conversation.id);
} else {
const code = await removeConversation([conversation.id]);
if (code === 0) {
setConversationBoth('', '');
}
}
}, [
conversation.id,
isNew,
removeConversation,
removeTemporaryConversation,
setConversationBoth,
]);
return (
<DropdownMenu>

View File

@ -15,13 +15,17 @@ import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import {
useFetchConversation,
useFetchConversationList,
useFetchConversationManually,
useFetchDialog,
useGetChatSearchParams,
} from '@/hooks/use-chat-request';
import { IClientConversation } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import { useMount } from 'ahooks';
import { isEmpty } from 'lodash';
import { ArrowUpRight, LogOut, Send } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { useHandleClickConversationCard } from '../hooks/use-click-card';
@ -37,26 +41,54 @@ export default function Chat() {
const { navigateToChatList } = useNavigatePage();
const { data } = useFetchDialog();
const { t } = useTranslation();
const { data: conversation } = useFetchConversation();
const [currentConversation, setCurrentConversation] =
useState<IClientConversation>({} as IClientConversation);
const { fetchConversationManually } = useFetchConversationManually();
const { handleConversationCardClick, controller, stopOutputMessage } =
useHandleClickConversationCard();
const { visible: settingVisible, switchVisible: switchSettingVisible } =
useSetModalState(true);
const {
removeChatBox,
addChatBox,
chatBoxIds,
hasSingleChatBox,
hasThreeChatBox,
} = useAddChatBox();
const { isDebugMode, switchDebugMode } = useSwitchDebugMode();
const { removeChatBox, addChatBox, chatBoxIds, hasSingleChatBox } =
useAddChatBox(isDebugMode);
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
const { conversationId, isNew } = useGetChatSearchParams();
const { isDebugMode, switchDebugMode } = useSwitchDebugMode();
const { data: dialogList } = useFetchConversationList();
const currentConversationName = useMemo(() => {
return dialogList.find((x) => x.id === conversationId)?.name;
}, [conversationId, dialogList]);
const fetchConversation: typeof handleConversationCardClick = useCallback(
async (conversationId, isNew) => {
if (conversationId && !isNew) {
const conversation = await fetchConversationManually(conversationId);
if (!isEmpty(conversation)) {
setCurrentConversation(conversation);
}
}
},
[fetchConversationManually],
);
const handleSessionClick: typeof handleConversationCardClick = useCallback(
(conversationId, isNew) => {
handleConversationCardClick(conversationId, isNew);
fetchConversation(conversationId, isNew);
},
[fetchConversation, handleConversationCardClick],
);
useMount(() => {
fetchConversation(conversationId, isNew === 'true');
});
if (isDebugMode) {
return (
@ -75,6 +107,7 @@ export default function Chat() {
removeChatBox={removeChatBox}
addChatBox={addChatBox}
stopOutputMessage={stopOutputMessage}
conversation={currentConversation}
></MultipleChatBox>
</section>
);
@ -104,7 +137,7 @@ export default function Chat() {
<div className="flex flex-1 min-h-0 pb-9">
<Sessions
hasSingleChatBox={hasSingleChatBox}
handleConversationCardClick={handleConversationCardClick}
handleConversationCardClick={handleSessionClick}
switchSettingVisible={switchSettingVisible}
></Sessions>
@ -115,16 +148,8 @@ export default function Chat() {
className={cn('p-5', { 'border-b': hasSingleChatBox })}
>
<CardTitle className="flex justify-between items-center text-base">
<div className="truncate">{conversation.name}</div>
<Button
variant={'ghost'}
onClick={switchDebugMode}
disabled={
hasThreeChatBox ||
isEmpty(conversationId) ||
isNew === 'true'
}
>
<div className="truncate">{currentConversationName}</div>
<Button variant={'ghost'} onClick={switchDebugMode}>
<ArrowUpRight /> {t('chat.multipleModels')}
</Button>
</CardTitle>
@ -133,6 +158,7 @@ export default function Chat() {
<SingleChatBox
controller={controller}
stopOutputMessage={stopOutputMessage}
conversation={currentConversation}
></SingleChatBox>
</CardContent>
</Card>

View File

@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
export function useAddChatBox() {
export function useAddChatBox(isDebugMode: boolean) {
const [ids, setIds] = useState<string[]>([uuid()]);
const hasSingleChatBox = ids.length === 1;
@ -16,6 +16,12 @@ export function useAddChatBox() {
setIds((prev) => prev.filter((x) => x !== id));
}, []);
useEffect(() => {
if (!isDebugMode) {
setIds((pre) => pre.slice(0, 1));
}
}, [isDebugMode]);
return {
chatBoxIds: ids,
hasSingleChatBox,

View File

@ -1,12 +1,10 @@
import { useGetChatSearchParams } from '@/hooks/use-chat-request';
import { trim } from 'lodash';
import { useParams } from 'umi';
export const useGetSendButtonDisabled = () => {
const { conversationId } = useGetChatSearchParams();
const { id: dialogId } = useParams();
return dialogId === '' || conversationId === '';
return dialogId === '';
};
export const useSendButtonDisabled = (value: string) => {

View File

@ -0,0 +1,97 @@
import { ChatSearchParams } from '@/constants/chat';
import { useGetChatSearchParams } from '@/hooks/use-chat-request';
import { IMessage } from '@/interfaces/database/chat';
import { generateConversationId } from '@/utils/chat';
import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'umi';
import { useSetConversation } from './use-set-conversation';
/**
* Consolidated hook for managing chat URL parameters (conversationId and isNew)
* Replaces: useClickConversationCard from use-chat-request.ts and useSetChatRouteParams from use-set-chat-route.ts
*/
export const useChatUrlParams = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(
() => new URLSearchParams(currentQueryParameters.toString()),
[currentQueryParameters],
);
const setConversationId = useCallback(
(conversationId: string) => {
newQueryParameters.set(ChatSearchParams.ConversationId, conversationId);
setSearchParams(newQueryParameters);
},
[setSearchParams, newQueryParameters],
);
const setIsNew = useCallback(
(isNew: string) => {
newQueryParameters.set(ChatSearchParams.isNew, isNew);
setSearchParams(newQueryParameters);
},
[setSearchParams, newQueryParameters],
);
const getIsNew = useCallback(() => {
return newQueryParameters.get(ChatSearchParams.isNew);
}, [newQueryParameters]);
const setConversationBoth = useCallback(
(conversationId: string, isNew: string) => {
newQueryParameters.set(ChatSearchParams.ConversationId, conversationId);
newQueryParameters.set(ChatSearchParams.isNew, isNew);
setSearchParams(newQueryParameters);
},
[setSearchParams, newQueryParameters],
);
return {
setConversationId,
setIsNew,
getIsNew,
setConversationBoth,
};
};
export function useCreateConversationBeforeSendMessage() {
const { conversationId, isNew } = useGetChatSearchParams();
const { setConversation } = useSetConversation();
const { setIsNew, setConversationBoth } = useChatUrlParams();
// Create conversation if it doesn't exist
const createConversationBeforeSendMessage = useCallback(
async (value: string) => {
let currentMessages: Array<IMessage> = [];
const currentConversationId = generateConversationId();
if (conversationId === '' || isNew === 'true') {
if (conversationId === '') {
setConversationBoth(currentConversationId, 'true');
}
const data = await setConversation(
value,
true,
conversationId || currentConversationId,
);
if (data.code !== 0) {
return;
} else {
setIsNew('');
currentMessages = data.data.message;
}
}
const targetConversationId = conversationId || currentConversationId;
return {
targetConversationId,
currentMessages,
};
},
[conversationId, isNew, setConversation, setConversationBoth, setIsNew],
);
return {
createConversationBeforeSendMessage,
};
}

View File

@ -1,9 +1,9 @@
import { useClickConversationCard } from '@/hooks/use-chat-request';
import { useCallback, useState } from 'react';
import { useChatUrlParams } from './use-chat-url';
export function useHandleClickConversationCard() {
const [controller, setController] = useState(new AbortController());
const { handleClickConversation } = useClickConversationCard();
const { setConversationBoth } = useChatUrlParams();
const stopOutputMessage = useCallback(() => {
setController((pre) => {
@ -14,10 +14,10 @@ export function useHandleClickConversationCard() {
const handleConversationCardClick = useCallback(
(conversationId: string, isNew: boolean) => {
handleClickConversation(conversationId, isNew ? 'true' : '');
setConversationBoth(conversationId, isNew ? 'true' : '');
stopOutputMessage();
},
[handleClickConversation, stopOutputMessage],
[setConversationBoth, stopOutputMessage],
);
return { controller, handleConversationCardClick, stopOutputMessage };

View File

@ -1,23 +1,23 @@
import { useCallback } from 'react';
import { useParams } from 'umi';
import { useSetChatRouteParams } from './use-set-chat-route';
import { useChatUrlParams } from './use-chat-url';
import { useSetConversation } from './use-set-conversation';
export const useCreateConversationBeforeUploadDocument = () => {
const { setConversation } = useSetConversation();
const { id: dialogId } = useParams();
const { getConversationIsNew } = useSetChatRouteParams();
const { getIsNew } = useChatUrlParams();
const createConversationBeforeUploadDocument = useCallback(
async (message: string) => {
const isNew = getConversationIsNew();
const isNew = getIsNew();
if (isNew === 'true') {
const data = await setConversation(message, true);
return data;
}
},
[setConversation, getConversationIsNew],
[setConversation, getIsNew],
);
return {

View File

@ -1,13 +1,14 @@
import { ChatSearchParams, MessageType } from '@/constants/chat';
import { MessageType } from '@/constants/chat';
import { useTranslate } from '@/hooks/common-hooks';
import {
useFetchConversationList,
useFetchDialogList,
} from '@/hooks/use-chat-request';
import { IConversation } from '@/interfaces/database/chat';
import { getConversationId } from '@/utils/chat';
import { generateConversationId } from '@/utils/chat';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useParams, useSearchParams } from 'umi';
import { useParams } from 'umi';
import { useChatUrlParams } from './use-chat-url';
export const useFindPrologueFromDialogList = () => {
const { id: dialogId } = useParams();
@ -20,25 +21,6 @@ export const useFindPrologueFromDialogList = () => {
return prologue;
};
export const useSetNewConversationRouteParams = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(
() => new URLSearchParams(currentQueryParameters.toString()),
[currentQueryParameters],
);
const setNewConversationRouteParams = useCallback(
(conversationId: string, isNew: string) => {
newQueryParameters.set(ChatSearchParams.ConversationId, conversationId);
newQueryParameters.set(ChatSearchParams.isNew, isNew);
setSearchParams(newQueryParameters);
},
[newQueryParameters, setSearchParams],
);
return { setNewConversationRouteParams };
};
export const useSelectDerivedConversationList = () => {
const { t } = useTranslate('chat');
@ -49,15 +31,16 @@ export const useSelectDerivedConversationList = () => {
handleInputChange,
searchString,
} = useFetchConversationList();
const { id: dialogId } = useParams();
const { setNewConversationRouteParams } = useSetNewConversationRouteParams();
const prologue = useFindPrologueFromDialogList();
const { setConversationBoth } = useChatUrlParams();
const addTemporaryConversation = useCallback(() => {
const conversationId = getConversationId();
const conversationId = generateConversationId();
setList((pre) => {
if (dialogId) {
setNewConversationRouteParams(conversationId, 'true');
setConversationBoth(conversationId, 'true');
const nextList = [
{
id: conversationId,
@ -78,7 +61,7 @@ export const useSelectDerivedConversationList = () => {
return pre;
});
}, [conversationList, dialogId, prologue, t, setNewConversationRouteParams]);
}, [dialogId, setConversationBoth, t, prologue, conversationList]);
const removeTemporaryConversation = useCallback((conversationId: string) => {
setList((prevList) => {

View File

@ -1,4 +1,3 @@
import { FileUploadProps } from '@/components/file-upload';
import { MessageType } from '@/constants/chat';
import {
useHandleMessageInputChange,
@ -6,19 +5,15 @@ import {
useSelectDerivedMessages,
useSendMessageWithSse,
} from '@/hooks/logic-hooks';
import {
useFetchConversation,
useGetChatSearchParams,
} from '@/hooks/use-chat-request';
import { IMessage, Message } from '@/interfaces/database/chat';
import { useGetChatSearchParams } from '@/hooks/use-chat-request';
import { IMessage } from '@/interfaces/database/chat';
import api from '@/utils/api';
import { trim } from 'lodash';
import { useCallback, useEffect } from 'react';
import { useParams } from 'umi';
import { v4 as uuid } from 'uuid';
import { useCreateConversationBeforeSendMessage } from './use-chat-url';
import { useFindPrologueFromDialogList } from './use-select-conversation-list';
import { useSetChatRouteParams } from './use-set-chat-route';
import { useSetConversation } from './use-set-conversation';
import { useUploadFile } from './use-upload-file';
export const useSelectNextMessages = () => {
@ -33,8 +28,7 @@ export const useSelectNextMessages = () => {
removeMessageById,
removeMessagesAfterCurrentMessage,
} = useSelectDerivedMessages();
const { data: conversation, loading } = useFetchConversation();
const { conversationId, isNew } = useGetChatSearchParams();
const { isNew, conversationId } = useGetChatSearchParams();
const { id: dialogId } = useParams();
const prologue = useFindPrologueFromDialogList();
@ -44,45 +38,31 @@ export const useSelectNextMessages = () => {
role: MessageType.Assistant,
content: prologue,
id: uuid(),
conversationId: conversationId,
} as IMessage;
setDerivedMessages([nextMessage]);
}
}, [dialogId, isNew, prologue, setDerivedMessages]);
}, [conversationId, dialogId, isNew, prologue, setDerivedMessages]);
useEffect(() => {
addPrologue();
}, [addPrologue]);
useEffect(() => {
if (
conversationId &&
isNew !== 'true' &&
conversation.message?.length > 0
) {
setDerivedMessages(conversation.message);
}
if (!conversationId) {
setDerivedMessages([]);
}
}, [conversation.message, conversationId, setDerivedMessages, isNew]);
return {
scrollRef,
messageContainerRef,
derivedMessages,
loading,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
setDerivedMessages,
};
};
export const useSendMessage = (controller: AbortController) => {
const { setConversation } = useSetConversation();
const { conversationId, isNew } = useGetChatSearchParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
@ -96,31 +76,13 @@ export const useSendMessage = (controller: AbortController) => {
scrollRef,
messageContainerRef,
derivedMessages,
loading,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
setDerivedMessages,
} = useSelectNextMessages();
const { setConversationIsNew, getConversationIsNew } =
useSetChatRouteParams();
const onUploadFile: NonNullable<FileUploadProps['onUpload']> = useCallback(
async (files, options) => {
const isNew = getConversationIsNew();
if (isNew === 'true' && Array.isArray(files) && files.length) {
const data = await setConversation(files[0].name, true);
if (data.code === 0) {
handleUploadFile(files, options, data.data?.id);
}
} else {
handleUploadFile(files, options);
}
},
[getConversationIsNew, handleUploadFile, setConversation],
);
const sendMessage = useCallback(
async ({
@ -128,14 +90,19 @@ export const useSendMessage = (controller: AbortController) => {
currentConversationId,
messages,
}: {
message: Message;
message: IMessage;
currentConversationId?: string;
messages?: Message[];
messages?: IMessage[];
}) => {
const res = await send(
{
conversation_id: currentConversationId ?? conversationId,
messages: [...(messages ?? derivedMessages ?? []), message],
messages: [
...(Array.isArray(messages) && messages?.length > 0
? messages
: derivedMessages ?? []),
message,
],
},
controller,
);
@ -157,44 +124,62 @@ export const useSendMessage = (controller: AbortController) => {
],
);
const handleSendMessage = useCallback(
async (message: Message) => {
const isNew = getConversationIsNew();
if (isNew !== 'true') {
sendMessage({ message });
} else {
const data = await setConversation(
message.content,
true,
conversationId,
);
if (data.code === 0) {
setConversationIsNew('');
const id = data.data.id;
// currentConversationIdRef.current = id;
sendMessage({
message,
currentConversationId: id,
messages: data.data.message,
});
}
}
},
[
setConversation,
sendMessage,
setConversationIsNew,
getConversationIsNew,
conversationId,
],
);
const { regenerateMessage } = useRegenerateMessage({
removeMessagesAfterCurrentMessage,
sendMessage,
messages: derivedMessages,
});
const { createConversationBeforeSendMessage } =
useCreateConversationBeforeSendMessage();
const handlePressEnter = useCallback(async () => {
if (trim(value) === '') return;
const data = await createConversationBeforeSendMessage(value);
if (data === undefined) {
return;
}
const { targetConversationId, currentMessages } = data;
const id = uuid();
addNewestQuestion({
content: value,
files: files,
id,
role: MessageType.User,
conversationId: targetConversationId,
});
if (done) {
setValue('');
sendMessage({
currentConversationId: targetConversationId,
messages: currentMessages,
message: {
id,
content: value.trim(),
role: MessageType.User,
files: files,
conversationId: targetConversationId,
},
});
}
clearFiles();
}, [
value,
createConversationBeforeSendMessage,
addNewestQuestion,
files,
done,
clearFiles,
setValue,
sendMessage,
]);
useEffect(() => {
// #1289
if (answer.answer && conversationId && isNew !== 'true') {
@ -202,36 +187,6 @@ export const useSendMessage = (controller: AbortController) => {
}
}, [answer, addNewestAnswer, conversationId, isNew]);
const handlePressEnter = useCallback(() => {
if (trim(value) === '') return;
const id = uuid();
addNewestQuestion({
content: value,
files: files,
id,
role: MessageType.User,
});
if (done) {
setValue('');
handleSendMessage({
id,
content: value.trim(),
role: MessageType.User,
files: files,
});
}
clearFiles();
}, [
value,
addNewestQuestion,
files,
done,
clearFiles,
setValue,
handleSendMessage,
]);
return {
handlePressEnter,
handleInputChange,
@ -239,13 +194,13 @@ export const useSendMessage = (controller: AbortController) => {
setValue,
regenerateMessage,
sendLoading: !done,
loading,
scrollRef,
messageContainerRef,
derivedMessages,
removeMessageById,
handleUploadFile: onUploadFile,
handleUploadFile,
isUploading,
removeFile,
setDerivedMessages,
};
};

View File

@ -12,6 +12,7 @@ import { trim } from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
import { useBuildFormRefs } from './use-build-form-refs';
import { useCreateConversationBeforeSendMessage } from './use-chat-url';
import { useUploadFile } from './use-upload-file';
export function useSendMultipleChatMessage(
@ -29,7 +30,11 @@ export function useSendMultipleChatMessage(
api.completeConversation,
);
const { handleUploadFile, files, clearFiles } = useUploadFile();
const { handleUploadFile, isUploading, files, clearFiles, removeFile } =
useUploadFile();
const { createConversationBeforeSendMessage } =
useCreateConversationBeforeSendMessage();
const { setFormRef, getLLMConfigById, isLLMConfigEmpty } =
useBuildFormRefs(chatBoxIds);
@ -170,10 +175,18 @@ export function useSendMultipleChatMessage(
],
);
const handlePressEnter = useCallback(() => {
const handlePressEnter = useCallback(async () => {
if (trim(value) === '') return;
const id = uuid();
const data = await createConversationBeforeSendMessage(value);
if (data === undefined) {
return;
}
const { targetConversationId, currentMessages } = data;
chatBoxIds.forEach((chatBoxId) => {
if (!isLLMConfigEmpty(chatBoxId)) {
addNewestQuestion({
@ -182,6 +195,7 @@ export function useSendMultipleChatMessage(
role: MessageType.User,
chatBoxId,
files,
conversationId: targetConversationId,
});
}
});
@ -196,8 +210,11 @@ export function useSendMultipleChatMessage(
content: value.trim(),
role: MessageType.User,
files,
conversationId: targetConversationId,
},
chatBoxId,
currentConversationId: targetConversationId,
messages: currentMessages,
});
}
});
@ -205,6 +222,7 @@ export function useSendMultipleChatMessage(
clearFiles();
}, [
value,
createConversationBeforeSendMessage,
chatBoxIds,
allDone,
clearFiles,
@ -234,5 +252,7 @@ export function useSendMultipleChatMessage(
sendLoading: !allDone,
setFormRef,
handleUploadFile,
isUploading,
removeFile,
};
}

View File

@ -1,25 +0,0 @@
import { ChatSearchParams } from '@/constants/chat';
import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'umi';
export const useSetChatRouteParams = () => {
const [currentQueryParameters, setSearchParams] = useSearchParams();
const newQueryParameters: URLSearchParams = useMemo(
() => new URLSearchParams(currentQueryParameters.toString()),
[currentQueryParameters],
);
const setConversationIsNew = useCallback(
(value: string) => {
newQueryParameters.set(ChatSearchParams.isNew, value);
setSearchParams(newQueryParameters);
},
[newQueryParameters, setSearchParams],
);
const getConversationIsNew = useCallback(() => {
return newQueryParameters.get(ChatSearchParams.isNew);
}, [newQueryParameters]);
return { setConversationIsNew, getConversationIsNew };
};

View File

@ -22,6 +22,7 @@ export const useSetConversation = () => {
{
role: MessageType.Assistant,
content: message,
conversationId,
},
],
});

View File

@ -1,6 +1,12 @@
import { FileUploadProps } from '@/components/file-upload';
import { useUploadAndParseFile } from '@/hooks/use-chat-request';
import {
useGetChatSearchParams,
useUploadAndParseFile,
} from '@/hooks/use-chat-request';
import { generateConversationId } from '@/utils/chat';
import { useCallback, useState } from 'react';
import { useChatUrlParams } from './use-chat-url';
import { useSetConversation } from './use-set-conversation';
export function useUploadFile() {
const { uploadAndParseFile, loading, cancel } = useUploadAndParseFile();
@ -8,6 +14,9 @@ export function useUploadFile() {
const [fileMap, setFileMap] = useState<Map<File, Record<string, any>>>(
new Map(),
);
const { setConversation } = useSetConversation();
const { conversationId, isNew } = useGetChatSearchParams();
const { setIsNew, setConversationBoth } = useChatUrlParams();
type FileUploadParameters = Parameters<
NonNullable<FileUploadProps['onUpload']>
@ -35,6 +44,44 @@ export function useUploadFile() {
[uploadAndParseFile],
);
const createConversationBeforeUploadFile: NonNullable<
FileUploadProps['onUpload']
> = useCallback(
async (files, options) => {
if (
(conversationId === '' || isNew === 'true') &&
Array.isArray(files) &&
files.length
) {
const currentConversationId = generateConversationId();
if (conversationId === '') {
setConversationBoth(currentConversationId, 'true');
}
const data = await setConversation(
files[0].name,
true,
conversationId || currentConversationId,
);
if (data.code === 0) {
setIsNew('');
handleUploadFile(files, options, data.data?.id);
}
} else {
handleUploadFile(files, options);
}
},
[
conversationId,
handleUploadFile,
isNew,
setConversation,
setConversationBoth,
setIsNew,
],
);
const clearFiles = useCallback(() => {
setCurrentFiles([]);
setFileMap(new Map());
@ -55,7 +102,7 @@ export function useUploadFile() {
);
return {
handleUploadFile,
handleUploadFile: createConversationBeforeUploadFile,
files: currentFiles,
isUploading: loading,
removeFile,

View File

@ -26,7 +26,7 @@ export const buildMessageListWithUuid = (messages?: Message[]) => {
);
};
export const getConversationId = () => {
export const generateConversationId = () => {
return uuid().replace(/-/g, '');
};