Compare commits

...

9 Commits

Author SHA1 Message Date
23062cb27a Feat: Configure colors according to the design draft#3221 (#9301)
### What problem does this PR solve?

Feat: Configure colors according to the design draft#3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-07 13:59:33 +08:00
63c2f5b821 Fix: virtual file cannot be displayed in KB (#9282)
### What problem does this PR solve?

Fix virtual file cannot be displayed in KB. #9265

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-07 11:08:03 +08:00
0a0bfc02a0 Refactor:naive_merge_with_images close useless images (#9296)
### What problem does this PR solve?

naive_merge_with_images close useless images

### Type of change

- [x] Refactoring
2025-08-07 11:07:29 +08:00
f0c34d4454 Feat: Render chat page #3221 (#9298)
### What problem does this PR solve?

Feat: Render chat page #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-07 11:07:15 +08:00
7c719f8365 fix: Optimized popups and the search page (#9297)
### What problem does this PR solve?

fix: Optimized popups and the search page #3221 
- Added a new PortalModal component
- Refactored the Modal component, adding show and hide methods to
support popups
- Updated the search page, adding a new query function and optimizing
the search card style
- Localized, added search-related translations

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-07 11:07:04 +08:00
4fc9e42e74 fix: add missing env vars and default values of service_conf.yaml (#9289)
### What problem does this PR solve?

Add missing env var `MYSQL_MAX_PACKET` to service_conf.yaml.template,
and add default values to opendal config to fix npe.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-08-07 10:41:05 +08:00
35539092d0 Add **kwargs to model base class constructors (#9252)
Updated constructors for base and derived classes in chat, embedding,
rerank, sequence2txt, and tts models to accept **kwargs. This change
improves extensibility and allows passing additional parameters without
breaking existing interfaces.

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

---------

Co-authored-by: IT: Sop.Son <sop.son@feavn.local>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-07 09:45:37 +08:00
581a54fbbb Feat: Search conversation by name #3221 (#9283)
### What problem does this PR solve?

Feat: Search conversation by name #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-08-07 09:41:03 +08:00
9ca86d801e Refa: add provider info while adding model. (#9273)
### What problem does this PR solve?
#9248

### Type of change

- [x] Refactoring
2025-08-07 09:40:42 +08:00
55 changed files with 1348 additions and 247 deletions

View File

@ -166,6 +166,17 @@ def create():
if DocumentService.query(name=req["name"], kb_id=kb_id):
return get_data_error_result(message="Duplicated document name in the same knowledgebase.")
kb_root_folder = FileService.get_kb_folder(kb.tenant_id)
if not kb_root_folder:
return get_data_error_result(message="Cannot find the root folder.")
kb_folder = FileService.new_a_file_from_kb(
kb.tenant_id,
kb.name,
kb_root_folder["id"],
)
if not kb_folder:
return get_data_error_result(message="Cannot find the kb folder for this file.")
doc = DocumentService.insert(
{
"id": get_uuid(),
@ -180,6 +191,9 @@ def create():
"size": 0,
}
)
FileService.add_file_from_kb(doc.to_dict(), kb_folder["id"], kb.tenant_id)
return get_json_result(data=doc.to_json())
except Exception as e:
return server_error_response(e)

View File

@ -81,7 +81,7 @@ def set_api_key():
raise Exception(m)
chat_passed = True
except Exception as e:
msg += f"\nFail to access model({llm.llm_name}) using this api key." + str(
msg += f"\nFail to access model({llm.fid}/{llm.llm_name}) using this api key." + str(
e)
elif not rerank_passed and llm.model_type == LLMType.RERANK:
assert factory in RerankModel, f"Re-rank model from {factory} is not supported yet."
@ -94,7 +94,7 @@ def set_api_key():
rerank_passed = True
logging.debug(f'passed model rerank {llm.llm_name}')
except Exception as e:
msg += f"\nFail to access model({llm.llm_name}) using this api key." + str(
msg += f"\nFail to access model({llm.fid}/{llm.llm_name}) using this api key." + str(
e)
if any([embd_passed, chat_passed, rerank_passed]):
msg = ''
@ -229,7 +229,7 @@ def add_llm():
if not tc and m.find("**ERROR**:") >= 0:
raise Exception(m)
except Exception as e:
msg += f"\nFail to access model({mdl_nm})." + str(
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(
e)
elif llm["model_type"] == LLMType.RERANK:
assert factory in RerankModel, f"RE-rank model from {factory} is not supported yet."
@ -243,9 +243,9 @@ def add_llm():
if len(arr) == 0:
raise Exception("Not known.")
except KeyError:
msg += f"{factory} dose not support this model({mdl_nm})"
msg += f"{factory} dose not support this model({factory}/{mdl_nm})"
except Exception as e:
msg += f"\nFail to access model({mdl_nm})." + str(
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(
e)
elif llm["model_type"] == LLMType.IMAGE2TEXT.value:
assert factory in CvModel, f"Image to text model from {factory} is not supported yet."
@ -260,7 +260,7 @@ def add_llm():
if not m and not tc:
raise Exception(m)
except Exception as e:
msg += f"\nFail to access model({mdl_nm})." + str(e)
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
elif llm["model_type"] == LLMType.TTS:
assert factory in TTSModel, f"TTS model from {factory} is not supported yet."
mdl = TTSModel[factory](
@ -270,7 +270,7 @@ def add_llm():
for resp in mdl.tts("Hello~ Ragflower!"):
pass
except RuntimeError as e:
msg += f"\nFail to access model({mdl_nm})." + str(e)
msg += f"\nFail to access model({factory}/{mdl_nm})." + str(e)
else:
# TODO: check other type of models
pass
@ -358,8 +358,6 @@ def my_llms():
return server_error_response(e)
@manager.route('/list', methods=['GET']) # noqa: F821
@login_required
def list_app():

View File

@ -62,6 +62,8 @@ MYSQL_DBNAME=rag_flow
# The port used to expose the MySQL service to the host machine,
# allowing EXTERNAL access to the MySQL database running inside the Docker container.
MYSQL_PORT=5455
# The maximum size of communication packets sent to the MySQL server
MYSQL_MAX_PACKET=1073741824
# The hostname where the MinIO service is exposed
MINIO_HOST=minio

View File

@ -9,6 +9,7 @@ mysql:
port: 3306
max_connections: 900
stale_timeout: 300
max_allowed_packet: ${MYSQL_MAX_PACKET:-1073741824}
minio:
user: '${MINIO_USER:-rag_flow}'
password: '${MINIO_PASSWORD:-infini_rag_flow}'

View File

@ -1216,11 +1216,11 @@ class LmStudioChat(Base):
class OpenAI_APIChat(Base):
_FACTORY_NAME = ["VLLM", "OpenAI-API-Compatible"]
def __init__(self, key, model_name, base_url):
def __init__(self, key, model_name, base_url, **kwargs):
if not base_url:
raise ValueError("url cannot be None")
model_name = model_name.split("___")[0]
super().__init__(key, model_name, base_url)
super().__init__(key, model_name, base_url, **kwargs)
class PPIOChat(Base):

View File

@ -37,7 +37,12 @@ from rag.utils import num_tokens_from_string, truncate
class Base(ABC):
def __init__(self, key, model_name):
def __init__(self, key, model_name, **kwargs):
"""
Constructor for abstract base class.
Parameters are accepted for interface consistency but are not stored.
Subclasses should implement their own initialization as needed.
"""
pass
def encode(self, texts: list):
@ -864,7 +869,7 @@ class VoyageEmbed(Base):
class HuggingFaceEmbed(Base):
_FACTORY_NAME = "HuggingFace"
def __init__(self, key, model_name, base_url=None):
def __init__(self, key, model_name, base_url=None, **kwargs):
if not model_name:
raise ValueError("Model name cannot be None")
self.key = key
@ -946,4 +951,4 @@ class Ai302Embed(Base):
def __init__(self, key, model_name, base_url="https://api.302.ai/v1/embeddings"):
if not base_url:
base_url = "https://api.302.ai/v1/embeddings"
super().__init__(key, model_name, base_url)
super().__init__(key, model_name, base_url)

View File

@ -33,7 +33,11 @@ from api.utils.log_utils import log_exception
from rag.utils import num_tokens_from_string, truncate
class Base(ABC):
def __init__(self, key, model_name):
def __init__(self, key, model_name, **kwargs):
"""
Abstract base class constructor.
Parameters are not stored; initialization is left to subclasses.
"""
pass
def similarity(self, query: str, texts: list):
@ -315,7 +319,7 @@ class NvidiaRerank(Base):
class LmStudioRerank(Base):
_FACTORY_NAME = "LM-Studio"
def __init__(self, key, model_name, base_url):
def __init__(self, key, model_name, base_url, **kwargs):
pass
def similarity(self, query: str, texts: list):
@ -396,7 +400,7 @@ class CoHereRerank(Base):
class TogetherAIRerank(Base):
_FACTORY_NAME = "TogetherAI"
def __init__(self, key, model_name, base_url):
def __init__(self, key, model_name, base_url, **kwargs):
pass
def similarity(self, query: str, texts: list):

View File

@ -28,7 +28,11 @@ from rag.utils import num_tokens_from_string
class Base(ABC):
def __init__(self, key, model_name):
def __init__(self, key, model_name, **kwargs):
"""
Abstract base class constructor.
Parameters are not stored; initialization is left to subclasses.
"""
pass
def transcription(self, audio, **kwargs):

View File

@ -63,7 +63,11 @@ class ServeTTSRequest(BaseModel):
class Base(ABC):
def __init__(self, key, model_name, base_url):
def __init__(self, key, model_name, base_url, **kwargs):
"""
Abstract base class constructor.
Parameters are not stored; subclasses should handle their own initialization.
"""
pass
def tts(self, audio):

View File

@ -611,6 +611,10 @@ def naive_merge_with_images(texts, images, chunk_token_num=128, delimiter="\n。
if re.match(f"^{dels}$", sub_sec):
continue
add_chunk(sub_sec, image)
for img in images:
if isinstance(img, Image.Image):
img.close()
return cks, result_images

View File

@ -231,7 +231,7 @@ async def get_storage_binary(bucket, name):
return await trio.to_thread.run_sync(lambda: STORAGE_IMPL.get(bucket, name))
@timeout(60*40, 1)
@timeout(60*80, 1)
async def build_chunks(task, progress_callback):
if task["size"] > DOC_MAXIMUM_SIZE:
set_progress(task["id"], prog=-1, msg="File size exceeds( <= %dMb )" %

View File

@ -23,7 +23,7 @@ SET GLOBAL max_allowed_packet={}
def get_opendal_config():
try:
opendal_config = get_base_config('opendal', {})
if opendal_config.get("scheme") == 'mysql':
if opendal_config.get("scheme", "mysql") == 'mysql':
mysql_config = get_base_config('mysql', {})
max_packet = mysql_config.get("max_allowed_packet", 134217728)
kwargs = {
@ -33,7 +33,7 @@ def get_opendal_config():
"user": mysql_config.get("user", "root"),
"password": mysql_config.get("password", ""),
"database": mysql_config.get("name", "test_open_dal"),
"table": opendal_config.get("config").get("oss_table", "opendal_storage"),
"table": opendal_config.get("config", {}).get("oss_table", "opendal_storage"),
"max_allowed_packet": str(max_packet)
}
kwargs["connection_string"] = f"mysql://{kwargs['user']}:{quote_plus(kwargs['password'])}@{kwargs['host']}:{kwargs['port']}/{kwargs['database']}?max_allowed_packet={max_packet}"

View File

@ -30,6 +30,7 @@ class RAGFlowS3:
self.s3_config = settings.S3
self.access_key = self.s3_config.get('access_key', None)
self.secret_key = self.s3_config.get('secret_key', None)
self.session_token = self.s3_config.get('session_token', None)
self.region = self.s3_config.get('region', None)
self.endpoint_url = self.s3_config.get('endpoint_url', None)
self.signature_version = self.s3_config.get('signature_version', None)
@ -73,31 +74,32 @@ class RAGFlowS3:
s3_params = {
'aws_access_key_id': self.access_key,
'aws_secret_access_key': self.secret_key,
'aws_session_token': self.session_token,
}
if self.region in self.s3_config:
if self.region:
s3_params['region_name'] = self.region
if 'endpoint_url' in self.s3_config:
if self.endpoint_url:
s3_params['endpoint_url'] = self.endpoint_url
if 'signature_version' in self.s3_config:
config_kwargs['signature_version'] = self.signature_version
if 'addressing_style' in self.s3_config:
config_kwargs['addressing_style'] = self.addressing_style
if self.signature_version:
s3_params['signature_version'] = self.signature_version
if self.addressing_style:
s3_params['addressing_style'] = self.addressing_style
if config_kwargs:
s3_params['config'] = Config(**config_kwargs)
self.conn = boto3.client('s3', **s3_params)
self.conn = [boto3.client('s3', **s3_params)]
except Exception:
logging.exception(f"Fail to connect at region {self.region} or endpoint {self.endpoint_url}")
def __close__(self):
del self.conn
del self.conn[0]
self.conn = None
@use_default_bucket
def bucket_exists(self, bucket):
def bucket_exists(self, bucket, *args, **kwargs):
try:
logging.debug(f"head_bucket bucketname {bucket}")
self.conn.head_bucket(Bucket=bucket)
self.conn[0].head_bucket(Bucket=bucket)
exists = True
except ClientError:
logging.exception(f"head_bucket error {bucket}")
@ -109,10 +111,10 @@ class RAGFlowS3:
fnm = "txtxtxtxt1"
fnm, binary = f"{self.prefix_path}/{fnm}" if self.prefix_path else fnm, b"_t@@@1"
if not self.bucket_exists(bucket):
self.conn.create_bucket(Bucket=bucket)
self.conn[0].create_bucket(Bucket=bucket)
logging.debug(f"create bucket {bucket} ********")
r = self.conn.upload_fileobj(BytesIO(binary), bucket, fnm)
r = self.conn[0].upload_fileobj(BytesIO(binary), bucket, fnm)
return r
def get_properties(self, bucket, key):
@ -123,14 +125,14 @@ class RAGFlowS3:
@use_prefix_path
@use_default_bucket
def put(self, bucket, fnm, binary, **kwargs):
def put(self, bucket, fnm, binary, *args, **kwargs):
logging.debug(f"bucket name {bucket}; filename :{fnm}:")
for _ in range(1):
try:
if not self.bucket_exists(bucket):
self.conn.create_bucket(Bucket=bucket)
self.conn[0].create_bucket(Bucket=bucket)
logging.info(f"create bucket {bucket} ********")
r = self.conn.upload_fileobj(BytesIO(binary), bucket, fnm)
r = self.conn[0].upload_fileobj(BytesIO(binary), bucket, fnm)
return r
except Exception:
@ -140,18 +142,18 @@ class RAGFlowS3:
@use_prefix_path
@use_default_bucket
def rm(self, bucket, fnm, **kwargs):
def rm(self, bucket, fnm, *args, **kwargs):
try:
self.conn.delete_object(Bucket=bucket, Key=fnm)
self.conn[0].delete_object(Bucket=bucket, Key=fnm)
except Exception:
logging.exception(f"Fail rm {bucket}/{fnm}")
@use_prefix_path
@use_default_bucket
def get(self, bucket, fnm, **kwargs):
def get(self, bucket, fnm, *args, **kwargs):
for _ in range(1):
try:
r = self.conn.get_object(Bucket=bucket, Key=fnm)
r = self.conn[0].get_object(Bucket=bucket, Key=fnm)
object_data = r['Body'].read()
return object_data
except Exception:
@ -162,9 +164,9 @@ class RAGFlowS3:
@use_prefix_path
@use_default_bucket
def obj_exist(self, bucket, fnm, **kwargs):
def obj_exist(self, bucket, fnm, *args, **kwargs):
try:
if self.conn.head_object(Bucket=bucket, Key=fnm):
if self.conn[0].head_object(Bucket=bucket, Key=fnm):
return True
except ClientError as e:
if e.response['Error']['Code'] == '404':
@ -174,10 +176,10 @@ class RAGFlowS3:
@use_prefix_path
@use_default_bucket
def get_presigned_url(self, bucket, fnm, expires, **kwargs):
def get_presigned_url(self, bucket, fnm, expires, *args, **kwargs):
for _ in range(10):
try:
r = self.conn.generate_presigned_url('get_object',
r = self.conn[0].generate_presigned_url('get_object',
Params={'Bucket': bucket,
'Key': fnm},
ExpiresIn=expires)

View File

@ -58,7 +58,7 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => {
<HoverCardTrigger>
<div
key={tag}
className="w-fit flex items-center justify-center gap-2 border-dashed border px-1 rounded-sm bg-background-card"
className="w-fit flex items-center justify-center gap-2 border-dashed border px-1 rounded-sm bg-bg-card"
>
<div className="flex gap-2 items-center">
<div className="max-w-80 overflow-hidden text-ellipsis">
@ -90,7 +90,7 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => {
<Input
ref={inputRef}
type="text"
className="h-8 bg-background-card"
className="h-8 bg-bg-card"
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
@ -103,7 +103,7 @@ const EditTag = ({ value = [], onChange }: EditTagsProps) => {
) : (
<Button
variant="dashed"
className="w-fit flex items-center justify-center gap-2 bg-background-card"
className="w-fit flex items-center justify-center gap-2 bg-bg-card"
onClick={showInput}
style={tagPlusStyle}
>

View File

@ -226,7 +226,7 @@ function MessageItem({
? styles.messageTextDark
: styles.messageText]: isAssistant,
[styles.messageUserText]: !isAssistant,
'bg-background-card': !isAssistant,
'bg-bg-card': !isAssistant,
})}
>
{item.data ? (

View File

@ -11,7 +11,7 @@ const badgeVariants = cva(
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-background-card text-text-sub-title-invert hover:bg-secondary/80 rounded-md',
'border-transparent bg-bg-card text-text-sub-title-invert hover:bg-secondary/80 rounded-md',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',

View File

@ -15,8 +15,7 @@ const buttonVariants = cva(
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-text-sub-title-invert bg-transparent hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-background-card text-secondary-foreground hover:bg-secondary/80',
secondary: 'bg-bg-card text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
tertiary:
@ -52,7 +51,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
return (
<Comp
className={cn(
'bg-background-card',
'bg-bg-card',
buttonVariants({ variant, size, className }),
)}
ref={ref}

View File

@ -8,10 +8,7 @@ const Card = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-lg bg-background-card text-card-foreground shadow-sm',
className,
)}
className={cn('rounded-lg bg-bg-card shadow-sm', className)}
{...props}
/>
));

View File

@ -0,0 +1,102 @@
import { ReactNode, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { createRoot } from 'react-dom/client';
import { Modal, ModalProps } from './modal';
type PortalModalProps = Omit<ModalProps, 'open' | 'onOpenChange'> & {
visible: boolean;
onVisibleChange: (visible: boolean) => void;
container?: HTMLElement;
children: ReactNode;
[key: string]: any;
};
const PortalModal = ({
visible,
onVisibleChange,
container,
children,
...restProps
}: PortalModalProps) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
return () => setMounted(false);
}, []);
if (!mounted || !visible) return null;
console.log('PortalModal:', visible);
return createPortal(
<Modal open={visible} onOpenChange={onVisibleChange} {...restProps}>
{children}
</Modal>,
container || document.body,
);
};
export const createPortalModal = () => {
let container = document.createElement('div');
document.body.appendChild(container);
let currentProps: any = {};
let isVisible = false;
let root: ReturnType<typeof createRoot> | null = null;
root = createRoot(container);
const destroy = () => {
if (root && container) {
root.unmount();
if (container.parentNode) {
container.parentNode.removeChild(container);
}
root = null;
}
isVisible = false;
currentProps = {};
};
const render = () => {
const { onVisibleChange, ...props } = currentProps;
const modalParam = {
visible: isVisible,
onVisibleChange: (visible: boolean) => {
isVisible = visible;
if (onVisibleChange) {
onVisibleChange(visible);
}
if (!visible) {
render();
}
},
...props,
};
root?.render(isVisible ? <PortalModal {...modalParam} /> : null);
};
const show = (props: PortalModalProps) => {
if (!container) {
container = document.createElement('div');
document.body.appendChild(container);
}
if (!root) {
root = createRoot(container);
}
currentProps = { ...currentProps, ...props };
isVisible = true;
render();
};
const hide = () => {
isVisible = false;
render();
};
const update = (props = {}) => {
currentProps = { ...currentProps, ...props };
render();
};
return { show, hide, update, destroy };
};

View File

@ -1,15 +1,19 @@
// src/components/ui/modal.tsx
import { cn } from '@/lib/utils';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Loader, X } from 'lucide-react';
import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { createPortalModal } from './modal-manage';
interface ModalProps {
export interface ModalProps {
open: boolean;
onOpenChange?: (open: boolean) => void;
title?: ReactNode;
titleClassName?: string;
children: ReactNode;
footer?: ReactNode;
footerClassName?: string;
showfooter?: boolean;
className?: string;
size?: 'small' | 'default' | 'large';
@ -24,13 +28,19 @@ interface ModalProps {
onOk?: () => void;
onCancel?: () => void;
}
export interface ModalType extends FC<ModalProps> {
show: typeof modalIns.show;
hide: typeof modalIns.hide;
}
export const Modal: FC<ModalProps> = ({
const Modal: ModalType = ({
open,
onOpenChange,
title,
titleClassName,
children,
footer,
footerClassName,
showfooter = true,
className = '',
size = 'default',
@ -74,6 +84,7 @@ export const Modal: FC<ModalProps> = ({
}, [onOpenChange, onOk]);
const handleChange = (open: boolean) => {
onOpenChange?.(open);
console.log('open', open, onOpenChange);
if (open) {
handleOk();
}
@ -113,7 +124,12 @@ export const Modal: FC<ModalProps> = ({
);
}
return (
<div className="flex items-center justify-end border-t border-border px-6 py-4">
<div
className={cn(
'flex items-center justify-end px-6 py-4',
footerClassName,
)}
>
{footerTemp}
</div>
);
@ -126,6 +142,7 @@ export const Modal: FC<ModalProps> = ({
handleCancel,
handleOk,
showfooter,
footerClassName,
]);
return (
<DialogPrimitive.Root open={open} onOpenChange={handleChange}>
@ -139,11 +156,23 @@ export const Modal: FC<ModalProps> = ({
onClick={(e) => e.stopPropagation()}
>
{/* title */}
{title && (
<div className="flex items-center justify-between border-b border-border px-6 py-4">
<DialogPrimitive.Title className="text-lg font-medium text-foreground">
{title}
</DialogPrimitive.Title>
{(title || closable) && (
<div
className={cn(
'flex items-center px-6 py-4',
{
'justify-end': closable && !title,
'justify-between': closable && title,
'justify-start': !closable,
},
titleClassName,
)}
>
{title && (
<DialogPrimitive.Title className="text-lg font-medium text-foreground">
{title}
</DialogPrimitive.Title>
)}
{closable && (
<DialogPrimitive.Close asChild>
<button
@ -156,13 +185,9 @@ export const Modal: FC<ModalProps> = ({
)}
</div>
)}
{/* title */}
{!title && (
<DialogPrimitive.Title className="text-lg font-medium text-foreground"></DialogPrimitive.Title>
)}
{/* content */}
<div className="p-6 overflow-y-auto max-h-[80vh] focus-visible:!outline-none">
<div className="py-2 px-6 overflow-y-auto max-h-[80vh] focus-visible:!outline-none">
{destroyOnClose && !open ? null : children}
</div>
@ -175,43 +200,13 @@ export const Modal: FC<ModalProps> = ({
);
};
// example usage
/*
import { Modal } from '@/components/ui/modal';
let modalIns = createPortalModal();
Modal.show = modalIns
? modalIns.show
: () => {
modalIns = createPortalModal();
return modalIns.show;
};
Modal.hide = modalIns.hide;
function Demo() {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(true)}>open modal</button>
<Modal
open={open}
onOpenChange={setOpen}
title="title"
footer={
<div className="flex gap-2">
<button onClick={() => setOpen(false)} className="px-4 py-2 border rounded-md">
cancel
</button>
<button onClick={() => setOpen(false)} className="px-4 py-2 bg-primary text-white rounded-md">
ok
</button>
</div>
}
>
<div className="py-4"></div>
</Modal>
<Modal
title={'modal-title'}
onOk={handleOk}
confirmLoading={loading}
destroyOnClose
>
<div className="py-4"></div>
</Modal>
</div>
);
}
*/
export { Modal };

View File

@ -8,7 +8,7 @@ const Table = React.forwardRef<
>(({ className, rootClassName, ...props }, ref) => (
<div
className={cn(
'relative w-full overflow-auto rounded-2xl bg-background-card',
'relative w-full overflow-auto rounded-2xl bg-bg-card',
rootClassName,
)}
>

View File

@ -1,9 +1,15 @@
import message from '@/components/ui/message';
import { ChatSearchParams } from '@/constants/chat';
import { IDialog } from '@/interfaces/database/chat';
import { IConversation, IDialog } from '@/interfaces/database/chat';
import { IAskRequestBody } from '@/interfaces/request/chat';
import { IClientConversation } from '@/pages/next-chats/chat/interface';
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
import { isConversationIdExist } from '@/pages/next-chats/utils';
import chatService from '@/services/next-chat-service ';
import { buildMessageListWithUuid, getConversationId } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { has } from 'lodash';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'umi';
@ -17,6 +23,13 @@ export const enum ChatApiAction {
RemoveDialog = 'removeDialog',
SetDialog = 'setDialog',
FetchDialog = 'fetchDialog',
FetchConversationList = 'fetchConversationList',
FetchConversation = 'fetchConversation',
UpdateConversation = 'updateConversation',
RemoveConversation = 'removeConversation',
DeleteMessage = 'deleteMessage',
FetchMindMap = 'fetchMindMap',
FetchRelatedQuestions = 'fetchRelatedQuestions',
}
export const useGetChatSearchParams = () => {
@ -74,11 +87,17 @@ export const useFetchDialogList = () => {
gcTime: 0,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await chatService.listDialog({
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
});
const { data } = await chatService.listDialog(
{
params: {
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
},
data: {},
},
true,
);
return data?.data ?? { dialogs: [], total: 0 };
},
@ -180,3 +199,227 @@ export const useFetchDialog = () => {
return { data, loading, refetch };
};
//#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 {
data,
isFetching: loading,
refetch,
} = useQuery<IConversation[]>({
queryKey: [ChatApiAction.FetchConversationList, id],
initialData: [],
gcTime: 0,
refetchOnWindowFocus: false,
enabled: !!id,
queryFn: async () => {
const { data } = await chatService.listConversation(
{ params: { dialog_id: id } },
true,
);
if (data.code === 0) {
if (data.data.length > 0) {
handleClickConversation(data.data[0].id, '');
} else {
handleClickConversation('', '');
}
}
return data?.data;
},
});
return { data, loading, refetch };
};
export const useFetchConversation = () => {
const { isNew, conversationId } = useGetChatSearchParams();
const { sharedId } = useGetSharedChatSearchParams();
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,
},
},
true,
);
const conversation = data?.data ?? {};
const messageList = buildMessageListWithUuid(conversation?.message);
return { ...conversation, message: messageList };
}
return { message: [] };
},
});
return { data, loading, refetch };
};
export const useUpdateConversation = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.UpdateConversation],
mutationFn: async (params: Record<string, any>) => {
const { data } = await chatService.setConversation({
...params,
conversation_id: params.conversation_id
? params.conversation_id
: getConversationId(),
});
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [ChatApiAction.FetchConversationList],
});
message.success(t(`message.modified`));
}
return data;
},
});
return { data, loading, updateConversation: mutateAsync };
};
export const useRemoveConversation = () => {
const queryClient = useQueryClient();
const { dialogId } = useGetChatSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.RemoveConversation],
mutationFn: async (conversationIds: string[]) => {
const { data } = await chatService.removeConversation({
conversationIds,
dialogId,
});
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [ChatApiAction.FetchConversationList],
});
}
return data.code;
},
});
return { data, loading, removeConversation: mutateAsync };
};
export const useDeleteMessage = () => {
const { conversationId } = useGetChatSearchParams();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.DeleteMessage],
mutationFn: async (messageId: string) => {
const { data } = await chatService.deleteMessage({
messageId,
conversationId,
});
if (data.code === 0) {
message.success(t(`message.deleted`));
}
return data.code;
},
});
return { data, loading, deleteMessage: mutateAsync };
};
//#endregion
//#region search page
export const useFetchMindMap = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.FetchMindMap],
gcTime: 0,
mutationFn: async (params: IAskRequestBody) => {
try {
const ret = await chatService.getMindMap(params);
return ret?.data?.data ?? {};
} catch (error: any) {
if (has(error, 'message')) {
message.error(error.message);
}
return [];
}
},
});
return { data, loading, fetchMindMap: mutateAsync };
};
export const useFetchRelatedQuestions = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [ChatApiAction.FetchRelatedQuestions],
gcTime: 0,
mutationFn: async (question: string): Promise<string[]> => {
const { data } = await chatService.getRelatedQuestions({ question });
return data?.data ?? [];
},
});
return { data, loading, fetchRelatedQuestions: mutateAsync };
};
//#endregion

View File

@ -1028,7 +1028,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
'30d': '30 days',
},
publish: 'API',
exeSQL: 'ExeSQL',
exeSQL: 'Execute SQL',
exeSQLDescription:
'A component that performs SQL queries on a relational database, supporting querying from MySQL, PostgreSQL, or MariaDB.',
dbType: 'Database Type',
@ -1376,5 +1376,8 @@ This delimiter is used to split the input text into several text pieces echo of
addMCP: 'Add MCP',
editMCP: 'Edit MCP',
},
search: {
createSearch: 'Create Search',
},
},
};

View File

@ -1310,4 +1310,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
okText: '确认',
cancelText: '取消',
},
search: {
createSearch: '新建查询',
},
};

View File

@ -83,12 +83,12 @@ function InnerAgentNode({
></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<section className="flex flex-col gap-2">
<div className={'bg-background-card rounded-sm p-1'}>
<div className={'bg-bg-card rounded-sm p-1'}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
{(isGotoMethod ||
exceptionMethod === AgentExceptionMethod.Comment) && (
<div className="bg-background-card rounded-sm p-1 flex justify-between gap-2">
<div className="bg-bg-card rounded-sm p-1 flex justify-between gap-2">
<span className="text-text-sub-title">On Failure</span>
<span className="truncate flex-1 text-right">
{t(`flow.${exceptionMethod}`)}

View File

@ -31,13 +31,13 @@ export function InnerCategorizeNode({
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<section className="flex flex-col gap-2">
<div className={'bg-background-card rounded-sm px-1'}>
<div className={'bg-bg-card rounded-sm px-1'}>
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
</div>
{positions.map((position) => {
return (
<div key={position.uuid}>
<div className={'bg-background-card rounded-sm p-1 truncate'}>
<div className={'bg-bg-card rounded-sm p-1 truncate'}>
{position.name}
</div>
<CommonHandle

View File

@ -42,7 +42,7 @@ function OperatorItemList({ operators }: OperatorItemProps) {
<TooltipTrigger asChild>
<DropdownMenuItem
key={x}
className="hover:bg-background-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
className="hover:bg-bg-card py-1 px-3 cursor-pointer rounded-sm flex gap-2 items-center justify-start"
onClick={addCanvasNode(x, {
nodeId,
id,

View File

@ -145,7 +145,7 @@ function EmbedDialog({
{t(isAgent ? 'flow' : 'chat', { keyPrefix: 'header' })}
<span className="ml-1 inline-block">ID</span>
</div>
<div className="bg-background-card rounded-lg flex justify-between p-2">
<div className="bg-bg-card rounded-lg flex justify-between p-2">
<span>{token} </span>
<CopyToClipboard text={token}></CopyToClipboard>
</div>

View File

@ -36,7 +36,7 @@ export function ToolCard({
<li
{...props}
className={cn(
'flex bg-background-card p-1 rounded-sm justify-between',
'flex bg-bg-card p-1 rounded-sm justify-between',
className,
)}
>

View File

@ -133,7 +133,7 @@ function ConditionCards({
},
)}
>
<section className="p-2 bg-background-card flex justify-between items-center">
<section className="p-2 bg-bg-card flex justify-between items-center">
<FormField
control={form.control}
name={`${name}.${index}.cpn_id`}

View File

@ -135,7 +135,7 @@ const ToolTimelineItem = ({
<Accordion
type="single"
collapsible
className="bg-background-card px-3"
className="bg-bg-card px-3"
>
<AccordionItem value={idx.toString()}>
<AccordionTrigger

View File

@ -254,7 +254,7 @@ export const WorkFlowTimeline = ({
<Accordion
type="single"
collapsible
className="bg-background-card px-3"
className="bg-bg-card px-3"
>
<AccordionItem value={idx.toString()}>
<AccordionTrigger

View File

@ -1,5 +1,5 @@
import MessageItem from '@/components/next-message-item';
import { Modal } from '@/components/ui/modal';
import { Modal } from '@/components/ui/modal/modal';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { IAgentLogMessage } from '@/interfaces/database/agent';

View File

@ -13,7 +13,7 @@ import {
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { Modal } from '@/components/ui/modal';
import { Modal } from '@/components/ui/modal/modal';
import Space from '@/components/ui/space';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';

View File

@ -61,7 +61,7 @@ export default ({
};
return (
<div className="flex pr-[25px]">
<div className="flex items-center gap-4 bg-background-card text-muted w-fit h-[35px] rounded-md px-4 py-2 text-base">
<div className="flex items-center gap-4 bg-bg-card text-muted-foreground w-fit h-[35px] rounded-md px-4 py-2">
{textSelectOptions.map((option) => (
<div
key={option.value}
@ -76,7 +76,7 @@ export default ({
</div>
<div className="ml-auto"></div>
<Input
className="bg-background-card text-muted-foreground"
className="bg-bg-card text-muted-foreground"
style={{ width: 200 }}
placeholder={t('search')}
icon={<SearchOutlined />}
@ -86,7 +86,7 @@ export default ({
<div className="w-[20px]"></div>
<Popover>
<PopoverTrigger asChild>
<Button className="bg-background-card text-muted-foreground hover:bg-card">
<Button className="bg-bg-card text-muted-foreground hover:bg-card">
<ListFilter />
</Button>
</PopoverTrigger>
@ -95,10 +95,7 @@ export default ({
</PopoverContent>
</Popover>
<div className="w-[20px]"></div>
<Button
onClick={() => createChunk()}
className="bg-background-card text-primary"
>
<Button onClick={() => createChunk()} className="bg-bg-card text-primary">
<Plus size={44} />
</Button>
</div>

View File

@ -98,13 +98,13 @@ export default function DatasetSettings() {
setCurrentTab(val);
}}
>
<TabsList className="grid bg-background grid-cols-2 rounded-none bg-[#161618]">
<TabsList className="grid bg-transparent grid-cols-2 rounded-none text-foreground">
<TabsTrigger
value="generalForm"
className="group bg-transparent p-0 !border-transparent"
>
<div className="flex w-full h-full justify-center items-center bg-[#161618]">
<span className="h-full group-data-[state=active]:border-b-2 border-white ">
<div className="flex w-full h-full justify-center items-center">
<span className="h-full group-data-[state=active]:border-b-2 border-foreground ">
General
</span>
</div>
@ -113,8 +113,8 @@ export default function DatasetSettings() {
value="chunkMethodForm"
className="group bg-transparent p-0 !border-transparent"
>
<div className="flex w-full h-full justify-center items-center bg-[#161618]">
<span className="h-full group-data-[state=active]:border-b-2 border-white ">
<div className="flex w-full h-full justify-center items-center">
<span className="h-full group-data-[state=active]:border-b-2 border-foreground ">
Chunk Method
</span>
</div>

View File

@ -84,7 +84,7 @@ export function SideBar({ refreshCount }: PropType) {
className={cn(
'w-full justify-start gap-2.5 px-3 relative h-10 text-text-sub-title-invert',
{
'bg-background-card': active,
'bg-bg-card': active,
'text-text-title': active,
},
)}

View File

@ -31,7 +31,7 @@ export function ChatCard({ data, showChatRenameModal }: IProps) {
</section>
<div className="flex justify-between items-end">
<div className="w-full">
<h3 className="text-lg font-semibold mb-2 line-clamp-1">
<h3 className="text-lg font-semibold mb-2 line-clamp-1 truncate">
{data.name}
</h3>
<p className="text-xs text-text-sub-title">{data.description}</p>

View File

@ -0,0 +1,23 @@
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { PropsWithChildren } from 'react';
import { ChatSettings } from './chat-settings';
export function ChatSettingSheet({ children }: PropsWithChildren) {
return (
<Sheet>
<SheetTrigger asChild>{children}</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Chat Settings</SheetTitle>
</SheetHeader>
<ChatSettings></ChatSettings>
</SheetContent>
</Sheet>
);
}

View File

@ -7,8 +7,9 @@ import { ChatModelSettings } from './chat-model-settings';
import { ChatPromptEngine } from './chat-prompt-engine';
import { useChatSettingSchema } from './use-chat-setting-schema';
export function AppSettings() {
export function ChatSettings() {
const formSchema = useChatSettingSchema();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@ -32,27 +33,19 @@ export function AppSettings() {
}
return (
<section className="py-6 w-[500px] max-w-[25%] ">
<div className="text-2xl font-bold mb-4 text-colors-text-neutral-strong px-6">
App settings
</div>
<div className="overflow-auto max-h-[81vh] px-6 ">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<ChatBasicSetting></ChatBasicSetting>
<ChatPromptEngine></ChatPromptEngine>
<ChatModelSettings></ChatModelSettings>
</form>
</FormProvider>
</div>
<div className="p-6 text-center">
<p className="text-colors-text-neutral-weak mb-1">
There are unsaved changes
</p>
<Button variant={'tertiary'} className="w-full">
Update
</Button>
</div>
<section className="py-6">
<FormProvider {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 overflow-auto max-h-[88vh] pr-4"
>
<ChatBasicSetting></ChatBasicSetting>
<ChatPromptEngine></ChatPromptEngine>
<ChatModelSettings></ChatModelSettings>
</form>
</FormProvider>
<Button className="w-full my-4">Update</Button>
</section>
);
}

View File

@ -1,9 +1,95 @@
import { ChatInput } from '@/components/chat-input';
import { NextMessageInput } from '@/components/message-input/next';
import MessageItem from '@/components/message-item';
import { MessageType } from '@/constants/chat';
import {
useFetchConversation,
useFetchDialog,
useGetChatSearchParams,
} from '@/hooks/use-chat-request';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import {
useGetSendButtonDisabled,
useSendButtonDisabled,
} from '../hooks/use-button-disabled';
import { useCreateConversationBeforeUploadDocument } from '../hooks/use-create-conversation';
import { useSendMessage } from '../hooks/use-send-chat-message';
import { buildMessageItemReference } from '../utils';
interface IProps {
controller: AbortController;
}
export function ChatBox({ controller }: IProps) {
const {
value,
scrollRef,
messageContainerRef,
sendLoading,
derivedMessages,
handleInputChange,
handlePressEnter,
regenerateMessage,
removeMessageById,
stopOutputMessage,
} = 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);
export function ChatBox() {
return (
<section className="border-x flex-1">
<ChatInput></ChatInput>
<section className="border-x flex flex-col p-5 w-full">
<div ref={messageContainerRef} className="flex-1 overflow-auto">
<div className="w-full">
{derivedMessages?.map((message, i) => {
return (
<MessageItem
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages.length - 1 === i
}
key={buildMessageUuidWithRole(message)}
item={message}
nickname={userInfo.nickname}
avatar={userInfo.avatar}
avatarDialog={currentDialog.icon}
reference={buildMessageItemReference(
{
message: derivedMessages,
reference: conversation.reference,
},
message,
)}
// clickDocumentButton={clickDocumentButton}
index={i}
removeMessageById={removeMessageById}
regenerateMessage={regenerateMessage}
sendLoading={sendLoading}
></MessageItem>
);
})}
</div>
<div ref={scrollRef} />
</div>
<NextMessageInput
disabled={disabled}
sendDisabled={sendDisabled}
sendLoading={sendLoading}
value={value}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
conversationId={conversationId}
createConversationBeforeUploadDocument={
createConversationBeforeUploadDocument
}
stopOutputMessage={stopOutputMessage}
/>
</section>
);
}

View File

@ -1,32 +1,48 @@
import { PageHeader } from '@/components/page-header';
import { Button } from '@/components/ui/button';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchDialog } from '@/hooks/use-chat-request';
import { EllipsisVertical } from 'lucide-react';
import { AppSettings } from './app-settings';
import { useTranslation } from 'react-i18next';
import { useHandleClickConversationCard } from '../hooks/use-click-card';
import { ChatBox } from './chat-box';
import { Sessions } from './sessions';
export default function Chat() {
const { navigateToChatList } = useNavigatePage();
useFetchDialog();
const { data } = useFetchDialog();
const { t } = useTranslation();
const { handleConversationCardClick, controller } =
useHandleClickConversationCard();
return (
<section className="h-full flex flex-col">
<PageHeader>
<div className="flex items-center gap-2">
<Button variant={'icon'} size={'icon'}>
<EllipsisVertical />
</Button>
<Button variant={'tertiary'} size={'sm'}>
Publish
</Button>
</div>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToChatList}>
{t('chat.chat')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{data.name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="flex flex-1">
<Sessions></Sessions>
<ChatBox></ChatBox>
<AppSettings></AppSettings>
<div className="flex flex-1 min-h-0">
<Sessions
handleConversationCardClick={handleConversationCardClick}
></Sessions>
<ChatBox controller={controller}></ChatBox>
</div>
</section>
);

View File

@ -0,0 +1,33 @@
import { IConversation, IReference, Message } from '@/interfaces/database/chat';
import { FormInstance } from 'antd';
export interface ISegmentedContentProps {
show: boolean;
form: FormInstance;
setHasError: (hasError: boolean) => void;
}
export interface IVariable {
temperature: number;
top_p: number;
frequency_penalty: number;
presence_penalty: number;
max_tokens: number;
}
export interface VariableTableDataType {
key: string;
variable: string;
optional: boolean;
}
export type IPromptConfigParameters = Omit<VariableTableDataType, 'variable'>;
export interface IMessage extends Message {
id: string;
reference?: IReference; // the latest news has reference
}
export interface IClientConversation extends IConversation {
message: IMessage[];
}

View File

@ -1,38 +1,60 @@
import { MoreButton } from '@/components/more-button';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { EllipsisVertical, Plus } from 'lucide-react';
import { useGetChatSearchParams } from '@/hooks/use-chat-request';
import { cn } from '@/lib/utils';
import { Plus } from 'lucide-react';
import { useCallback } from 'react';
import { useHandleClickConversationCard } from '../hooks/use-click-card';
import { useSelectDerivedConversationList } from '../hooks/use-select-conversation-list';
import { ChatSettingSheet } from './app-settings/chat-settings-sheet';
function SessionCard() {
return (
<Card className="bg-colors-background-inverse-weak border-colors-outline-neutral-standard">
<CardContent className="px-3 py-2 flex justify-between items-center">
xxx
<Button variant={'icon'} size={'icon'}>
<EllipsisVertical />
</Button>
</CardContent>
</Card>
type SessionProps = Pick<
ReturnType<typeof useHandleClickConversationCard>,
'handleConversationCardClick'
>;
export function Sessions({ handleConversationCardClick }: SessionProps) {
const { list: conversationList, addTemporaryConversation } =
useSelectDerivedConversationList();
const handleCardClick = useCallback(
(conversationId: string, isNew: boolean) => () => {
handleConversationCardClick(conversationId, isNew);
},
[handleConversationCardClick],
);
}
export function Sessions() {
const sessionList = new Array(10).fill(1);
const { conversationId } = useGetChatSearchParams();
return (
<section className="p-6 w-[400px] max-w-[20%]">
<section className="p-6 w-[400px] max-w-[20%] flex flex-col">
<div className="flex justify-between items-center mb-4">
<span className="text-colors-text-neutral-strong text-2xl font-bold">
Sessions
</span>
<Button variant={'icon'} size={'icon'}>
<span className="text-xl font-bold">Conversations</span>
<Button variant={'ghost'} onClick={addTemporaryConversation}>
<Plus></Plus>
</Button>
</div>
<div className="space-y-4">
{sessionList.map((x) => (
<SessionCard key={x.id}></SessionCard>
<div className="space-y-4 flex-1 overflow-auto">
{conversationList.map((x) => (
<Card
key={x.id}
onClick={handleCardClick(x.id, x.is_new)}
className={cn('cursor-pointer bg-transparent', {
'bg-bg-card': conversationId === x.id,
})}
>
<CardContent className="px-3 py-2 flex justify-between items-center group">
{x.name}
<MoreButton></MoreButton>
</CardContent>
</Card>
))}
</div>
<div className="py-2">
<ChatSettingSheet>
<Button className="w-full">Chat Settings</Button>
</ChatSettingSheet>
</div>
</section>
);
}

View File

@ -0,0 +1,14 @@
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 === '';
};
export const useSendButtonDisabled = (value: string) => {
return trim(value) === '';
};

View File

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

View File

@ -0,0 +1,29 @@
import { useGetChatSearchParams } from '@/hooks/use-chat-request';
import { useCallback } from 'react';
import {
useSetChatRouteParams,
useSetConversation,
} from './use-send-chat-message';
export const useCreateConversationBeforeUploadDocument = () => {
const { setConversation } = useSetConversation();
const { dialogId } = useGetChatSearchParams();
const { getConversationIsNew } = useSetChatRouteParams();
const createConversationBeforeUploadDocument = useCallback(
async (message: string) => {
const isNew = getConversationIsNew();
if (isNew === 'true') {
const data = await setConversation(message, true);
return data;
}
},
[setConversation, getConversationIsNew],
);
return {
createConversationBeforeUploadDocument,
dialogId,
};
};

View File

@ -0,0 +1,85 @@
import { ChatSearchParams, 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 { useCallback, useEffect, useMemo, useState } from 'react';
import { useParams, useSearchParams } from 'umi';
export const useFindPrologueFromDialogList = () => {
const { id: dialogId } = useParams();
const { data } = useFetchDialogList();
const prologue = useMemo(() => {
return data.dialogs.find((x) => x.id === dialogId)?.prompt_config.prologue;
}, [dialogId, data]);
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');
const [list, setList] = useState<Array<IConversation>>([]);
const { data: conversationList, loading } = useFetchConversationList();
const { id: dialogId } = useParams();
const { setNewConversationRouteParams } = useSetNewConversationRouteParams();
const prologue = useFindPrologueFromDialogList();
const addTemporaryConversation = useCallback(() => {
const conversationId = getConversationId();
setList((pre) => {
if (dialogId) {
setNewConversationRouteParams(conversationId, 'true');
const nextList = [
{
id: conversationId,
name: t('newConversation'),
dialog_id: dialogId,
is_new: true,
message: [
{
content: prologue,
role: MessageType.Assistant,
},
],
} as any,
...conversationList,
];
return nextList;
}
return pre;
});
}, [conversationList, dialogId, prologue, t, setNewConversationRouteParams]);
// When you first enter the page, select the top conversation card
useEffect(() => {
setList([...conversationList]);
}, [conversationList]);
return { list, addTemporaryConversation, loading };
};

View File

@ -0,0 +1,279 @@
import { ChatSearchParams, MessageType } from '@/constants/chat';
import {
useHandleMessageInputChange,
useRegenerateMessage,
useSelectDerivedMessages,
useSendMessageWithSse,
} from '@/hooks/logic-hooks';
import {
useFetchConversation,
useGetChatSearchParams,
useUpdateConversation,
} from '@/hooks/use-chat-request';
import { Message } from '@/interfaces/database/chat';
import api from '@/utils/api';
import { trim } from 'lodash';
import { useCallback, useEffect, useMemo } from 'react';
import { useParams, useSearchParams } from 'umi';
import { v4 as uuid } from 'uuid';
import { IMessage } from '../chat/interface';
import { useFindPrologueFromDialogList } from './use-select-conversation-list';
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 };
};
export const useSelectNextMessages = () => {
const {
scrollRef,
messageContainerRef,
setDerivedMessages,
derivedMessages,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
} = useSelectDerivedMessages();
const { data: conversation, loading } = useFetchConversation();
const { conversationId, isNew } = useGetChatSearchParams();
const { id: dialogId } = useParams();
const prologue = useFindPrologueFromDialogList();
const addPrologue = useCallback(() => {
if (dialogId !== '' && isNew === 'true') {
const nextMessage = {
role: MessageType.Assistant,
content: prologue,
id: uuid(),
} as IMessage;
setDerivedMessages([nextMessage]);
}
}, [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,
};
};
export const useSetConversation = () => {
const { id: dialogId } = useParams();
const { updateConversation } = useUpdateConversation();
const setConversation = useCallback(
async (
message: string,
isNew: boolean = false,
conversationId?: string,
) => {
const data = await updateConversation({
dialog_id: dialogId,
name: message,
is_new: isNew,
conversation_id: conversationId,
message: [
{
role: MessageType.Assistant,
content: message,
},
],
});
return data;
},
[updateConversation, dialogId],
);
return { setConversation };
};
export const useSendMessage = (controller: AbortController) => {
const { setConversation } = useSetConversation();
const { conversationId, isNew } = useGetChatSearchParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { send, answer, done } = useSendMessageWithSse(
api.completeConversation,
);
const {
scrollRef,
messageContainerRef,
derivedMessages,
loading,
addNewestAnswer,
addNewestQuestion,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
} = useSelectNextMessages();
const { setConversationIsNew, getConversationIsNew } =
useSetChatRouteParams();
const stopOutputMessage = useCallback(() => {
controller.abort();
}, [controller]);
const sendMessage = useCallback(
async ({
message,
currentConversationId,
messages,
}: {
message: Message;
currentConversationId?: string;
messages?: Message[];
}) => {
const res = await send(
{
conversation_id: currentConversationId ?? conversationId,
messages: [...(messages ?? derivedMessages ?? []), message],
},
controller,
);
if (res && (res?.response.status !== 200 || res?.data?.code !== 0)) {
// cancel loading
setValue(message.content);
console.info('removeLatestMessage111');
removeLatestMessage();
}
},
[
derivedMessages,
conversationId,
removeLatestMessage,
setValue,
send,
controller,
],
);
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,
});
useEffect(() => {
// #1289
if (answer.answer && conversationId && isNew !== 'true') {
addNewestAnswer(answer);
}
}, [answer, addNewestAnswer, conversationId, isNew]);
const handlePressEnter = useCallback(
(documentIds: string[]) => {
if (trim(value) === '') return;
const id = uuid();
addNewestQuestion({
content: value,
doc_ids: documentIds,
id,
role: MessageType.User,
});
if (done) {
setValue('');
handleSendMessage({
id,
content: value.trim(),
role: MessageType.User,
doc_ids: documentIds,
});
}
},
[addNewestQuestion, handleSendMessage, done, setValue, value],
);
return {
handlePressEnter,
handleInputChange,
value,
setValue,
regenerateMessage,
sendLoading: !done,
loading,
scrollRef,
messageContainerRef,
derivedMessages,
removeMessageById,
stopOutputMessage,
};
};

View File

@ -11,7 +11,8 @@ import { ChatCard } from './chat-card';
import { useRenameChat } from './hooks/use-rename-chat';
export default function ChatList() {
const { data, setPagination, pagination } = useFetchDialogList();
const { data, setPagination, pagination, handleInputChange, searchString } =
useFetchDialogList();
const { t } = useTranslation();
const {
initialChatName,
@ -36,7 +37,11 @@ export default function ChatList() {
return (
<section className="flex flex-col w-full flex-1">
<div className="px-8 pt-8">
<ListFilterBar title="Chat apps">
<ListFilterBar
title="Chat apps"
onSearchChange={handleInputChange}
searchString={searchString}
>
<Button onClick={handleShowCreateModal}>
<Plus className="size-2.5" />
{t('chat.createChat')}

View File

@ -1,4 +1,4 @@
import { Modal } from '@/components/ui/modal';
import { Modal } from '@/components/ui/modal/modal';
import { IModalProps } from '@/interfaces/common';
import DebugContent from '@/pages/agent/debug-content';
import { buildBeginInputListFromObject } from '@/pages/agent/form/begin-form/utils';

View File

@ -1,23 +1,73 @@
import ListFilterBar from '@/components/list-filter-bar';
import { Input } from '@/components/originui/input';
import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchFlowList } from '@/hooks/flow-hooks';
import { Plus } from 'lucide-react';
import { Plus, Search } from 'lucide-react';
import { useState } from 'react';
import { SearchCard } from './search-card';
export default function SearchList() {
const { data } = useFetchFlowList();
const { t } = useTranslate('search');
const [searchName, setSearchName] = useState('');
const handleSearchChange = (value: string) => {
console.log(value);
};
return (
<section>
<div className="px-8 pt-8">
<ListFilterBar title="Search apps">
<Button variant={'tertiary'} size={'sm'}>
<ListFilterBar
icon={
<div className="rounded-sm bg-emerald-400 bg-gradient-to-t from-emerald-400 via-emerald-400 to-emerald-200 p-1 size-6 flex justify-center items-center">
<Search size={14} className="font-bold m-auto" />
</div>
}
title="Search apps"
showFilter={false}
onSearchChange={(e) => handleSearchChange(e.target.value)}
>
<Button
variant={'default'}
onClick={() => {
Modal.show({
title: (
<div className="rounded-sm bg-emerald-400 bg-gradient-to-t from-emerald-400 via-emerald-400 to-emerald-200 p-1 size-6 flex justify-center items-center">
<Search size={14} className="font-bold m-auto" />
</div>
),
titleClassName: 'border-none',
footerClassName: 'border-none',
visible: true,
children: (
<div>
<div>{t('createSearch')}</div>
<div>name:</div>
<Input
defaultValue={searchName}
onChange={(e) => {
console.log(e.target.value, e);
setSearchName(e.target.value);
}}
/>
</div>
),
onOk: () => {
console.log('ok', searchName);
},
onVisibleChange: (e) => {
Modal.hide();
},
});
}}
>
<Plus className="mr-2 h-4 w-4" />
Create app
{t('createSearch')}
</Button>
</ListFilterBar>
</div>
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 max-h-[84vh] overflow-auto px-8">
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 max-h-[84vh] overflow-auto px-8">
{data.map((x) => {
return <SearchCard key={x.id} data={x}></SearchCard>;
})}

View File

@ -1,4 +1,5 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { MoreButton } from '@/components/more-button';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
@ -15,38 +16,40 @@ export function SearchCard({ data }: IProps) {
return (
<Card className="bg-colors-background-inverse-weak border-colors-outline-neutral-standard">
<CardContent className="p-4">
<CardContent className="p-4 flex gap-2 items-start group">
<div className="flex justify-between mb-4">
{data.avatar ? (
<div
className="w-[70px] h-[70px] rounded-xl bg-cover"
style={{ backgroundImage: `url(${data.avatar})` }}
/>
) : (
<Avatar className="w-[70px] h-[70px]">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
)}
<RAGFlowAvatar
className="w-[70px] h-[70px]"
avatar={data.avatar}
name={data.title}
/>
</div>
<div className="flex flex-col gap-1">
<section className="flex justify-between">
<div className="text-[20px] font-bold size-7 leading-5">
{data.title}
</div>
<MoreButton></MoreButton>
</section>
<div>An app that does things An app that does things</div>
<section className="flex justify-between">
<div>
Search app
<p className="text-sm opacity-80">
{formatPureDate(data.update_time)}
</p>
</div>
<div className="space-x-2 invisible group-hover:visible">
<Button variant="icon" size="icon" onClick={navigateToSearch}>
<ChevronRight className="h-6 w-6" />
</Button>
<Button variant="icon" size="icon">
<Trash2 />
</Button>
</div>
</section>
</div>
<h3 className="text-xl font-bold mb-2">{data.title}</h3>
<p>An app that does things An app that does things</p>
<section className="flex justify-between pt-3">
<div>
Search app
<p className="text-sm opacity-80">
{formatPureDate(data.update_time)}
</p>
</div>
<div className="space-x-2">
<Button variant="icon" size="icon" onClick={navigateToSearch}>
<ChevronRight className="h-6 w-6" />
</Button>
<Button variant="icon" size="icon">
<Trash2 />
</Button>
</div>
</section>
</CardContent>
</Card>
);

View File

@ -58,6 +58,28 @@ module.exports = {
'dot-green': 'var(--dot-green)',
'dot-red': 'var(--dot-red)',
/* design colors */
'bg-base': 'var(--bg-base)',
'bg-card': 'var(--bg-card)',
'text-primary': 'var(--text-primary)',
'text-disabled': 'var(--text-disabled)',
'text-input-tip': 'var(--text-input-tip)',
'border-default': 'var(--border-default)',
'border-accent': 'var(--border-accent)',
'border-button': 'var(--border-button)',
'accent-primary': 'var(--accent-primary)',
'bg-accent': 'var(--bg-accent)',
'state--success': 'var(--state--success)',
'state--warning': 'var(--state--warning)',
'state--error': 'var(--state--error)',
'team-group': 'var(--team-group)',
'team-member': 'var(--team-member)',
'team-department': 'var(--team-department)',
'bg-group': 'var(--bg-group)',
'bg-member': 'var(--bg-member)',
'bg-department': 'var(--bg-department)',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',

View File

@ -93,6 +93,38 @@
--input-border: rgba(22, 22, 24, 0.2);
--dot-green: rgba(59, 160, 92, 1);
--dot-red: rgba(216, 73, 75, 1);
/* design colors */
--bg-base: #f6f6f7;
/* card color , dividing line */
--bg-card: rgba(0, 0, 0, 0.05);
/* Button ,Body text, Input completed text */
--text-primary: #161618;
--text-secondary: #75787a;
--text-disabled: #b2b5b7;
/* input placeholder color */
--text-input-tip: #b2b5b7;
/* Input default color */
--border-default: rgba(0, 0, 0, 0.2);
/* Input accent color */
--border-accent: #000000;
--border-button: rgba(0, 0, 0, 0.1);
/* Regulators, parsing, switches, variables */
--accent-primary: #4ca4e7;
/* Output Variables Box */
--bg-accent: rgba(76, 164, 231, 0.05);
--state--success: #3ba05c;
--state--warning: #faad14;
--state-error: #d8494b;
--team-group: #5ab77e;
--team-member: #5c96c8;
--team-department: #8866d3;
--bg-group: rgba(90, 183, 126, 0.1);
--bg-member: rgba(92, 150, 200, 0.1);
--bg-department: rgba(136, 102, 211, 0.1);
}
.dark {
@ -207,6 +239,18 @@
--dot-green: rgba(59, 160, 92, 1);
--dot-red: rgba(216, 73, 75, 1);
/* design colors */
--bg-base: #161618;
--bg-card: rgba(255, 255, 255, 0.05);
--text-primary: #f6f6f7;
--text-secondary: #b2b5b7;
--text-disabled: #75787a;
--text-input-tip: #75787a;
--border-default: rgba(255, 255, 255, 0.2);
--border-accent: #ffffff;
--border-button: rgba(255, 255, 255, 0.1);
}
}