mirror of
https://github.com/dataelement/bisheng.git
synced 2026-10-31 02:57:37 +08:00
Merge branch 'feat/2.1.0' of https://github.com/dataelement/bisheng into feat/2.1.0
This commit is contained in:
@@ -401,10 +401,13 @@ async def chat_completions(
|
||||
extra['prompt'] = prompt
|
||||
message.extra = json.dumps(extra, ensure_ascii=False)
|
||||
ChatMessageDao.insert_one(message)
|
||||
except MessageException as e:
|
||||
error = True
|
||||
final_res = str(e)
|
||||
except Exception as e:
|
||||
logger.exception(f'Error in processing the prompt')
|
||||
error = True
|
||||
final_res = 'Error in processing the prompt'
|
||||
final_res = f'Error in processing the prompt: {str(e)[-100:]}'
|
||||
|
||||
if not error:
|
||||
messages = WorkStationService.get_chat_history(conversationId, 8)[:-1]
|
||||
|
||||
@@ -2,8 +2,6 @@ import json
|
||||
from queue import Queue
|
||||
from typing import Dict, Callable, List
|
||||
|
||||
from bisheng.utils import generate_uuid
|
||||
from bisheng_langchain.gpts.message_types import LiberalToolMessage
|
||||
from fastapi import WebSocket, status, Request
|
||||
from langchain_core.messages import AIMessage, HumanMessage, BaseMessage, ToolMessage
|
||||
from loguru import logger
|
||||
@@ -20,7 +18,9 @@ from bisheng.database.models.flow import FlowType
|
||||
from bisheng.database.models.message import ChatMessageDao, ChatMessage as ChatMessageModel
|
||||
from bisheng.database.models.session import MessageSession, MessageSessionDao
|
||||
from bisheng.settings import settings
|
||||
from bisheng.utils import generate_uuid
|
||||
from bisheng.utils.threadpool import thread_pool
|
||||
from bisheng_langchain.gpts.message_types import LiberalToolMessage
|
||||
|
||||
|
||||
class ChatClient:
|
||||
@@ -301,16 +301,7 @@ class ChatClient:
|
||||
_ = await self.add_message('bot', one.json(), 'tool_result')
|
||||
else:
|
||||
logger.warning("unexpected message type")
|
||||
# for one in result:
|
||||
# if isinstance(one, AIMessage):
|
||||
# answer += one.content
|
||||
|
||||
# todo: 后续优化代码解释器的实现方案,保证输出的文件可以公开访问 ugly solve
|
||||
# 获取minio的share地址,把share域名去掉, 为毕昇的部署方案特殊处理下
|
||||
for one in self.gpts_agent.tools:
|
||||
if one.name == "bisheng_code_interpreter":
|
||||
minio_share = settings.get_minio_conf().sharepoint
|
||||
answer = answer.replace(f"http://{minio_share}", "")
|
||||
answer_end_type = 'end'
|
||||
# 如果是流式的llm则用end_cover结束, 覆盖之前流式的输出
|
||||
if getattr(self.gpts_agent.llm, 'streaming', False):
|
||||
|
||||
@@ -134,6 +134,11 @@ class BishengEmbedding(BaseModel, Embeddings):
|
||||
params.update({
|
||||
'model': model_info.model_name,
|
||||
})
|
||||
|
||||
# 非openai官方但是符合openai接口标准的embedding模型,强制chunk_size=1,防止batch_size太大导致服务报错
|
||||
if self.llm_node_type.get(server_info.type) == "OpenAIEmbeddings":
|
||||
params['chunk_size'] = params.pop('chunk_size', 1)
|
||||
|
||||
if server_info.type == LLMServerType.QWEN.value:
|
||||
params = {
|
||||
'dashscope_api_key': params.get('openai_api_key'),
|
||||
@@ -150,9 +155,6 @@ class BishengEmbedding(BaseModel, Embeddings):
|
||||
LLMServerType.VLLM.value
|
||||
]:
|
||||
params['openai_api_key'] = params.pop('openai_api_key', None) or 'EMPTY'
|
||||
params['chunk_size'] = params.pop('chunk_size', 1)
|
||||
elif server_info.type == LLMServerType.OPENAI.value:
|
||||
params['chunk_size'] = params.pop('chunk_size', 1)
|
||||
elif server_info.type == LLMServerType.OLLAMA.value:
|
||||
params['query_instruction'] = 'passage: '
|
||||
return params
|
||||
|
||||
@@ -117,67 +117,6 @@ class CustomChatOllamaWithReasoning(ChatOllama):
|
||||
)
|
||||
yield chunk
|
||||
|
||||
def _generate(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||
**kwargs
|
||||
) -> Any:
|
||||
"""Handle non-streaming calls, extract think content to reasoning_content field."""
|
||||
result = super()._generate(messages, stop=stop, run_manager=run_manager, **kwargs)
|
||||
self._process_generations(result.generations, is_async=False)
|
||||
return result
|
||||
|
||||
async def _agenerate(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
||||
**kwargs
|
||||
) -> ChatResult:
|
||||
"""Handle async non-streaming calls, extract think content to reasoning_content field."""
|
||||
result = await super()._agenerate(messages, stop=stop, run_manager=run_manager, **kwargs)
|
||||
self._process_generations(result.generations, is_async=True)
|
||||
return result
|
||||
|
||||
def _stream(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
||||
**kwargs
|
||||
) -> Iterator[ChatGenerationChunk]:
|
||||
"""Process streaming output, modifying chunks to add reasoning_content field."""
|
||||
logger.debug("CustomChatOllamaWithReasoning._stream called")
|
||||
self._reset_think_state()
|
||||
|
||||
try:
|
||||
for chunk in super()._stream(messages, stop=stop, run_manager=run_manager, **kwargs):
|
||||
yield from self._process_stream_chunk(chunk)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in CustomChatOllamaWithReasoning stream processing: {e}")
|
||||
raise
|
||||
|
||||
async def _astream(
|
||||
self,
|
||||
messages: List[BaseMessage],
|
||||
stop: Optional[List[str]] = None,
|
||||
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
|
||||
**kwargs
|
||||
) -> AsyncIterator[ChatGenerationChunk]:
|
||||
"""Process async streaming output, modifying chunks to add reasoning_content field."""
|
||||
logger.debug("CustomChatOllamaWithReasoning._astream called")
|
||||
self._reset_think_state()
|
||||
|
||||
try:
|
||||
async for chunk in super()._astream(messages, stop=stop, run_manager=run_manager, **kwargs):
|
||||
for processed_chunk in self._process_stream_chunk(chunk):
|
||||
yield processed_chunk
|
||||
except Exception as e:
|
||||
logger.error(f"Error in CustomChatOllamaWithReasoning async stream processing: {e}")
|
||||
raise
|
||||
|
||||
# Helper methods for better code organization
|
||||
|
||||
def _reset_think_state(self) -> None:
|
||||
@@ -243,238 +182,3 @@ class CustomChatOllamaWithReasoning(ChatOllama):
|
||||
),
|
||||
generation_info=generation_info,
|
||||
)
|
||||
|
||||
def _process_generations(self, generations: List, is_async: bool = False) -> None:
|
||||
"""Process generations to extract think content."""
|
||||
if not generations:
|
||||
return
|
||||
|
||||
async_prefix = "Async " if is_async else ""
|
||||
|
||||
for generation in generations:
|
||||
try:
|
||||
self._process_generation_message(generation, async_prefix)
|
||||
self._process_generation_text(generation, async_prefix)
|
||||
except Exception as e:
|
||||
logger.error(f"{async_prefix}Error processing generation: {e}")
|
||||
|
||||
def _process_generation_message(self, generation, async_prefix: str) -> None:
|
||||
"""Process generation message content."""
|
||||
if not (hasattr(generation, 'message') and
|
||||
hasattr(generation.message, 'content')):
|
||||
return
|
||||
|
||||
original_content = generation.message.content
|
||||
reasoning_content, cleaned_content = self._extract_think_content(original_content)
|
||||
|
||||
generation.message.content = cleaned_content
|
||||
|
||||
if not hasattr(generation.message, 'additional_kwargs'):
|
||||
generation.message.additional_kwargs = {}
|
||||
generation.message.additional_kwargs['reasoning_content'] = reasoning_content or None
|
||||
|
||||
logger.debug(
|
||||
f"{async_prefix}Original content length: {len(original_content)}, "
|
||||
f"reasoning length: {len(reasoning_content)}, "
|
||||
f"processed length: {len(cleaned_content)}"
|
||||
)
|
||||
|
||||
def _process_generation_text(self, generation, async_prefix: str) -> None:
|
||||
"""Process generation text content."""
|
||||
if not hasattr(generation, 'text'):
|
||||
return
|
||||
|
||||
original_text = generation.text
|
||||
reasoning_content, cleaned_text = self._extract_think_content(original_text)
|
||||
generation.text = cleaned_text
|
||||
|
||||
logger.debug(
|
||||
f"{async_prefix}Original text length: {len(original_text)}, "
|
||||
f"processed length: {len(cleaned_text)}"
|
||||
)
|
||||
|
||||
def _extract_think_content(self, content: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Extract think tag content and return (reasoning_content, cleaned_content).
|
||||
|
||||
Args:
|
||||
content: Original content with potential think tags
|
||||
|
||||
Returns:
|
||||
Tuple of (reasoning_content, cleaned_content)
|
||||
"""
|
||||
if not content:
|
||||
return "", ""
|
||||
|
||||
# Extract content inside think tags
|
||||
think_matches = self.THINK_PATTERN.findall(content)
|
||||
reasoning_content = '\n'.join(think_matches).strip() if think_matches else ''
|
||||
|
||||
# Remove entire <think>...</think> blocks
|
||||
cleaned_content = self.THINK_REMOVAL_PATTERN.sub('', content).strip()
|
||||
|
||||
return reasoning_content, cleaned_content
|
||||
|
||||
def _process_stream_chunk(self, chunk: ChatGenerationChunk) -> Iterator[ChatGenerationChunk]:
|
||||
"""Process a single stream chunk and yield modified chunks."""
|
||||
if not (hasattr(chunk, 'message') and hasattr(chunk.message, 'content')):
|
||||
# Add reasoning_content field for chunks without content
|
||||
self._ensure_reasoning_content_field(chunk)
|
||||
yield chunk
|
||||
return
|
||||
|
||||
content = chunk.message.content or ""
|
||||
|
||||
if not content:
|
||||
self._ensure_reasoning_content_field(chunk)
|
||||
yield chunk
|
||||
return
|
||||
|
||||
logger.debug(f"Processing chunk content: '{content}', in_think state: {self.in_think}")
|
||||
|
||||
# Process content with think tags
|
||||
modified_chunks = self._process_chunk_with_think_tags(chunk, content)
|
||||
|
||||
# Yield all modified chunks
|
||||
yield from modified_chunks
|
||||
|
||||
def _ensure_reasoning_content_field(self, chunk: ChatGenerationChunk) -> None:
|
||||
"""Ensure chunk has reasoning_content field set to None."""
|
||||
if not hasattr(chunk, 'message') or not chunk.message:
|
||||
return
|
||||
|
||||
if not hasattr(chunk.message, 'additional_kwargs'):
|
||||
chunk.message.additional_kwargs = {}
|
||||
chunk.message.additional_kwargs['reasoning_content'] = None
|
||||
|
||||
def _process_chunk_with_think_tags(
|
||||
self,
|
||||
original_chunk: ChatGenerationChunk,
|
||||
content: str
|
||||
) -> List[ChatGenerationChunk]:
|
||||
"""
|
||||
Process chunk containing think tags and return list of modified chunks.
|
||||
|
||||
Args:
|
||||
original_chunk: Original chunk from parent stream
|
||||
content: Text content of the chunk
|
||||
|
||||
Returns:
|
||||
List of modified chunks with proper content/reasoning_content fields
|
||||
"""
|
||||
chunks = []
|
||||
current_pos = 0
|
||||
|
||||
while current_pos < len(content):
|
||||
if not self.in_think:
|
||||
current_pos = self._process_outside_think_tags(
|
||||
chunks, original_chunk, content, current_pos
|
||||
)
|
||||
else:
|
||||
current_pos = self._process_inside_think_tags(
|
||||
chunks, original_chunk, content, current_pos
|
||||
)
|
||||
|
||||
if current_pos < 0: # Break condition
|
||||
break
|
||||
|
||||
return chunks
|
||||
|
||||
def _process_outside_think_tags(
|
||||
self,
|
||||
chunks: List[ChatGenerationChunk],
|
||||
original_chunk: ChatGenerationChunk,
|
||||
content: str,
|
||||
current_pos: int
|
||||
) -> int:
|
||||
"""Process content when outside think tags."""
|
||||
think_start = content.find(self.THINK_START_TAG, current_pos)
|
||||
|
||||
if think_start != -1:
|
||||
# Found <think> tag, output content before it
|
||||
before_think = content[current_pos:think_start]
|
||||
if before_think:
|
||||
chunk = self._create_content_chunk(original_chunk, before_think)
|
||||
chunks.append(chunk)
|
||||
|
||||
# Enter think state, skip <think> tag and optional newline
|
||||
new_pos = think_start + len(self.THINK_START_TAG)
|
||||
if new_pos < len(content) and content[new_pos] == '\n':
|
||||
new_pos += 1
|
||||
|
||||
self.in_think = True
|
||||
return new_pos
|
||||
else:
|
||||
# No <think> tag found, output remaining content
|
||||
remaining = content[current_pos:]
|
||||
if remaining:
|
||||
chunk = self._create_content_chunk(original_chunk, remaining)
|
||||
chunks.append(chunk)
|
||||
return -1 # Break condition
|
||||
|
||||
def _process_inside_think_tags(
|
||||
self,
|
||||
chunks: List[ChatGenerationChunk],
|
||||
original_chunk: ChatGenerationChunk,
|
||||
content: str,
|
||||
current_pos: int
|
||||
) -> int:
|
||||
"""Process content when inside think tags."""
|
||||
think_end = content.find(self.THINK_END_TAG, current_pos)
|
||||
|
||||
if think_end != -1:
|
||||
# Found </think> tag, output reasoning content
|
||||
reasoning = content[current_pos:think_end]
|
||||
if reasoning:
|
||||
chunk = self._create_reasoning_chunk(original_chunk, reasoning)
|
||||
chunks.append(chunk)
|
||||
|
||||
# Exit think state, skip </think> tag and optional newline
|
||||
new_pos = think_end + len(self.THINK_END_TAG)
|
||||
if new_pos < len(content) and content[new_pos] == '\n':
|
||||
new_pos += 1
|
||||
|
||||
self.in_think = False
|
||||
return new_pos
|
||||
else:
|
||||
# No </think> tag found, output remaining reasoning content
|
||||
remaining_reasoning = content[current_pos:]
|
||||
if remaining_reasoning:
|
||||
chunk = self._create_reasoning_chunk(original_chunk, remaining_reasoning)
|
||||
chunks.append(chunk)
|
||||
return -1 # Break condition
|
||||
|
||||
def _create_content_chunk(
|
||||
self,
|
||||
original_chunk: ChatGenerationChunk,
|
||||
content: str
|
||||
) -> ChatGenerationChunk:
|
||||
"""Create a chunk for regular content."""
|
||||
return ChatGenerationChunk(
|
||||
message=AIMessageChunk(
|
||||
content=content,
|
||||
additional_kwargs={'reasoning_content': None}
|
||||
),
|
||||
generation_info=original_chunk.generation_info
|
||||
)
|
||||
|
||||
def _create_reasoning_chunk(
|
||||
self,
|
||||
original_chunk: ChatGenerationChunk,
|
||||
reasoning_content: str
|
||||
) -> ChatGenerationChunk:
|
||||
"""Create a chunk for reasoning content."""
|
||||
return ChatGenerationChunk(
|
||||
message=AIMessageChunk(
|
||||
content="",
|
||||
additional_kwargs={'reasoning_content': reasoning_content}
|
||||
),
|
||||
generation_info=original_chunk.generation_info
|
||||
)
|
||||
|
||||
# Deprecated method kept for backward compatibility
|
||||
def _remove_think_tags(self, content: str) -> str:
|
||||
"""Remove think tags, keeping only content outside tags. (Deprecated)"""
|
||||
logger.warning("_remove_think_tags is deprecated, use _extract_think_content instead")
|
||||
_, cleaned_content = self._extract_think_content(content)
|
||||
return cleaned_content
|
||||
|
||||
@@ -78,6 +78,8 @@ class MinioConf(BaseModel):
|
||||
cert_check: Optional[bool] = Field(default=False, description="是否校验证书")
|
||||
endpoint: Optional[str] = Field(default="127.0.0.1:9000", description="minio 地址")
|
||||
sharepoint: Optional[str] = Field(default="127.0.0.1:9000", description="minio 公开访问地址")
|
||||
share_schema: Optional[bool] = Field(default=False, description="minio 公开访问地址是否使用https")
|
||||
share_cert_check: Optional[bool] = Field(default=False, description="minio 公开访问地址是否校验证书")
|
||||
access_key: Optional[str] = Field(default="minioadmin", description="minio 用户名")
|
||||
secret_key: Optional[str] = Field(default="minioadmin", description="minio 密码")
|
||||
public_bucket: Optional[str] = Field(default="bisheng",
|
||||
|
||||
@@ -32,8 +32,8 @@ class MinioClient:
|
||||
endpoint=_MinioConf.sharepoint,
|
||||
access_key=_MinioConf.access_key,
|
||||
secret_key=_MinioConf.secret_key,
|
||||
secure=_MinioConf.schema,
|
||||
cert_check=_MinioConf.cert_check)
|
||||
secure=_MinioConf.share_schema,
|
||||
cert_check=_MinioConf.share_cert_check)
|
||||
|
||||
self._init_bucket_conf()
|
||||
|
||||
@@ -165,7 +165,7 @@ class MinioClient:
|
||||
获取minio share host
|
||||
"""
|
||||
minio_share = _MinioConf.sharepoint
|
||||
if _MinioConf.schema:
|
||||
if _MinioConf.share_schema:
|
||||
return f'https://{minio_share}'
|
||||
return f'http://{minio_share}'
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ class BaseExecutor(ABC):
|
||||
endpoint=self.minio.get('sharepoint'),
|
||||
access_key=self.minio.get('access_key'),
|
||||
secret_key=self.minio.get('secret_key'),
|
||||
secure=self.minio.get('schema'),
|
||||
cert_check=self.minio.get('cert_check'),
|
||||
secure=self.minio.get('share_schema', False),
|
||||
cert_check=self.minio.get('share_cert_check', False),
|
||||
)
|
||||
bucket = self.minio.get('tmp_bucket', 'tmp-dir')
|
||||
logger.debug(
|
||||
@@ -54,4 +54,4 @@ class BaseExecutor(ABC):
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -88,7 +88,6 @@ export const ChatToolDown = ({ linsi, tools, setTools, config, searchType, setSe
|
||||
|
||||
const LinsiTools = ({ tools, setTools }) => {
|
||||
const { data: bsConfig } = useGetBsConfig()
|
||||
const [building] = useModelBuilding()
|
||||
|
||||
useEffect(() => {
|
||||
const defaultTools = [{
|
||||
@@ -101,7 +100,7 @@ const LinsiTools = ({ tools, setTools }) => {
|
||||
id: 'knowledge',
|
||||
name: '个人知识库',
|
||||
icon: <Pencil size="16" />,
|
||||
checked: !building
|
||||
checked: true
|
||||
},]
|
||||
if (bsConfig) {
|
||||
const tools = bsConfig.linsightConfig?.tools || []
|
||||
@@ -115,7 +114,7 @@ const LinsiTools = ({ tools, setTools }) => {
|
||||
setTools((tools) => [...defaultTools, ...newTools])
|
||||
}
|
||||
|
||||
}, [bsConfig, building])
|
||||
}, [bsConfig])
|
||||
|
||||
|
||||
const active = useMemo(() => tools.some(tool => tool.checked), [tools])
|
||||
@@ -136,7 +135,6 @@ const LinsiTools = ({ tools, setTools }) => {
|
||||
</div>
|
||||
<Switch className='data-[state=checked]:bg-blue-600'
|
||||
checked={tool.checked}
|
||||
disabled={tool.id === 'knowledge' && building}
|
||||
onCheckedChange={val =>
|
||||
setTools(tools.map(t => t.id === tool.id ? { ...t, checked: val } : t))
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export const getKnowledgeColumns = (
|
||||
return '状态';
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return ['', '处理中', '成功', '失败'][row.original.status]
|
||||
return ['', '处理中', '成功', '失败', '模型向量切换中'][row.original.status]
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -60,7 +60,7 @@ export const ErrorMessage = ({
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className={cn(
|
||||
'rounded-xl border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200',
|
||||
'rounded-xl mt-2 border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-gray-600 dark:text-gray-200',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -103,7 +103,7 @@ const Error = ({ text }: { text: string }) => {
|
||||
const localize = useLocalize();
|
||||
const jsonString = extractJson(text);
|
||||
const errorMessage = text.length > 512 && !jsonString ? text.slice(0, 512) + '...' : text;
|
||||
const defaultResponse = `Something went wrong. Here's the specific error message we encountered: ${errorMessage}`;
|
||||
const defaultResponse = `${errorMessage}`;
|
||||
|
||||
if (!isJson(jsonString)) {
|
||||
return defaultResponse;
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ChatMessages({ useName, title, logo }) {
|
||||
const remark = chatState?.flow?.guide_word
|
||||
|
||||
|
||||
return <div ref={messageScrollRef} className="h-full overflow-y-auto scrollbar-hide pt-12 pb-96 px-4">
|
||||
return <div id="messageScrollPanne" ref={messageScrollRef} className="h-full overflow-y-auto scrollbar-hide pt-12 pb-96 px-4">
|
||||
{/* 助手开场白 */}
|
||||
{remark && <MessageRemark
|
||||
logo={logo}
|
||||
|
||||
@@ -4,7 +4,10 @@ export default function ChatFile({ fileName, filePath }) {
|
||||
|
||||
// download file
|
||||
const handleDownloadFile = (filePath) => {
|
||||
filePath && downloadFile(filePath, fileName)
|
||||
if (filePath) {
|
||||
const path = filePath.replace(/https?:\/\/[^\/]+/, __APP_ENV__.BASE_URL)
|
||||
downloadFile(path, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
return <div
|
||||
|
||||
@@ -64,9 +64,9 @@ export default function MessageBs({ logo, title, data, onUnlike = () => { }, onS
|
||||
<div className="w-fit group max-w-[90%]">
|
||||
<ReasoningLog loading={!data.end && (data.reasoning_log || reasoningLog)} msg={data.reasoning_log || reasoningLog} />
|
||||
{!(data.reasoning_log && !message && !data.files.length) && <>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<div className="flex gap-2 items-center mb-1 ml-2">
|
||||
{data.sender ? <p className="text-gray-600 text-xs">{data.sender}</p> : <p />}
|
||||
<div className={`text-right group-hover:opacity-100 opacity-0`}>
|
||||
<div className={`group-hover:opacity-100 opacity-0`}>
|
||||
<span className="text-slate-400 text-sm">{formatStrTime(data.create_time, 'MM 月 dd 日 HH:mm')}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,7 +92,7 @@ export default function MessageBs({ logo, title, data, onUnlike = () => { }, onS
|
||||
</>}
|
||||
{/* 附加信息 */}
|
||||
{
|
||||
data.end && <div className="flex justify-between mt-2">
|
||||
data.end && <div className="flex justify-between">
|
||||
<MessageSource
|
||||
extra={data.extra || {}}
|
||||
end={data.end}
|
||||
|
||||
@@ -35,11 +35,10 @@ export default function MessageBsChoose({ type = 'choose', logo, data, flow }: {
|
||||
|
||||
// input
|
||||
const textRef = useRef(null)
|
||||
const [inputSended, setInputSended] = useState(!!data.message.hisValue || false)
|
||||
const inputSended = useMemo(() => !!data.message.hisValue || false, [data.message.hisValue])
|
||||
const handleSend = () => {
|
||||
const val = textRef.current.value
|
||||
if (!val.trim()) return
|
||||
setInputSended(true)
|
||||
emitAreaTextEvent({
|
||||
action: EVENT_TYPE.MESSAGE_INPUT, data: {
|
||||
nodeId: data.message.node_id,
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function MessageUser({ useName, data, showButton }) {
|
||||
|
||||
return <div className="flex w-full">
|
||||
<div className="w-fit group min-h-8 max-w-[90%]">
|
||||
<div className="flex justify-end items-center mb-2 gap-2">
|
||||
<div className="flex justify-start items-center gap-2 ml-4">
|
||||
<div className={`text-right group-hover:opacity-100 opacity-0`}>
|
||||
<span className="text-slate-400 text-sm">{formatStrTime(data.create_time, 'MM 月 dd 日 HH:mm')}</span>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,9 @@ import { Chat } from "~/@types/chat"
|
||||
import { baseMsgItem } from "~/api/apps"
|
||||
import { formatDate, generateUUID } from "~/utils"
|
||||
import { FLOW_TYPES } from "."
|
||||
import { SkillMethod } from "./appUtils/skillMethod"
|
||||
import { bishengConfState, chatIdState, chatsState, currentChatState, currentRunningState, runningState } from "./store/atoms"
|
||||
import { emitAreaTextEvent, EVENT_TYPE } from "./useAreaText"
|
||||
import { SkillMethod } from "./appUtils/skillMethod"
|
||||
|
||||
export default function useChatHelpers() {
|
||||
const chatState = useRecoilValue(currentChatState)
|
||||
@@ -171,7 +171,7 @@ export default function useChatHelpers() {
|
||||
// 兼容后端问题
|
||||
const _files = Array.isArray(files) ? files : []
|
||||
|
||||
const messageId = message_id || (category === "guide_word" ? generateUUID(4) : "")
|
||||
const messageId = message_id || (category === "guide_word" ? 'u-' + generateUUID(6) : "")
|
||||
const filteredMessages = deduplicateMessages(messages, message_id)
|
||||
|
||||
return [
|
||||
@@ -184,7 +184,7 @@ export default function useChatHelpers() {
|
||||
files: _files.map(el => ({
|
||||
// 兼容
|
||||
file_name: el.file_name || el.name,
|
||||
file_url: el.file_url || el.url
|
||||
file_url: el.file_url || el.url || el.path
|
||||
})),
|
||||
is_bot,
|
||||
message: msg,
|
||||
@@ -321,13 +321,40 @@ export default function useChatHelpers() {
|
||||
{
|
||||
...baseMsgItem,
|
||||
category: "question",
|
||||
id: generateUUID(8),
|
||||
id: 'u-' + generateUUID(8),
|
||||
message: msg,
|
||||
create_time: formatDate(new Date(), "yyyy-MM-ddTHH:mm:ss"),
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
// 滚动到底部
|
||||
const dom = document.getElementById('messageScrollPanne')
|
||||
setTimeout(() => {
|
||||
if (dom) {
|
||||
dom.scrollTop = dom.scrollHeight
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
closeOutputMsg: (input) => {
|
||||
setChats((prev) =>
|
||||
updateChatMessages(prev, chatId, (messages) => {
|
||||
const updatedMessages = messages.map(msg => {
|
||||
if (["output_with_input_msg", "output_with_choose_msg"].includes(msg.category)) {
|
||||
return {
|
||||
...msg,
|
||||
message: {
|
||||
...msg.message,
|
||||
hisValue: input
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
return updatedMessages
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -47,6 +47,9 @@ const useLoadMessage = (chatId: string, chatState: any, messageScrollRef: React.
|
||||
const loadMore = async (chatId: string) => {
|
||||
// 运行中or没有更多 || 最后一条消息id不存在,忽略
|
||||
if (running || historyEnd || !messages?.[0]?.id || !flow) return
|
||||
const messageId = messages[0].id
|
||||
// u-开头的消息表示新建会话,新建会话无需加载历史消息
|
||||
if (typeof messageId === 'string' && messageId.startsWith('u-')) return
|
||||
|
||||
const msgs = await getChatHistoryApi(flow.id, chatId, flow.flow_type, messages[0].id || 0)
|
||||
setChats((prev) => {
|
||||
|
||||
@@ -325,6 +325,8 @@ export const useWebSocket = (helpers) => {
|
||||
}
|
||||
},
|
||||
})
|
||||
// 闭合所有输出节点
|
||||
helpers.message.closeOutputMsg(submitData.data.data.output_result)
|
||||
break;
|
||||
case ActionType.STOP:
|
||||
sendWsMsg({ action: 'stop' })
|
||||
|
||||
@@ -63,7 +63,7 @@ export function AgentCard({
|
||||
{/* 2. 描述区域:占满剩余空间,最多显示 2 行(适配 150px 高度) */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<p className="text-sm text-[#64748b] leading-5 break-words line-clamp-2"> {/* 文字缩小为 xs,最多 2 行 */}
|
||||
{agent.description || "无描述信息"} {/* 空描述兜底,避免高度塌陷 */}
|
||||
{agent.description} {/* 空描述兜底,避免高度塌陷 */}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ChevronDown, Loader2 } from "lucide-react"
|
||||
import type React from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useState, useCallback } from "react"
|
||||
import { getChatOnlineApi, getFrequently, getHomeLabelApi, getUncategorized } from "~/api/apps"
|
||||
import { Button } from "~/components"
|
||||
import { AgentCard } from "./AgentCard"
|
||||
@@ -26,6 +26,7 @@ interface AgentGridProps {
|
||||
sectionRefs: React.MutableRefObject<Record<string, HTMLElement | null>>
|
||||
refreshTrigger: number
|
||||
onCardClick: (agent: Agent) => void
|
||||
onSectionMounted: (id: string, element: HTMLElement | null) => void // 新增回调函数
|
||||
}
|
||||
|
||||
// 分类标签类型
|
||||
@@ -49,7 +50,8 @@ export function AgentGrid({
|
||||
onRemoveFromFavorites,
|
||||
sectionRefs,
|
||||
refreshTrigger,
|
||||
onCardClick
|
||||
onCardClick,
|
||||
onSectionMounted // 新增回调函数
|
||||
}: AgentGridProps) {
|
||||
const pageSize = 8 // 固定单页容量
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
@@ -435,11 +437,11 @@ const handleAddToFavorites = async (type: number, id: string) => {
|
||||
return (
|
||||
<section
|
||||
key={id}
|
||||
id={id}
|
||||
className="relative"
|
||||
ref={(el) => {
|
||||
ref={(el) => {
|
||||
sectionRefs.current[id] = el
|
||||
}}
|
||||
>
|
||||
}} >
|
||||
{/* 分区标题 */}
|
||||
<h2 className="text-base font-medium mb-4 text-blue-600">{name}</h2>
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export function SearchOverlay({
|
||||
agent={agent}
|
||||
isFavorite={favorites ? favorites.includes(agent.id.toString()) : false}
|
||||
showRemove={false}
|
||||
onAddToFavorites={() => onAddToFavorites(agent.id.toString())}
|
||||
onAddToFavorites={() => onAddToFavorites(agent.flow_type,agent.id.toString())}
|
||||
onRemoveFromFavorites={() => onRemoveFromFavorites(agent.id.toString())}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -110,24 +110,56 @@ export default function AgentCenter() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchChange = async (query: string) => {
|
||||
if (query.trim()) {
|
||||
setIsSearching(true)
|
||||
setSearchLoading(true)
|
||||
try {
|
||||
const result = await getChatOnlineApi(1, query, -1, true)
|
||||
setSearchResults(result.data || [])
|
||||
} catch (error) {
|
||||
console.error("搜索失败:", error)
|
||||
setSearchResults([])
|
||||
} finally {
|
||||
setSearchLoading(false)
|
||||
// 修改handleSearchChange函数,实现多页数据加载
|
||||
const handleSearchChange = async (query: string) => {
|
||||
if (query.trim()) {
|
||||
setIsSearching(true);
|
||||
setSearchLoading(true);
|
||||
let allResults: any[] = []; // 存储所有页的结果
|
||||
let currentPage = 1;
|
||||
const pageSize = 8; // 每页条数(和接口保持一致)
|
||||
|
||||
try {
|
||||
// 循环加载所有页数据
|
||||
while (true) {
|
||||
// 调用接口,禁用默认限制(或按实际需要调整)
|
||||
const result = await getChatOnlineApi(
|
||||
currentPage,
|
||||
query,
|
||||
-1,
|
||||
true // 禁用默认限制,或根据接口逻辑调整
|
||||
);
|
||||
|
||||
const pageData = result.data || [];
|
||||
allResults = [...allResults, ...pageData];
|
||||
|
||||
// 终止条件:当前页数据不足一页,说明已加载完所有数据
|
||||
if (pageData.length < pageSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentPage++; // 加载下一页
|
||||
}
|
||||
} else {
|
||||
setIsSearching(false)
|
||||
setSearchResults([])
|
||||
|
||||
// 处理可能的id字段映射(确保id存在)
|
||||
const formattedResults = allResults.map(item => ({
|
||||
...item,
|
||||
id: item.id || item.agentId || item.flowId // 兼容不同字段名
|
||||
}));
|
||||
|
||||
setSearchResults(formattedResults);
|
||||
} catch (error) {
|
||||
console.error("搜索失败:", error);
|
||||
setSearchResults([]);
|
||||
} finally {
|
||||
setSearchLoading(false);
|
||||
}
|
||||
} else {
|
||||
setIsSearching(false);
|
||||
setSearchResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = useDebounce(handleSearchChange, 360, false)
|
||||
|
||||
const handleSearchClear = () => {
|
||||
@@ -253,7 +285,7 @@ export default function AgentCenter() {
|
||||
{/* Scrollable Content */}
|
||||
<div className="relative" style={{ height: "calc(100vh - 200px)" }}>
|
||||
<div ref={scrollContainerRef} className="container mx-auto px-6 py-6 pb-96 h-full overflow-y-auto scrollbar-hide">
|
||||
{!isSearching ? (
|
||||
|
||||
<AgentGrid
|
||||
favorites={favorites}
|
||||
onAddToFavorites={addToFavorites}
|
||||
@@ -262,7 +294,7 @@ export default function AgentCenter() {
|
||||
refreshTrigger={refreshTrigger}
|
||||
onCardClick={handleCardClick}
|
||||
/>
|
||||
) : (
|
||||
{isSearching &&(
|
||||
<SearchOverlay
|
||||
query={searchQuery}
|
||||
results={searchResults}
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
"enterUniqueId": "请选择具体应用",
|
||||
"enterVersion": "请选择版本",
|
||||
"workFlow":"请选择工作流",
|
||||
"enterFile": "请选择测试集数据",
|
||||
"enterFile": "请上传测试集数据",
|
||||
"fileSizeLimit": "文件大小限制在10M以内",
|
||||
"enterPrompt": "评测指令不能为空",
|
||||
"evaluationCollection": "评测集合",
|
||||
|
||||
@@ -14,7 +14,7 @@ const gradients = [
|
||||
export default function AppAvator({ id = 1, flowType = '', url = '', className = '' }) {
|
||||
|
||||
const color = useMemo(() => {
|
||||
const str = id + ''
|
||||
const str = (id + '').substring(0, 4)
|
||||
let hex = '';
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hex += str.charCodeAt(i).toString(16);
|
||||
|
||||
@@ -54,7 +54,7 @@ export const ReasoningLog = ({ loading, msg = '' }) => {
|
||||
</div>
|
||||
}
|
||||
|
||||
export default function MessageBs({ debug, mark = false, logo, data, onUnlike = () => { }, onSource, onMarkClick,chat }: { logo: string, data: ChatMessageType, onUnlike?: any, onSource?: any }) {
|
||||
export default function MessageBs({ debug, mark = false, logo, data, onUnlike = () => { }, onSource, onMarkClick, chat }: { logo: string, data: ChatMessageType, onUnlike?: any, onSource?: any }) {
|
||||
const avatarColor = colorList[
|
||||
(data.sender?.split('').reduce((num, s) => num + s.charCodeAt(), 0) || 0) % colorList.length
|
||||
]
|
||||
@@ -84,6 +84,7 @@ export default function MessageBs({ debug, mark = false, logo, data, onUnlike =
|
||||
</div>
|
||||
<div className="min-h-8 px-6 py-4 rounded-2xl bg-[#F5F6F8] dark:bg-[#313336]">
|
||||
<div className="flex gap-2">
|
||||
{logo}
|
||||
{data.message.toString() ?
|
||||
<div ref={messageRef} className="text-sm max-w-[calc(100%-24px)]">
|
||||
{<MessageMarkDown message={message} />}
|
||||
|
||||
@@ -62,9 +62,21 @@ const handleHistoryMsg = (data: any[]): ChatMessageType[] => {
|
||||
// 未考虑的情况暂不处理
|
||||
console.error('消息 to JSON error :>> ', e);
|
||||
}
|
||||
|
||||
// hack
|
||||
let chatKey = undefined
|
||||
if (typeof message !== 'string') {
|
||||
// 优先 input
|
||||
if ('input' in message) {
|
||||
chatKey = 'input'
|
||||
} else {
|
||||
Object.keys(message)[0]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...other,
|
||||
chatKey: typeof message === 'string' ? undefined : Object.keys(message)[0],
|
||||
chatKey,
|
||||
end: true,
|
||||
files: files ? JSON.parse(files) : [],
|
||||
isSend: !is_bot,
|
||||
|
||||
@@ -52,7 +52,7 @@ export const saveAssistanttApi = async (
|
||||
): Promise<any> => {
|
||||
if (data.logo) {
|
||||
// logo保存相对路径
|
||||
data.logo = data.logo.replace('/bisheng', '')
|
||||
data.logo = data.logo.replace(/^\/\w+/, '')
|
||||
}
|
||||
return await axios.put(`/api/v1/assistant`, data)
|
||||
};
|
||||
|
||||
@@ -118,6 +118,13 @@ export async function getFlowApi(flowId: string, version: string = 'v1'): Promis
|
||||
return await axios.get(`/api/${version}/flows/${flowId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除的技能 工作流详情
|
||||
*/
|
||||
export async function getDeleteFlowApi(chatId: string): Promise<FlowType> {
|
||||
return await axios.get(`/api/v1/chat/info?chat_id=${chatId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new flow to the database.
|
||||
*
|
||||
@@ -194,7 +201,7 @@ export const createCustomFlowApi = async (params: {
|
||||
}, userName: string) => {
|
||||
if (params.logo) {
|
||||
// logo保存相对路径
|
||||
params.logo = params.logo.replace('/bisheng', '')
|
||||
params.logo = params.logo.replace(/^\/\w+/, '')
|
||||
}
|
||||
const response: FlowType = await axios.post("/api/v1/flows/", {
|
||||
...params,
|
||||
@@ -221,7 +228,7 @@ export async function updateFlowApi(
|
||||
): Promise<FlowType> {
|
||||
if (updatedFlow.logo) {
|
||||
// logo保存相对路径
|
||||
updatedFlow.logo = updatedFlow.logo.replace('/bisheng', '')
|
||||
updatedFlow.logo = updatedFlow.logo.replace(/^\/\w+/, '')
|
||||
}
|
||||
return await axios.patch(`/api/v1/flows/${updatedFlow.id}`, {
|
||||
logo: updatedFlow.logo || '',
|
||||
|
||||
@@ -41,7 +41,7 @@ export const createWorkflowApi = async (name, desc, url, flow): Promise<any> =>
|
||||
export const saveWorkflow = async (versionId: number, data: WorkFlow): Promise<any> => {
|
||||
if (data.logo) {
|
||||
// logo保存相对路径
|
||||
data.logo = data.logo.replace('/bisheng', '')
|
||||
data.logo = data.logo.replace(/^\/\w+/, '')
|
||||
}
|
||||
return await axios.put(`/api/v1/workflow/versions/${versionId}`, data);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export async function getWorkFlowVersions(flow_id): Promise<{ data: any[], total
|
||||
*/
|
||||
export const onlineWorkflow = async (flow, status = ''): Promise<any> => {
|
||||
const { name, description, logo } = flow
|
||||
const data = { name, description, logo: logo && logo.replace('/bisheng', '') }
|
||||
const data = { name, description, logo: logo && logo.replace(/^\/\w+/, '') }
|
||||
if (status) {
|
||||
data['status'] = status
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { copyReportTemplate, createWorkflowApi } from "@/controllers/API/workflo
|
||||
import { captureAndAlertRequestErrorHoc } from "@/controllers/request";
|
||||
import { uploadFileWithProgress } from "@/modals/UploadModal/upload";
|
||||
import { AppType, AppTypeToNum } from "@/types/app";
|
||||
import { forwardRef, useContext, useImperativeHandle, useRef, useState } from "react";
|
||||
import { forwardRef, useContext, useImperativeHandle, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -230,10 +230,14 @@ ${t('build.exampleTwo', { ns: 'bs' })}
|
||||
// 上传头像逻辑
|
||||
const uploadAvator = (file: File) => {
|
||||
uploadFileWithProgress(file, (progress) => { }, 'icon').then(res => {
|
||||
setFormData(prev => ({ ...prev, url: '/bisheng/' + res.relative_path }));
|
||||
setFormData(prev => ({ ...prev, url: res.file_path }));
|
||||
});
|
||||
};
|
||||
|
||||
const previewAvatar = useMemo(() =>
|
||||
formData.url ? __APP_ENV__.BASE_URL + formData.url : '',
|
||||
[formData.url])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-[625px] bg-background-login">
|
||||
@@ -245,8 +249,8 @@ ${t('build.exampleTwo', { ns: 'bs' })}
|
||||
<label htmlFor="name" className="bisheng-label">
|
||||
{appType === AppType.ASSISTANT ? t('assistantAvatar') : t('workflowAvatar')}
|
||||
</label>
|
||||
<Avator value={formData.url} className="mt-3" onChange={uploadAvator}>
|
||||
<AppAvator id={formData.name} url={loca?.logo} flowType={AppTypeToNum[appType]} className="size-8"></AppAvator>
|
||||
<Avator value={previewAvatar} className="mt-3" onChange={uploadAvator}>
|
||||
<AppAvator id={6} flowType={AppTypeToNum[appType]} className="size-8"></AppAvator>
|
||||
</Avator>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
|
||||
+7
-3
@@ -5,7 +5,7 @@ import { Input, Textarea } from "@/components/bs-ui/input";
|
||||
import Avator from "@/components/bs-ui/input/avator";
|
||||
import { useToast } from "@/components/bs-ui/toast/use-toast";
|
||||
import { uploadFileWithProgress } from "@/modals/UploadModal/upload";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function EditAssistantDialog({ logo, name, desc, onSave, loca }) {
|
||||
@@ -84,6 +84,10 @@ export default function EditAssistantDialog({ logo, name, desc, onSave, loca })
|
||||
})
|
||||
}
|
||||
|
||||
const previewAvatar = useMemo(() =>
|
||||
formData.logo ? __APP_ENV__.BASE_URL + formData.logo : '',
|
||||
[formData.logo])
|
||||
|
||||
return <DialogContent className="sm:max-w-[625px] bg-background-login">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('build.editAssistant')}</DialogTitle>
|
||||
@@ -92,11 +96,11 @@ export default function EditAssistantDialog({ logo, name, desc, onSave, loca })
|
||||
<div className="">
|
||||
<label htmlFor="name" className="bisheng-label">{t('build.assistantAvatar')}<span className="bisheng-tip">*</span></label>
|
||||
<Avator
|
||||
value={formData.logo}
|
||||
value={previewAvatar}
|
||||
className="mt-2"
|
||||
onChange={uploadAvator}
|
||||
>
|
||||
<AppAvator id={formData.name} url={formData.logo} flowType={5} className="size-8"></AppAvator>
|
||||
<AppAvator id={6} flowType={5} className="size-9"></AppAvator>
|
||||
</Avator>
|
||||
|
||||
{errors.name && <p className="bisheng-tip mt-1">{errors.name}</p>}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function Header({ loca, onSave, onLine, onTabChange }) {
|
||||
return <div className="flex justify-between bg-background-login items-center border-b px-4">
|
||||
<div className="flex items-center gap-2 py-4">
|
||||
<Button variant="outline" size="icon" onClick={() => navigate(-1)}><ChevronLeft className="h-4 w-4" /></Button>
|
||||
<AppAvator id={assistantState.name} url={loca?.logo} flowType={5} className="ml-4"></AppAvator>
|
||||
<AppAvator id={assistantState.name} url={assistantState.logo} flowType={5} className="ml-4"></AppAvator>
|
||||
<span className="bisheng-title">{assistantState.name}</span>
|
||||
{/* edit dialog */}
|
||||
<Dialog open={editShow} onOpenChange={setEditShow}>
|
||||
|
||||
@@ -68,10 +68,21 @@ const handleHistoryMsg = (data: any[]): ChatMessageType[] => {
|
||||
// 未考虑的情况暂不处理
|
||||
console.error('消息 to JSON error :>> ', e);
|
||||
}
|
||||
// hack
|
||||
let chatKey = undefined
|
||||
if (typeof message !== 'string') {
|
||||
// 优先 input
|
||||
if ('input' in message) {
|
||||
chatKey = 'input'
|
||||
} else {
|
||||
Object.keys(message)[0]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...other,
|
||||
category,
|
||||
chatKey: typeof message === 'string' ? undefined : Object.keys(message)[0],
|
||||
chatKey,
|
||||
end: true,
|
||||
files: files ? JSON.parse(files) : [],
|
||||
isSend: !is_bot,
|
||||
|
||||
@@ -144,10 +144,14 @@ export default function l2Edit() {
|
||||
const [logo, setLogo] = useState('')
|
||||
const uploadAvator = (file) => {
|
||||
uploadFileWithProgress(file, (progress) => { }, 'icon').then(res => {
|
||||
setLogo('/bisheng/' + res.relative_path);
|
||||
setLogo(res.file_path);
|
||||
})
|
||||
}
|
||||
|
||||
const previewAvatar = useMemo(() =>
|
||||
logo ? __APP_ENV__.BASE_URL + logo : '',
|
||||
[logo])
|
||||
|
||||
return <div className="relative box-border h-full overflow-auto">
|
||||
<div className="p-6 pb-48 h-full overflow-y-auto">
|
||||
<div className="flex justify-between w-full">
|
||||
@@ -174,8 +178,8 @@ export default function l2Edit() {
|
||||
<div className="w-full overflow-hidden transition-all px-1">
|
||||
<div className="mt-4">
|
||||
<Label htmlFor="name">{t('skills.avatar')}</Label>
|
||||
<Avator value={logo} className="mt-2" onChange={uploadAvator}>
|
||||
<AppAvator id={name} url={logo} flowType={1} className="w-9 h-9"></AppAvator>
|
||||
<Avator value={previewAvatar} className="mt-2" onChange={uploadAvator}>
|
||||
<AppAvator id={6} flowType={1} className="w-9 h-9"></AppAvator>
|
||||
</Avator>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
|
||||
@@ -281,8 +281,8 @@ export default function ChatPanne({ customWsHost = '', chatList, chat, appendHis
|
||||
{
|
||||
workflow && <div className={`w-full chat-box h-full relative ${type === AppNumType.FLOW ? 'block' : 'hidden'}`}>
|
||||
<div className="absolute flex top-2 gap-2 items-center z-10 bg-[rgba(255,255,255,0.8)] px-6 py-1 dark:bg-[#1B1B1B]">
|
||||
<AppAvator id={chat.flow_name} url={chat.logo} flowType={10} ></AppAvator>
|
||||
<span className="text-sm">{chat.flow_name}</span>
|
||||
<AppAvator id={workflow.name} url={workflow.logo} flowType={10} ></AppAvator>
|
||||
<span className="text-sm">{workflow.name}</span>
|
||||
</div>
|
||||
<ChatPane autoRun={autoRun} chatId={flowChatId} flow={workflow} wsUrl={wsUrl} />
|
||||
</div>
|
||||
|
||||
@@ -169,7 +169,7 @@ const CreateDataSet = forwardRef(({ onChange }, ref) => {
|
||||
</div>
|
||||
<SimpleUpload
|
||||
filekey="file"
|
||||
uploadUrl={__APP_ENV__.BASE_URL + '/api/v1/knowledge/upload'}
|
||||
uploadUrl={'/api/v1/knowledge/upload'}
|
||||
accept={['json']}
|
||||
className={`${error.fileUrl ? 'border-red-400' : ''}`}
|
||||
onSuccess={handleFileUploadSuccess}
|
||||
|
||||
@@ -114,21 +114,22 @@ export default function EvaluatingCreate() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleCreateEvaluation = async () => {
|
||||
const errorlist = [];
|
||||
if (!selectedType) errorlist.push(t("evaluation.enterExecType"));
|
||||
// if (!selectedKeyId) errorlist.push(t("evaluation.workFlow"));
|
||||
const errorlist = [];
|
||||
if (!selectedType) errorlist.push(t("evaluation.enterExecType"));
|
||||
|
||||
if (selectedType && !selectedKeyId) {
|
||||
if (selectedType === "workflow") errorlist.push(t("请选择工作流"));
|
||||
if (selectedType === "flow") errorlist.push(t("请选择技能"));
|
||||
if (selectedType === "assistant") errorlist.push(t("请选择助手"));
|
||||
}
|
||||
|
||||
// 修复版本验证 - 取消注释并添加正确的验证
|
||||
if ((selectedType === "workflow" || selectedType === "flow" || !selectedKeyId) && !selectedVersion) {
|
||||
errorlist.push(t("evaluation.workFlow"));
|
||||
}
|
||||
if (selectedKeyId && (selectedType === "workflow" || selectedType === "flow") && !selectedVersion) {
|
||||
errorlist.push(t("请选择版本"));
|
||||
}
|
||||
if (!fileRef.current && selectedKeyId&&selectedVersion) errorlist.push(t("evaluation.enterFile"));
|
||||
if (!prompt) errorlist.push(t("evaluation.enterPrompt"));
|
||||
|
||||
// 修复文件验证 - 所有类型都需要测试集数据
|
||||
// if (!fileRef.current) errorlist.push(t("evaluation.enterFile"));
|
||||
|
||||
if (!prompt) errorlist.push(t("evaluation.enterPrompt"));
|
||||
|
||||
if (errorlist.length) return handleError(errorlist);
|
||||
if (errorlist.length) return handleError(errorlist);
|
||||
setLoading(true);
|
||||
try {
|
||||
await captureAndAlertRequestErrorHoc(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useToast } from "@/components/bs-ui/toast/use-toast";
|
||||
import { rebUploadFile, retryKnowledgeFileApi, subUploadLibFile } from "@/controllers/API";
|
||||
import { captureAndAlertRequestErrorHoc } from "@/controllers/request";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import FileUploadStep2 from "./components/FileUploadStep2";
|
||||
@@ -29,6 +29,12 @@ export default function AdjustFilesUpload() {
|
||||
|
||||
// 从路由状态获取调整模式的初始数据(必须传文件数据)
|
||||
const initFileData = location.state?.fileData;
|
||||
useEffect(() => {
|
||||
// 如果没有初始化数据,说明是直接访问,跳转到/filelib
|
||||
if (!initFileData) {
|
||||
navigate('/filelib', { replace: true });
|
||||
}
|
||||
}, [initFileData, navigate]);
|
||||
if (!initFileData) {
|
||||
navigate(-1); // 无数据时回退
|
||||
return null;
|
||||
@@ -112,17 +118,18 @@ export default function AdjustFilesUpload() {
|
||||
const fileName = initFileData.name || initFileData.file_name || '';
|
||||
const fileSuffix = fileName.split('.').pop()?.toLowerCase() || 'txt';
|
||||
const fileType = ['xlsx', 'xls', 'csv'].includes(fileSuffix) ? 'table' : 'file';
|
||||
|
||||
const [resultFiles, setResultFiles] = useState([
|
||||
{
|
||||
id: initFileData.id,
|
||||
fileName: initFileData.name || initFileData.file_name, // 兼容字段名
|
||||
file_path: initFileData.filePath || initFileData.object_name, // 兼容文件路径
|
||||
suffix: initFileData.suffix || initFileData.file_name?.split(".").pop() || "", // 解析后缀
|
||||
previewUrl:initFileData.previewUrl,
|
||||
fileType: fileType,
|
||||
split_rule: getParsedSplitRule(initFileData.split_rule) // 传入转换后的配置对象
|
||||
}
|
||||
]);
|
||||
console.log(initFileData,78);
|
||||
|
||||
const [segmentRules, setSegmentRules] = useState(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogT
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/bs-ui/select";
|
||||
import { useToast } from "@/components/bs-ui/toast/use-toast";
|
||||
import { getKnowledgeModelConfig, getLLmServerDetail, getModelListApi } from "@/controllers/API/finetune";
|
||||
import { CircleAlert, Copy, Ellipsis, LoaderCircle, Settings, Trash2 } from "lucide-react";
|
||||
import { BookCopy, CircleAlert, Copy, Ellipsis, LoaderCircle, Settings, Trash2 } from "lucide-react";
|
||||
import AutoPagination from "../../components/bs-ui/pagination/autoPagination";
|
||||
import { useTable } from "../../util/hook";
|
||||
import { ModelSelect } from "../ModelPage/manage/tabs/WorkbenchModel";
|
||||
@@ -112,7 +112,7 @@ function CreateModal({ datalist, open, onOpenChange, onLoadEnd, mode = 'create',
|
||||
|
||||
fetchModelData();
|
||||
}, [open, mode, currentLib]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// 当弹窗关闭时,清空所有内部状态
|
||||
if (!open) {
|
||||
@@ -122,112 +122,112 @@ function CreateModal({ datalist, open, onOpenChange, onLoadEnd, mode = 'create',
|
||||
setError({ name: false, desc: false });
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
|
||||
const { toast } = useToast()
|
||||
const [error, setError] = useState({ name: false, desc: false })
|
||||
|
||||
const handleCreate = async (e, isImport = false) => {
|
||||
const name = nameRef.current.value || ''; // 名称(默认空字符串,避免null)
|
||||
let desc = descRef.current.value || ''; // 描述(默认空字符串)
|
||||
const handleCreate = async (e, isImport = false) => {
|
||||
const name = nameRef.current.value || ''; // 名称(默认空字符串,避免null)
|
||||
let desc = descRef.current.value || ''; // 描述(默认空字符串)
|
||||
|
||||
// 1. 定义默认描述的"固定文本部分"(不含名称)
|
||||
const defaultDescPrefix = "当回答与";
|
||||
const defaultDescSuffix = "相关的问题时,参考此知识库";
|
||||
// 固定文本总长度 = 前缀长度 + 后缀长度
|
||||
const fixedTextLength = defaultDescPrefix.length + defaultDescSuffix.length;
|
||||
// 名称可占用的最大长度 = 200 - 固定文本长度(确保名称+固定文本≤200)
|
||||
const maxNameLengthForDefaultDesc = 200 - fixedTextLength;
|
||||
// 1. 定义默认描述的"固定文本部分"(不含名称)
|
||||
const defaultDescPrefix = "当回答与";
|
||||
const defaultDescSuffix = "相关的问题时,参考此知识库";
|
||||
// 固定文本总长度 = 前缀长度 + 后缀长度
|
||||
const fixedTextLength = defaultDescPrefix.length + defaultDescSuffix.length;
|
||||
// 名称可占用的最大长度 = 200 - 固定文本长度(确保名称+固定文本≤200)
|
||||
const maxNameLengthForDefaultDesc = 200 - fixedTextLength;
|
||||
|
||||
// 2. 未输入描述时,生成默认描述(严格控制总长度≤200)
|
||||
if (!desc) {
|
||||
// 情况1:名称长度 ≤ 可占用最大长度 → 直接拼接生成默认描述
|
||||
if (name.length <= maxNameLengthForDefaultDesc) {
|
||||
desc = `${defaultDescPrefix}${name}${defaultDescSuffix}`;
|
||||
}
|
||||
// 情况2:名称长度 > 可占用最大长度 → 截断名称后再拼接
|
||||
else {
|
||||
// 截断名称(保留前 maxNameLengthForDefaultDesc 个字,避免总长度超200)
|
||||
const truncatedName = name.slice(0, maxNameLengthForDefaultDesc);
|
||||
// 生成截断截断后的默认描述
|
||||
desc = `${defaultDescPrefix}${truncatedName}${defaultDescSuffix}`;
|
||||
// 提示用户:名称过长已被截断(提升体验,避免用户困惑)
|
||||
toast({
|
||||
variant: "info",
|
||||
description: `知识库名称过长,已自动截断为${maxNameLengthForDefaultDesc}字,确保默认描述不超过200字限制`
|
||||
});
|
||||
// 2. 未输入描述时,生成默认描述(严格控制总长度≤200)
|
||||
if (!desc) {
|
||||
// 情况1:名称长度 ≤ 可占用最大长度 → 直接拼接生成默认描述
|
||||
if (name.length <= maxNameLengthForDefaultDesc) {
|
||||
desc = `${defaultDescPrefix}${name}${defaultDescSuffix}`;
|
||||
}
|
||||
// 情况2:名称长度 > 可占用最大长度 → 截断名称后再拼接
|
||||
else {
|
||||
// 截断名称(保留前 maxNameLengthForDefaultDesc 个字,避免总长度超200)
|
||||
const truncatedName = name.slice(0, maxNameLengthForDefaultDesc);
|
||||
// 生成截断截断后的默认描述
|
||||
desc = `${defaultDescPrefix}${truncatedName}${defaultDescSuffix}`;
|
||||
// 提示用户:名称过长已被截断(提升体验,避免用户困惑)
|
||||
toast({
|
||||
variant: "info",
|
||||
description: `知识库名称过长,已自动截断为${maxNameLengthForDefaultDesc}字,确保默认描述不超过200字限制`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 原有校验逻辑(仅针对用户手动输入的描述,默认描述已确保≤200)
|
||||
if (!name) {
|
||||
handleError(t('lib.enterLibraryName'));
|
||||
return;
|
||||
}
|
||||
if (name.length > 30) {
|
||||
handleError('知识库名称不能超过30字');
|
||||
return;
|
||||
}
|
||||
if (!modal) {
|
||||
handleError(t('lib.selectModel'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复:名称重复校验逻辑
|
||||
// 编辑模式且名称未变更时,不进行重复校验
|
||||
const isEditMode = mode === 'edit' && currentLib;
|
||||
const nameUnchanged = isEditMode && name === currentLib.name;
|
||||
|
||||
if (!nameUnchanged && datalist.find(data => data.name === name && (!currentLib || data.id !== currentLib.id))) {
|
||||
handleError(t('lib.nameExists'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅校验用户手动输入的描述(默认描述已控制长度,可跳过)
|
||||
if (descRef.current.value && desc.length > 200) {
|
||||
handleError(t('lib.descriptionLimit'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
if (mode === 'create') {
|
||||
await captureAndAlertRequestErrorHoc(createFileLib({
|
||||
name,
|
||||
description: desc,
|
||||
model: modal[1].value,
|
||||
type: 0
|
||||
}).then(res => {
|
||||
window.libname = [name, desc]
|
||||
navigate(isImport
|
||||
? `/filelib/upload/${res.id}`
|
||||
: `/filelib/${res.id}`
|
||||
);
|
||||
onOpenChange(false); // 修复:用onOpenChange关闭弹窗
|
||||
})).finally(() => {
|
||||
setIsSubmitting(false)
|
||||
})
|
||||
} else {
|
||||
const data = {
|
||||
"model_id": modal[1].value,
|
||||
"model_type": "embedding",
|
||||
"knowledge_id": currentLib.id,
|
||||
"knowledge_name": name,
|
||||
"description": desc
|
||||
// 3. 原有校验逻辑(仅针对用户手动输入的描述,默认描述已确保≤200)
|
||||
if (!name) {
|
||||
handleError(t('lib.enterLibraryName'));
|
||||
return;
|
||||
}
|
||||
await captureAndAlertRequestErrorHoc(updateKnowledge(data).then(res => {
|
||||
toast({
|
||||
variant: "success",
|
||||
description: '更新成功'
|
||||
if (name.length > 200) {
|
||||
handleError('知识库名称不能超过200字');
|
||||
return;
|
||||
}
|
||||
if (!modal) {
|
||||
handleError(t('lib.selectModel'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 修复:名称重复校验逻辑
|
||||
// 编辑模式且名称未变更时,不进行重复校验
|
||||
const isEditMode = mode === 'edit' && currentLib;
|
||||
const nameUnchanged = isEditMode && name === currentLib.name;
|
||||
|
||||
if (!nameUnchanged && datalist.find(data => data.name === name && (!currentLib || data.id !== currentLib.id))) {
|
||||
handleError(t('lib.nameExists'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅校验用户手动输入的描述(默认描述已控制长度,可跳过)
|
||||
if (descRef.current.value && desc.length > 200) {
|
||||
handleError(t('lib.descriptionLimit'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
if (mode === 'create') {
|
||||
await captureAndAlertRequestErrorHoc(createFileLib({
|
||||
name,
|
||||
description: desc,
|
||||
model: modal[1].value,
|
||||
type: 0
|
||||
}).then(res => {
|
||||
window.libname = [name, desc]
|
||||
navigate(isImport
|
||||
? `/filelib/upload/${res.id}`
|
||||
: `/filelib/${res.id}`
|
||||
);
|
||||
onOpenChange(false); // 修复:用onOpenChange关闭弹窗
|
||||
})).finally(() => {
|
||||
setIsSubmitting(false)
|
||||
})
|
||||
onOpenChange(false); // 修复:用onOpenChange关闭弹窗(替代原setOpen)
|
||||
onLoadEnd()
|
||||
}).catch(error => {
|
||||
toast({ variant: "error", description: '更新失败,请重试' });
|
||||
onOpenChange(false); // 错误时也关闭弹窗,避免状态卡住
|
||||
})).finally(() => {
|
||||
setIsSubmitting(false)
|
||||
})
|
||||
} else {
|
||||
const data = {
|
||||
"model_id": modal[1].value,
|
||||
"model_type": "embedding",
|
||||
"knowledge_id": currentLib.id,
|
||||
"knowledge_name": name,
|
||||
"description": desc
|
||||
}
|
||||
await captureAndAlertRequestErrorHoc(updateKnowledge(data).then(res => {
|
||||
toast({
|
||||
variant: "success",
|
||||
description: '更新成功'
|
||||
})
|
||||
onOpenChange(false); // 修复:用onOpenChange关闭弹窗(替代原setOpen)
|
||||
onLoadEnd()
|
||||
}).catch(error => {
|
||||
toast({ variant: "error", description: '更新失败,请重试' });
|
||||
onOpenChange(false); // 错误时也关闭弹窗,避免状态卡住
|
||||
})).finally(() => {
|
||||
setIsSubmitting(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = (message) => {
|
||||
toast({
|
||||
@@ -411,21 +411,21 @@ export default function KnowledgeFile() {
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenSettings = (lib) => {
|
||||
console.log("=== handleOpenSettings 开始执行 ===");
|
||||
console.log("当前点击的lib ID:", lib.id);
|
||||
// 1. 深拷贝:彻底断开与原 lib 的引用关联(解决嵌套属性引用不变问题)
|
||||
const newCurrentLib = JSON.parse(JSON.stringify(lib));
|
||||
// 2. 注入唯一标识:即使数据完全相同,也让 currentSettingLib 引用绝对唯一
|
||||
newCurrentLib.__updateKey = Date.now(); // 每次点击生成不同的时间戳
|
||||
|
||||
setCurrentSettingLib(newCurrentLib); // 此时传递的是完全新的对象引用
|
||||
setSettingsOpen(true);
|
||||
setModalKey(prev => prev + 1); // 保留 modalKey 确保弹窗重新挂载
|
||||
console.log("handleOpenSettings called with lib:", newCurrentLib); // 验证打印
|
||||
};
|
||||
const handleOpenSettings = (lib) => {
|
||||
console.log("=== handleOpenSettings 开始执行 ===");
|
||||
console.log("当前点击的lib ID:", lib.id);
|
||||
// 1. 深拷贝:彻底断开与原 lib 的引用关联(解决嵌套属性引用不变问题)
|
||||
const newCurrentLib = JSON.parse(JSON.stringify(lib));
|
||||
// 2. 注入唯一标识:即使数据完全相同,也让 currentSettingLib 引用绝对唯一
|
||||
newCurrentLib.__updateKey = Date.now(); // 每次点击生成不同的时间戳
|
||||
|
||||
const handleSettingsClose = (isOpen) => {
|
||||
setCurrentSettingLib(newCurrentLib); // 此时传递的是完全新的对象引用
|
||||
setSettingsOpen(true);
|
||||
setModalKey(prev => prev + 1); // 保留 modalKey 确保弹窗重新挂载
|
||||
console.log("handleOpenSettings called with lib:", newCurrentLib); // 验证打印
|
||||
};
|
||||
|
||||
const handleSettingsClose = (isOpen) => {
|
||||
console.log("handleSettingsClose called with isOpen:", isOpen);
|
||||
setSettingsOpen(isOpen);
|
||||
if (!isOpen) {
|
||||
@@ -439,7 +439,7 @@ const handleOpenSettings = (lib) => {
|
||||
const handleCachePage = () => {
|
||||
window.LibPage = { page, type: 'file' }
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const _page = window.LibPage
|
||||
if (_page) {
|
||||
@@ -511,7 +511,7 @@ const handleOpenSettings = (lib) => {
|
||||
key={el.id}
|
||||
className=" h-[70px]"
|
||||
>
|
||||
<TableCell
|
||||
<TableCell
|
||||
className="font-medium max-w-[200px]"
|
||||
onClick={() => {
|
||||
window.libname = [el.name, el.description];
|
||||
@@ -520,27 +520,27 @@ const handleOpenSettings = (lib) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<div className="w-[50px] h-[50px]">
|
||||
<img src={TipPng} alt="" />
|
||||
<div className="flex items-center justify-center w-[50px] h-[50px] bg-primary text-white rounded-[10px]">
|
||||
<BookCopy />
|
||||
</div>
|
||||
<div>
|
||||
<div className="truncate max-w-[500px] w-[264px] text-[18px] font-medium pt-2 flex items-center gap-2">
|
||||
{el.name}
|
||||
</div>
|
||||
<QuestionTooltip
|
||||
content={el.description || ''}
|
||||
content={el.description || ''}
|
||||
error={false}
|
||||
className="w-full text-start"
|
||||
>
|
||||
<div className="truncate max-w-[300px] text-[14px] text-[#5A5A5A] pt-1">
|
||||
{el.description || ''}
|
||||
{el.description || ''}
|
||||
</div>
|
||||
</QuestionTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
<TableCell
|
||||
className="text-[#5A5A5A]"
|
||||
onClick={() => {
|
||||
window.libname = [el.name, el.description];
|
||||
@@ -551,7 +551,7 @@ const handleOpenSettings = (lib) => {
|
||||
{el.update_time.replace('T', ' ')}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
<TableCell
|
||||
className="max-w-[300px] break-all"
|
||||
onClick={() => {
|
||||
window.libname = [el.name, el.description];
|
||||
@@ -565,15 +565,15 @@ const handleOpenSettings = (lib) => {
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Select
|
||||
key={`${el.id}-${modalKey}`}
|
||||
key={`${el.id}-${modalKey}`}
|
||||
open={selectOpenId === el.id}
|
||||
onOpenChange={(isOpen) => {
|
||||
onOpenChange={(isOpen) => {
|
||||
setSelectOpenId(isOpen ? el.id : null);
|
||||
}}
|
||||
onValueChange={(selectedValue) => {
|
||||
setSelectOpenId(null);
|
||||
console.log("Selected value:", selectedValue, "for lib:", el.id);
|
||||
|
||||
|
||||
switch (selectedValue) {
|
||||
case 'copy':
|
||||
el.state === 1 && handleCopy(el);
|
||||
@@ -606,7 +606,7 @@ const handleOpenSettings = (lib) => {
|
||||
<Ellipsis size={24} color="#a69ba2" strokeWidth={1.75} />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
<SelectContent
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
@@ -620,12 +620,12 @@ const handleOpenSettings = (lib) => {
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Copy className="w-4 h-4" />
|
||||
{ t('lib.copy') }
|
||||
{t('lib.copy')}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
<SelectItem
|
||||
value="set"
|
||||
<SelectItem
|
||||
value="set"
|
||||
showIcon={false}
|
||||
>
|
||||
<div className="flex gap-2 items-center">
|
||||
@@ -663,7 +663,7 @@ const handleOpenSettings = (lib) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 创建弹窗 */}
|
||||
<CreateModal
|
||||
datalist={datalist}
|
||||
@@ -672,18 +672,18 @@ const handleOpenSettings = (lib) => {
|
||||
onLoadEnd={() => { }}
|
||||
mode="create"
|
||||
/>
|
||||
|
||||
|
||||
{/* 编辑(设置)弹窗 - 使用 key 强制重新渲染 */}
|
||||
{settingsOpen && (
|
||||
<CreateModal
|
||||
key={`settings-modal-${modalKey}`}
|
||||
datalist={datalist}
|
||||
open={settingsOpen}
|
||||
onOpenChange={handleSettingsClose}
|
||||
onLoadEnd={reload}
|
||||
mode="edit"
|
||||
currentLib={currentSettingLib}
|
||||
/>
|
||||
<CreateModal
|
||||
key={`settings-modal-${modalKey}`}
|
||||
datalist={datalist}
|
||||
open={settingsOpen}
|
||||
onOpenChange={handleSettingsClose}
|
||||
onLoadEnd={reload}
|
||||
mode="edit"
|
||||
currentLib={currentSettingLib}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -56,7 +56,6 @@ const FileUploadStep2 = forwardRef(({ step, resultFiles, isSubmitting, onNext, o
|
||||
});
|
||||
const displayMode: DisplayModeType | null = useMemo(() => {
|
||||
if (!resultFiles || resultFiles.length === 0) return null;
|
||||
console.log(resultFiles,55555);
|
||||
|
||||
const hasTableFiles = resultFiles.some(file => file.fileType === 'table');
|
||||
const hasDocumentFiles = resultFiles.some(file => file.fileType === 'file');
|
||||
@@ -338,6 +337,7 @@ const useFileProcessingRules = (initialStrategies, resultFiles, kid, splitRule)
|
||||
id: file.id,
|
||||
filePath: file.file_path,
|
||||
fileName: file.fileName,
|
||||
previewUrl:file.previewUrl,
|
||||
suffix: file.suffix,
|
||||
fileType: file.fileType,
|
||||
excelRule: file.fileType === 'table' ? {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import CardComponent from "@/components/bs-comp/cardComponent";
|
||||
import ProgressItem from "@/components/bs-comp/knowledgeUploadComponent/ProgressItem";
|
||||
import { Button } from "@/components/bs-ui/button";
|
||||
@@ -15,106 +14,148 @@ export default function FileUploadStep4({ data ,kId}) {
|
||||
const [finish, setFinish] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
const { id: kid } = useParams()
|
||||
console.log(data,kId,44);
|
||||
console.log(data,kId,44);
|
||||
|
||||
const [files, setFiles] = useState([])
|
||||
const timerRef = useRef(null); // 轮询定时器引用
|
||||
const fileIdsRef = useRef([]); // 文件ID列表引用
|
||||
// 初始化文件状态
|
||||
const processingRef = useRef(new Set()); // 跟踪正在处理的文件ID
|
||||
const isPollingRef = useRef(false); // 防止轮询并发
|
||||
|
||||
// 初始化文件状态(只执行一次)
|
||||
useEffect(() => {
|
||||
const initialFiles = data.map(item => ({
|
||||
id: item.fileId,
|
||||
fileName: item.fileName,
|
||||
error: false,
|
||||
reason: '',
|
||||
progress: 'await' // 初始状态设为解析中
|
||||
}));
|
||||
setFiles(initialFiles);
|
||||
fileIdsRef.current = data.map(item => item.fileId); // 保存文件ID列表
|
||||
}, [data]);
|
||||
if (files.length === 0 && data.length > 0) { // 防止重复初始化
|
||||
const initialFiles = data.map(item => ({
|
||||
id: item.fileId,
|
||||
fileName: item.fileName,
|
||||
error: false,
|
||||
reason: '',
|
||||
progress: 'await' // 初始状态设为解析中
|
||||
}));
|
||||
setFiles(initialFiles);
|
||||
fileIdsRef.current = data.map(item => item.fileId);
|
||||
// 初始化处理中的文件集合
|
||||
fileIdsRef.current.forEach(id => processingRef.current.add(id));
|
||||
}
|
||||
}, [data]); // 只依赖data变化
|
||||
|
||||
|
||||
// 轮询文件状态
|
||||
useEffect(() => {
|
||||
// 如果文件列表为空,直接完成
|
||||
// 如果没有文件需要处理,直接完成
|
||||
if (fileIdsRef.current.length === 0) {
|
||||
setFinish(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 轮询函数
|
||||
// 轮询函数(添加并发控制)
|
||||
const pollFilesStatus = async () => {
|
||||
console.log(kid,fileIdsRef.current,22);
|
||||
|
||||
// 防止上一次请求未完成时发起新请求
|
||||
if (isPollingRef.current) return;
|
||||
isPollingRef.current = true;
|
||||
|
||||
try {
|
||||
// 只轮询仍在处理中的文件
|
||||
const pendingFileIds = Array.from(processingRef.current);
|
||||
if (pendingFileIds.length === 0) {
|
||||
clearInterval(timerRef.current);
|
||||
setFinish(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(kid,kId, pendingFileIds, 22);
|
||||
|
||||
const res = await readFileByLibDatabase({
|
||||
id: kid || kId,
|
||||
page: 0,
|
||||
pageSize: 0,
|
||||
file_ids: fileIdsRef.current
|
||||
file_ids: pendingFileIds // 只查询未完成的文件
|
||||
});
|
||||
|
||||
// 更新文件状态
|
||||
setFiles(prev => {
|
||||
const updatedFiles = [...prev];
|
||||
const resMap = new Map(res.data.map(item => [item.id, item]));
|
||||
return prev.map(file => {
|
||||
if (resMap.has(file.id)) {
|
||||
const resItem = resMap.get(file.id);
|
||||
let progress = 'await';
|
||||
let error = false;
|
||||
let reason = '';
|
||||
|
||||
pendingFileIds.forEach(fileId => {
|
||||
if (resMap.has(fileId)) {
|
||||
const resItem = resMap.get(fileId);
|
||||
const fileIndex = updatedFiles.findIndex(f => f.id === fileId);
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
let progress = updatedFiles[fileIndex].progress;
|
||||
let error = updatedFiles[fileIndex].error;
|
||||
let reason = updatedFiles[fileIndex].reason;
|
||||
|
||||
if (resItem.status === 2) {
|
||||
progress = 'end'; // 成功
|
||||
} else if (resItem.status === 3) {
|
||||
progress = 'end'; // 失败
|
||||
error = true;
|
||||
reason = resItem.remark;
|
||||
// 状态变为完成或失败时,从处理中集合移除
|
||||
if (resItem.status === 2) {
|
||||
progress = 'end'; // 成功
|
||||
processingRef.current.delete(fileId);
|
||||
} else if (resItem.status === 3) {
|
||||
progress = 'end'; // 失败
|
||||
error = true;
|
||||
reason = resItem.remark;
|
||||
processingRef.current.delete(fileId);
|
||||
}
|
||||
|
||||
updatedFiles[fileIndex] = {
|
||||
...updatedFiles[fileIndex],
|
||||
progress,
|
||||
error,
|
||||
reason
|
||||
};
|
||||
}
|
||||
|
||||
return { ...file, progress, error, reason };
|
||||
}
|
||||
return file;
|
||||
});
|
||||
|
||||
return updatedFiles;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("轮询文件状态出错:", e);
|
||||
} finally {
|
||||
isPollingRef.current = false; // 释放锁
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行第一次轮询,然后每5秒轮询一次
|
||||
pollFilesStatus();
|
||||
timerRef.current = setInterval(pollFilesStatus, 3000 + fileIdsRef.current.length * 10);
|
||||
timerRef.current = setInterval(pollFilesStatus, 5000); // 固定间隔,避免过频繁
|
||||
|
||||
// 清理定时器
|
||||
return () => clearInterval(timerRef.current);
|
||||
}, [kid]);
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, [kid, kId]); // 只依赖kid和kId变化
|
||||
|
||||
// 检查所有文件是否完成
|
||||
useEffect(() => {
|
||||
// 检查是否有文件仍在处理中
|
||||
const hasProcessingFiles = files.some(f => f.progress !== 'end');
|
||||
|
||||
// 只有当存在文件且没有文件在处理中时,才标记为完成
|
||||
if (files.length > 0 && !hasProcessingFiles) {
|
||||
console.log('所有文件处理完成');
|
||||
clearInterval(timerRef.current);
|
||||
setFinish(true);
|
||||
} else {
|
||||
// 仍有文件在处理中或文件列表为空
|
||||
if (files.length > 0) {
|
||||
const processingCount = files.filter(f => f.progress !== 'end').length;
|
||||
console.log(`仍有${processingCount}个文件正在处理中`);
|
||||
useEffect(() => {
|
||||
// 当处理中集合为空时,标记为完成
|
||||
if (processingRef.current.size === 0 && fileIdsRef.current.length > 0) {
|
||||
console.log('所有文件处理完成');
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
}
|
||||
setFinish(true);
|
||||
} else {
|
||||
setFinish(false);
|
||||
}
|
||||
setFinish(false);
|
||||
}
|
||||
}, [files]);
|
||||
console.log('fukes :>> ', data, files);
|
||||
}, [files]); // 依赖文件状态变化
|
||||
|
||||
const [details] = useKnowledgeDetails([kid])
|
||||
console.log('files :>> ', files);
|
||||
|
||||
let finalId = kid;
|
||||
if (kId) {
|
||||
finalId = kId.replace(/\D/g, '');
|
||||
}
|
||||
const [details] = useKnowledgeDetails([finalId])
|
||||
|
||||
const handleCreateFlow = async (params) => {
|
||||
const model = await getLlmDefaultModel()
|
||||
const flow = await getKnowledgeDefaultFlowTemplate(kid, details[0].name, model.model_id)
|
||||
console.log(details,9999);
|
||||
|
||||
const flow = await getKnowledgeDefaultFlowTemplate(finalId, details[0]?.name || '', model.model_id)
|
||||
const res = await captureAndAlertRequestErrorHoc(createWorkflowApi(
|
||||
"文档知识库问答-" + generateUUID(5),
|
||||
"检索文档知识库,根据检索结果进行回答。",
|
||||
@@ -148,8 +189,7 @@ useEffect(() => {
|
||||
</div>
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 保持getKnowledgeDefaultFlowTemplate函数不变
|
||||
const getKnowledgeDefaultFlowTemplate = async (kid, kname, modelId) => {
|
||||
const templates = await getWorkflowNodeTemplate()
|
||||
let startNode = null
|
||||
@@ -289,4 +329,5 @@ const getKnowledgeDefaultFlowTemplate = async (kid, kname, modelId) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function Paragraphs({ fileId, onBack }) {
|
||||
const searchInputRef = useRef(null);
|
||||
const isChangingRef = useRef(false);
|
||||
const [isInitReady, setIsInitReady] = useState(false);
|
||||
|
||||
const [previewUrl,setPreviewUrl] = useState()
|
||||
// 1. 修复:URL乱码(添加decodeURIComponent)+ 确保数据顺序
|
||||
const fetchFileUrl = useCallback(async (fileId) => {
|
||||
console.log('获取文件URL:', fileId);
|
||||
@@ -66,15 +66,20 @@ export default function Paragraphs({ fileId, onBack }) {
|
||||
const res = await getFilePathApi(fileId);
|
||||
const pares = await getFileBboxApi(fileId)
|
||||
setPartitions(pares)
|
||||
console.log('getFilePathApi 响应:', res,pares);
|
||||
console.log('getFilePathApi 响应:', res.original_url,pares);
|
||||
|
||||
// 修复:提取URL并解码(解决中文/特殊字符乱码)
|
||||
let url;
|
||||
if (res.data) {
|
||||
url = res.data.url || res.data.filePath || res.data;
|
||||
if (res) {
|
||||
url = res.original_url || res.data.filePath || res.url;
|
||||
if(res.preview_url){
|
||||
setPreviewUrl(res.preview_url)
|
||||
}
|
||||
} else {
|
||||
url = res.url || res.filePath || res;
|
||||
}
|
||||
console.log(url,2222);
|
||||
|
||||
const trimmedUrl = (url || '').trim();
|
||||
|
||||
if (!trimmedUrl) {
|
||||
@@ -363,6 +368,7 @@ export default function Paragraphs({ fileId, onBack }) {
|
||||
skipToStep: 2,
|
||||
fileId: selectedFileId,
|
||||
fileData: {
|
||||
previewUrl:previewUrl,
|
||||
id: currentFile?.id,
|
||||
name: currentFile?.name,
|
||||
split_rule: currentFile?.split_rule || currentFile?.fullData?.split_rule,
|
||||
|
||||
@@ -45,8 +45,9 @@ export default function PreviewFile({
|
||||
|
||||
const targetFile = matchedRawFile || file;
|
||||
const fileParseType = targetFile.parse_type;
|
||||
const fileName = targetFile.name || file.fileName;
|
||||
const fileName = targetFile.name || file.fileName||file.name;
|
||||
const suffix = useMemo(() => {
|
||||
console.log(fileName,3322);
|
||||
|
||||
return fileName?.split('.').pop()?.toLowerCase() || '';
|
||||
}, [fileName]);
|
||||
|
||||
@@ -29,6 +29,8 @@ export type Partition = {
|
||||
[key in string]: { text: string, type: string, part_id: string }
|
||||
}
|
||||
export default function PreviewResult({showPreview ,previewCount, rules, step, applyEachCell, cellGeneralConfig,kId,handlePreviewResult }: IProps) {
|
||||
console.log(showPreview,rules,567);
|
||||
|
||||
const { id } = useParams()
|
||||
|
||||
const [chunks, setChunks] = useState([]) // 当前文件分块
|
||||
@@ -71,13 +73,22 @@ export default function PreviewResult({showPreview ,previewCount, rules, step, a
|
||||
const { fileList, pageHeaderFooter, chunkOverlap, chunkSize, enableFormula, forceOcr, knowledgeId
|
||||
, retainImages, separator, separatorRule } = rules
|
||||
const currentFile = fileList.find(file => file.id === selectId)
|
||||
|
||||
let preview_url
|
||||
if(showPreview){
|
||||
preview_url = currentFile.previewUrl ||currentFile?.filePath
|
||||
}else {
|
||||
preview_url=currentFile?.filePath
|
||||
}
|
||||
console.log(preview_url ,currentFile,6789);
|
||||
|
||||
captureAndAlertRequestErrorHoc(previewFileSplitApi({
|
||||
// 缓存(修改规则后需要清空缓存, 切换文件使用缓存)
|
||||
// previewCount变更时为重新预览分段操作,不使用缓存
|
||||
cache: prevPreviewCountMapRef.current[currentFile.id] === previewCount,
|
||||
knowledge_id: id||kId,
|
||||
file_list: [{
|
||||
file_path: currentFile?.filePath,
|
||||
file_path:preview_url,
|
||||
excel_rule: applyEachCell
|
||||
? currentFile.excelRule
|
||||
: { ...cellGeneralConfig }
|
||||
@@ -151,7 +162,7 @@ export default function PreviewResult({showPreview ,previewCount, rules, step, a
|
||||
setChunks={setChunks}
|
||||
partitions={partitions}
|
||||
/>}
|
||||
<div className={cn('relative', 'w-1/2')}>
|
||||
<div className={cn('relative',)}>
|
||||
{/* 下拉框 - 右上角 */}
|
||||
{(step === 3 ||(step===2 &&showPreview))&&(
|
||||
<div className="flex justify-end">
|
||||
|
||||
@@ -251,14 +251,29 @@ export default function QasPage() {
|
||||
}
|
||||
}, [datalist])
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
const libname = window.libname // 临时记忆
|
||||
if (libname) {
|
||||
localStorage.setItem('libname', window.libname)
|
||||
// 修改 useEffect 中处理标题的部分
|
||||
useEffect(() => {
|
||||
// 处理 window.libname 可能的格式问题
|
||||
let libName = '';
|
||||
// 检查 window.libname 是否存在且有效
|
||||
if (window.libname) {
|
||||
// 处理数组格式([名称, 描述])
|
||||
if (Array.isArray(window.libname) && window.libname.length > 0) {
|
||||
libName = window.libname[0];
|
||||
}
|
||||
// 处理字符串格式
|
||||
else if (typeof window.libname === 'string') {
|
||||
libName = window.libname;
|
||||
}
|
||||
setTitle(window.libname[0] || localStorage.getItem('libname'))
|
||||
}, [])
|
||||
// 存储到 localStorage 时只存名称
|
||||
localStorage.setItem('libname', libName);
|
||||
}
|
||||
// 从 localStorage 获取备份
|
||||
else {
|
||||
libName = localStorage.getItem('libname') || '';
|
||||
}
|
||||
setTitle(libName || t('unknownKnowledgeBase')); // 提供默认文本
|
||||
}, []);
|
||||
const handleEnableSelected = async () => {
|
||||
if (!selectedItems.length) return;
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import AppAvator from "@/components/bs-comp/cardComponent/avatar";
|
||||
import MessagePanne from "@/components/bs-comp/chatComponent/MessagePanne";
|
||||
import { useMessageStore } from "@/components/bs-comp/chatComponent/messageStore";
|
||||
import { LoadingIcon } from "@/components/bs-icons/loading";
|
||||
import { Button } from "@/components/bs-ui/button";
|
||||
import ShadTooltip from "@/components/ShadTooltipComponent";
|
||||
import { getDeleteFlowApi } from "@/controllers/API/flow";
|
||||
import ChatMessages from "@/pages/BuildPage/flow/FlowChat/ChatMessages";
|
||||
import { useMessageStore as useFlowMessageStore } from "@/pages/BuildPage/flow/FlowChat/messageStore";
|
||||
import { useAssistantStore } from "@/store/assistantStore";
|
||||
import { AppNumType } from "@/types/app";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
@@ -47,6 +49,8 @@ export default function AppChatDetail() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const logo = useAppAvatar(cid, type)
|
||||
|
||||
return <div>
|
||||
{loading && <div className="absolute w-full h-full top-0 left-0 flex justify-center items-center z-10 bg-[rgba(255,255,255,0.6)] dark:bg-blur-shared">
|
||||
<LoadingIcon />
|
||||
@@ -66,8 +70,8 @@ export default function AppChatDetail() {
|
||||
</div>
|
||||
<div className="h-[calc(100vh-132px)]">
|
||||
{type === AppNumType.FLOW
|
||||
? <ChatMessages logo={''} debug useName={''} guideWord={''} loadMore={() => loadMoreFlowHistoryMsg(fid, true)} onMarkClick={null}></ChatMessages>
|
||||
: <MessagePanne logo='' useName='' guideWord=''
|
||||
? <ChatMessages logo={logo} debug useName={''} guideWord={''} loadMore={() => loadMoreFlowHistoryMsg(fid, true)} onMarkClick={null}></ChatMessages>
|
||||
: <MessagePanne logo={logo} useName='' guideWord=''
|
||||
loadMore={() => loadMoreHistoryMsg(fid, true)}
|
||||
></MessagePanne>
|
||||
}
|
||||
@@ -75,3 +79,19 @@ export default function AppChatDetail() {
|
||||
</div>
|
||||
</div >
|
||||
};
|
||||
|
||||
|
||||
const useAppAvatar = (cid, flowType) => {
|
||||
const [logo, setLogo] = useState('')
|
||||
const [title, setTitle] = useState('')
|
||||
const loadAppDetail = async () => {
|
||||
const lostflow = await getDeleteFlowApi(cid)
|
||||
setLogo(lostflow.flow_logo)
|
||||
setTitle(lostflow.flow_name)
|
||||
}
|
||||
useEffect(() => {
|
||||
loadAppDetail()
|
||||
}, [])
|
||||
|
||||
return <AppAvator id={title} flowType={flowType} url={logo} className="size-5 min-w-5"></AppAvator>
|
||||
}
|
||||
Reference in New Issue
Block a user