mirror of
https://github.com/dataelement/bisheng.git
synced 2026-09-24 23:19:52 +08:00
Merge branch 'feat/2.0.0' of github.com:dataelement/bisheng into feat/2.0.0
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
# 毕昇后端代码
|
||||
|
||||
* Dockerfile 使用 poetry 进行 Python 依赖管理
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ class SOPManageService:
|
||||
|
||||
if error_msg:
|
||||
error_msg = "、".join(error_msg)
|
||||
error_rows.extend(f"• 第{i}行: {error_msg}")
|
||||
error_rows.append(f"• 第{i}行: {error_msg}")
|
||||
else:
|
||||
success_rows.append({
|
||||
"name": str(name),
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
from ast import literal_eval
|
||||
from abc import ABC
|
||||
from ast import literal_eval
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
import jieba
|
||||
from bisheng_langchain.vectorstores.elastic_keywords_search import DEFAULT_PROMPT
|
||||
from bisheng_langchain.vectorstores.milvus import DEFAULT_MILVUS_CONNECTION
|
||||
from langchain.chains.llm import LLMChain
|
||||
from langchain.docstore.document import Document
|
||||
from langchain.embeddings.base import Embeddings
|
||||
@@ -15,6 +13,9 @@ from langchain_core.language_models import BaseLLM
|
||||
from langchain_core.prompts import PromptTemplate
|
||||
from loguru import logger
|
||||
|
||||
from bisheng_langchain.vectorstores.elastic_keywords_search import DEFAULT_PROMPT
|
||||
from bisheng_langchain.vectorstores.milvus import DEFAULT_MILVUS_CONNECTION
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from elasticsearch import Elasticsearch # noqa: F401
|
||||
|
||||
@@ -167,12 +168,12 @@ class MilvusWithPermissionCheck(MilvusLangchain):
|
||||
connections.remove_connection(using)
|
||||
|
||||
def _init(
|
||||
self,
|
||||
embeddings: Optional[list] = None,
|
||||
metadatas: Optional[list[dict]] = None,
|
||||
partition_names: Optional[list] = None,
|
||||
replica_number: int = 1,
|
||||
timeout: Optional[float] = None,
|
||||
self,
|
||||
embeddings: Optional[list] = None,
|
||||
metadatas: Optional[list[dict]] = None,
|
||||
partition_names: Optional[list] = None,
|
||||
replica_number: int = 1,
|
||||
timeout: Optional[float] = None,
|
||||
) -> None:
|
||||
self._extract_fields(col_index=0)
|
||||
self._create_search_params()
|
||||
@@ -227,18 +228,18 @@ class MilvusWithPermissionCheck(MilvusLangchain):
|
||||
|
||||
@classmethod
|
||||
def from_texts(
|
||||
cls,
|
||||
texts: List[str],
|
||||
embedding: Embeddings,
|
||||
metadatas: Optional[List[dict]] = None,
|
||||
collection_name: list[str] = None,
|
||||
connection_args: dict[str, Any] = DEFAULT_MILVUS_CONNECTION,
|
||||
consistency_level: str = 'Session',
|
||||
index_params: Optional[dict] = None,
|
||||
search_params: Optional[dict] = None,
|
||||
drop_old: bool = False,
|
||||
no_embedding: bool = False,
|
||||
**kwargs: Any,
|
||||
cls,
|
||||
texts: List[str],
|
||||
embedding: Embeddings,
|
||||
metadatas: Optional[List[dict]] = None,
|
||||
collection_name: list[str] = None,
|
||||
connection_args: dict[str, Any] = DEFAULT_MILVUS_CONNECTION,
|
||||
consistency_level: str = 'Session',
|
||||
index_params: Optional[dict] = None,
|
||||
search_params: Optional[dict] = None,
|
||||
drop_old: bool = False,
|
||||
no_embedding: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""
|
||||
no insert data into milvus, only search from milvus
|
||||
@@ -256,13 +257,13 @@ class MilvusWithPermissionCheck(MilvusLangchain):
|
||||
return vector_db
|
||||
|
||||
def similarity_search(
|
||||
self,
|
||||
query: str,
|
||||
k: int = 4,
|
||||
param: Optional[dict] = None,
|
||||
expr: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
self,
|
||||
query: str,
|
||||
k: int = 4,
|
||||
param: Optional[dict] = None,
|
||||
expr: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[Document]:
|
||||
"""Perform a similarity search against the query string.
|
||||
|
||||
@@ -288,13 +289,13 @@ class MilvusWithPermissionCheck(MilvusLangchain):
|
||||
return [doc for doc, _ in res]
|
||||
|
||||
def similarity_search_by_vector(
|
||||
self,
|
||||
embedding: List[float],
|
||||
k: int = 4,
|
||||
param: Optional[dict] = None,
|
||||
expr: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
self,
|
||||
embedding: List[float],
|
||||
k: int = 4,
|
||||
param: Optional[dict] = None,
|
||||
expr: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[Document]:
|
||||
"""Perform a similarity search against the query string.
|
||||
|
||||
@@ -320,13 +321,13 @@ class MilvusWithPermissionCheck(MilvusLangchain):
|
||||
return [doc for doc, _ in res]
|
||||
|
||||
def similarity_search_with_score(
|
||||
self,
|
||||
query: str,
|
||||
k: int = 4,
|
||||
param: Optional[dict] = None,
|
||||
expr: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
self,
|
||||
query: str,
|
||||
k: int = 4,
|
||||
param: Optional[dict] = None,
|
||||
expr: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[Tuple[Document, float]]:
|
||||
"""Perform a search on a query string and return results with score.
|
||||
|
||||
@@ -367,14 +368,14 @@ class MilvusWithPermissionCheck(MilvusLangchain):
|
||||
return res
|
||||
|
||||
def similarity_search_with_score_by_vector(
|
||||
self,
|
||||
embedding: List[float],
|
||||
k: int = 4,
|
||||
param: Optional[dict] = None,
|
||||
query: Optional[str] = None,
|
||||
expr: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
self,
|
||||
embedding: List[float],
|
||||
k: int = 4,
|
||||
param: Optional[dict] = None,
|
||||
query: Optional[str] = None,
|
||||
expr: Optional[str] = None,
|
||||
timeout: Optional[int] = None,
|
||||
**kwargs: Any,
|
||||
) -> List[Tuple[Document, float]]:
|
||||
"""Perform a search on a query string and return results with score.
|
||||
|
||||
@@ -452,8 +453,7 @@ class MilvusWithPermissionCheck(MilvusLangchain):
|
||||
@staticmethod
|
||||
def _relevance_score_fn(distance: float) -> float:
|
||||
"""Normalize the distance to a score on a scale [0, 1]."""
|
||||
# Todo: normalize the es score on a scale [0, 1]
|
||||
return 1 - distance
|
||||
return 1 - distance / 2
|
||||
|
||||
def _select_relevance_score_fn(self) -> Callable[[float], float]:
|
||||
return self._relevance_score_fn
|
||||
@@ -465,13 +465,13 @@ class ElasticsearchWithPermissionCheck(VectorStore, ABC):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
elasticsearch_url: str,
|
||||
index_name: List[str],
|
||||
drop_old: Optional[bool] = False,
|
||||
*,
|
||||
ssl_verify: Optional[Dict[str, Any]] = None,
|
||||
llm_chain: Optional[LLMChain] = None,
|
||||
self,
|
||||
elasticsearch_url: str,
|
||||
index_name: List[str],
|
||||
drop_old: Optional[bool] = False,
|
||||
*,
|
||||
ssl_verify: Optional[Dict[str, Any]] = None,
|
||||
llm_chain: Optional[LLMChain] = None,
|
||||
):
|
||||
"""Initialize with necessary components."""
|
||||
try:
|
||||
@@ -572,17 +572,17 @@ class ElasticsearchWithPermissionCheck(VectorStore, ABC):
|
||||
|
||||
@classmethod
|
||||
def from_texts(
|
||||
cls,
|
||||
texts: List[str],
|
||||
embedding: Embeddings,
|
||||
metadatas: Optional[List[dict]] = None,
|
||||
ids: Optional[List[str]] = None,
|
||||
index_name: Optional[List[str]] = None,
|
||||
refresh_indices: bool = True,
|
||||
llm: Optional[BaseLLM] = None,
|
||||
prompt: Optional[PromptTemplate] = DEFAULT_PROMPT,
|
||||
drop_old: Optional[bool] = False,
|
||||
**kwargs: Any,
|
||||
cls,
|
||||
texts: List[str],
|
||||
embedding: Embeddings,
|
||||
metadatas: Optional[List[dict]] = None,
|
||||
ids: Optional[List[str]] = None,
|
||||
index_name: Optional[List[str]] = None,
|
||||
refresh_indices: bool = True,
|
||||
llm: Optional[BaseLLM] = None,
|
||||
prompt: Optional[PromptTemplate] = DEFAULT_PROMPT,
|
||||
drop_old: Optional[bool] = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Construct ElasticKeywordsSearch wrapper from raw documents.
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ SopPrompt = """<背景>
|
||||
|
||||
使用@来说明使用什么工具,文件以及知识库,必须遵循以下格式:
|
||||
工具的表示方法:@工具名@
|
||||
用户上传文件的表示方法:@文件名的文件储存信息:{'文件储存在语义检索库中的id':'{id}','文件储存地址':'{文件路径}'}@
|
||||
知识库的表示方法:@知识库名的储存信息:{'知识库储存在语义检索库中的id':'{id}'}@
|
||||
用户上传文件的表示方法:@文件名的文件储存信息:{{'文件储存在语义检索库中的id':'{{id}}','文件储存地址':'{{文件路径}}'}}@
|
||||
知识库的表示方法:@知识库名的储存信息:{{'知识库储存在语义检索库中的id':'{{id}}'}}@
|
||||
输出文件的表示方法:文件名
|
||||
</指导手册格式要求>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -10,6 +11,7 @@ import { useAddedResponse, useChatHelpers, useSSE } from '~/hooks';
|
||||
import { AddedChatContext, ChatContext, ChatFormProvider, useFileMapContext } from '~/Providers';
|
||||
import store from '~/store';
|
||||
import { buildTree, cn } from '~/utils';
|
||||
import { Button } from '../ui';
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
import ChatForm from './Input/ChatForm';
|
||||
import InvitationCodeForm from './InviteCode';
|
||||
@@ -84,6 +86,8 @@ function ChatView({ index = 0 }: { index?: number }) {
|
||||
window.open(`${__APP_ENV__.BASE_URL}/linsight/${caseId}`)
|
||||
}
|
||||
|
||||
const selfHost = location.host.indexOf('bisheng') !== -1;
|
||||
|
||||
return (
|
||||
<ChatFormProvider {...methods}>
|
||||
<ChatContext.Provider value={chatHelpers}>
|
||||
@@ -120,14 +124,16 @@ function ChatView({ index = 0 }: { index?: number }) {
|
||||
{/* input */}
|
||||
<ChatForm isLingsi={isLingsi} setShowCode={setShowCode} index={index} />
|
||||
{/* <Footer /> */}
|
||||
{isLingsi && <div className='relative mt-20'>
|
||||
{isLingsi && selfHost && <div className='relative mt-20'>
|
||||
<p className='text-sm text-center text-gray-400'>灵思精选案例</p>
|
||||
<div className='flex pt-4 justify-center md:max-w-2xl xl:max-w-3xl mx-auto gap-8'>
|
||||
<div className='border rounded-xl p-4 text-sm hover:shadow-xl cursor-pointer' onClick={() => handleCardClick('case_0001')}>
|
||||
<div className='relative border rounded-xl py-8 p-5 text-sm hover:shadow-xl cursor-pointer' onClick={() => handleCardClick('case_0001')}>
|
||||
<Button className='absolute top-3 right-3 p-0 h-8 w-8 shadow-md border-none' variant="outline" size="icon"><ArrowRight /></Button>
|
||||
<p className='font-bold'>查询近 7 日的销售数据趋势</p>
|
||||
<p className='mt-2 text-gray-400'>基于客户数据识别不同群体对智能投资顾问的需求和偏好如高收入、高参与度客户或区域差异。</p>
|
||||
</div>
|
||||
<div className='border rounded-xl p-4 text-sm hover:shadow-xl cursor-pointer' onClick={() => handleCardClick('case_0002')}>
|
||||
<div className='relative border rounded-xl py-8 p-5 text-sm hover:shadow-xl cursor-pointer' onClick={() => handleCardClick('case_0001')}>
|
||||
<Button className='absolute top-3 right-3 p-0 h-8 w-8 shadow-md border-none' variant="outline" size="icon"><ArrowRight /></Button>
|
||||
<p className='font-bold'>查询近 7 日的销售数据趋势</p>
|
||||
<p className='mt-2 text-gray-400'>基于客户数据识别不同群体对智能投资顾问的需求和偏好如高收入、高参与度客户或区域差异。</p>
|
||||
</div>
|
||||
|
||||
@@ -285,6 +285,7 @@ const ChatForm = ({ isLingsi, setShowCode, index = 0 }) => {
|
||||
removeFocusRings,
|
||||
'transition-[max-height] duration-200',
|
||||
'transition-[height] duration-500',
|
||||
isLingsi ? 'min-h-32' : 'min-h-24'
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { saveSop, startLinsight } from '~/api/linsight';
|
||||
import { useLinsightManager, useLinsightSessionManager } from '~/hooks/useLinsightManager';
|
||||
import { Button, Textarea } from '../ui';
|
||||
import SopMarkdown from './SopMarkdown';
|
||||
import ErrorDisplay from './components/ErrorDisplay';
|
||||
|
||||
export const enum SopStatus {
|
||||
/* 未开始 */
|
||||
@@ -159,7 +160,7 @@ export const SOPEditor = ({ versionId, sopError, onRun }) => {
|
||||
<div className='flex items-center justify-between border-b border-b-[#E8E9ED] bg-[#FDFEFF] p-2 px-4 text-[13px] text-[#737780] rounded-t-2xl'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<PencilLineIcon size={14} />
|
||||
SOP编辑器
|
||||
指导手册编辑器
|
||||
</div>
|
||||
{/* <CopyButton text={linsight.sop} /> */}
|
||||
</div>
|
||||
@@ -167,7 +168,11 @@ export const SOPEditor = ({ versionId, sopError, onRun }) => {
|
||||
{linsight.status === SopStatus.SopGenerating && !linsight.sop?.trim() && <p className='p-6 text-sm flex gap-2'>
|
||||
<img className='size-5' src={__APP_ENV__.BASE_URL + '/assets/load.webp'} alt="" />
|
||||
</p>}
|
||||
<p className={linsight.sopError ? 'bg-red-100 p-2 rounded-md text-sm text-red-500 m-2' : 'hidden'}>SOP生成失败:{linsight.sopError}</p>
|
||||
{linsight.sopError &&
|
||||
<div className='p-2 m-2'>
|
||||
<ErrorDisplay title="SOP生成失败" taskError={linsight.sopError} />
|
||||
</div>
|
||||
}
|
||||
<div className={`p-8 linsight-markdown flex-1 min-h-0 ${linsight.sopError && 'hidden'}`}>
|
||||
<SopMarkdown ref={markdownRef} linsight={linsight} edit={showSopEdit} onChange={handleChange} />
|
||||
</div>
|
||||
@@ -183,14 +188,14 @@ export const SOPEditor = ({ versionId, sopError, onRun }) => {
|
||||
{/* <span className='text-lg'>SOP</span> */}
|
||||
<p className='mt-0.5 text-sm flex gap-2'>
|
||||
<img className='size-5' src={__APP_ENV__.BASE_URL + '/assets/load.webp'} alt="" />
|
||||
确认是否可以按照 SOP 执⾏任务
|
||||
确认是否可以按照指导手册执⾏任务
|
||||
</p>
|
||||
<div className='absolute right-4 bottom-3 flex gap-2'>
|
||||
<Button variant="outline" className="px-3" onClick={() => {
|
||||
const sop = markdownRef.current.getValue()
|
||||
sop?.trim() === '' ? handleReExcute('') : setOpenAreaText(true)
|
||||
}}>
|
||||
重新生成 SOP
|
||||
重新生成指导手册
|
||||
</Button>
|
||||
<Button className="px-6" disabled={sopError || disabled} onClick={handleRun}>
|
||||
开始执行
|
||||
|
||||
@@ -138,7 +138,9 @@ const SopMarkdown = forwardRef<MarkdownRef, MarkdownProps>((props, ref) => {
|
||||
getMarkdownPaste(editorElement, (text) => {
|
||||
const value = replaceBracesToMarkers(text, nameToValueRef.current)
|
||||
const name = replaceMarkersToBraces(value, valueToNameRef.current, nameToValueRef.current)
|
||||
vditor.insertValue(name);
|
||||
// inset方法可及时渲染;update可覆盖选区; 只有包含变量使用inset,尽量使用update
|
||||
const regex = /\{\{[@#](.*?)[@#]\}\}/g;
|
||||
regex.test(name) ? vditor.insertValue(name) : vditor.updateValue(name)
|
||||
})
|
||||
},
|
||||
input: (val) => onChange(replaceBracesToMarkers(val, nameToValueRef.current)),
|
||||
|
||||
@@ -118,7 +118,7 @@ export const TaskFlow = ({ versionId, setVersions, setVersionId }) => {
|
||||
alt="Loading"
|
||||
/>
|
||||
</div>
|
||||
<p className='text-sm text-gray-400 mt-7'>确认SOP规划后,任务开始运行</p>
|
||||
<p className='text-sm text-gray-400 mt-7'>确认指导手册规划后,任务开始运行</p>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@ import FilePreviewDrawer from './FilePreviewDrawer';
|
||||
import { SopStatus } from './SOPEditor';
|
||||
import FileDrawer from './TaskFiles';
|
||||
import ErrorDisplay from './components/ErrorDisplay';
|
||||
import { PlaySop } from './components/SopLoading';
|
||||
|
||||
const ToolButtonLink = ({ params, setCurrentDirectFile }) => {
|
||||
if (!params) return null
|
||||
@@ -285,7 +286,7 @@ const Task = ({
|
||||
|
||||
|
||||
export const TaskFlowContent = ({ linsight, sendInput }) => {
|
||||
const { status, title, tasks, taskError, summary, file_list: files, queueCount = 0 } = linsight
|
||||
const { status, sop, title, tasks, taskError, summary, file_list: files, queueCount = 0 } = linsight
|
||||
const allFiles = linsight?.output_result?.all_from_session_files || []
|
||||
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
@@ -351,8 +352,9 @@ export const TaskFlowContent = ({ linsight, sendInput }) => {
|
||||
<img className='size-5' src={__APP_ENV__.BASE_URL + '/assets/load.webp'} alt="" />
|
||||
正在整理内容...
|
||||
</p>}
|
||||
{/* {!tasks?.length && <PlaySop content={sop} />} */}
|
||||
{/* 任务 */}
|
||||
{!!tasks?.length && <div className='pl-6'>
|
||||
{!!tasks?.length && status === SopStatus.Running && <div className='pl-6'>
|
||||
<p className='text-sm text-gray-400 mt-6 mb-4'>规划任务执行路径:</p>
|
||||
{tasks.map((task, i) => (
|
||||
<p key={task.id} className='leading-7'>{i + 1}. {task.name}</p>
|
||||
@@ -458,6 +460,7 @@ export const TaskFlowContent = ({ linsight, sendInput }) => {
|
||||
<img className='size-5' src={__APP_ENV__.BASE_URL + '/assets/load.webp'} alt="" />
|
||||
</p>
|
||||
}
|
||||
|
||||
{/* 文件列表抽屉 */}
|
||||
<FileDrawer
|
||||
title={title}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
// LoadingBox组件
|
||||
@@ -8,13 +10,123 @@ export const LoadingBox = () => {
|
||||
<div className='w-[194px] h-[102px] bg-no-repeat mx-auto rounded-md bg-white'
|
||||
style={{ backgroundImage: `url(${__APP_ENV__.BASE_URL}/assets/linsi-load.png)` }}></div>
|
||||
</div>
|
||||
<h1 className='text-2xl mt-10'>为您提供详细 SOP,以确保任务精准</h1>
|
||||
<p className='mt-5'>灵思正在为您规划 SOP...</p>
|
||||
<h1 className='text-2xl mt-10'>为您提供详细指导手册,以确保任务精准</h1>
|
||||
<p className='mt-5'>灵思正在为您规划指导手册...</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
interface PlaySopProps {
|
||||
content: string[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
|
||||
export const PlaySop = ({ content: _content, className }: PlaySopProps) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
const content = useMemo(() => _content.split('\n').filter((item) => item.trim() !== ''), [_content])
|
||||
|
||||
useEffect(() => {
|
||||
if (!content || content.length === 0) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setIsVisible(false)
|
||||
|
||||
setTimeout(() => {
|
||||
setCurrentIndex((prev) => (prev + 1) % content.length)
|
||||
setIsVisible(true)
|
||||
}, 500)
|
||||
}, 1400)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [content])
|
||||
|
||||
if (!content || content.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className={cn("relative h-32 shadow-2xl rounded-xl overflow-hidden mt-6", className)}>
|
||||
{/* Video Background */}
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
preload="auto"
|
||||
className={cn(
|
||||
"absolute size-full object-fill object-center",
|
||||
"transition-opacity duration-500 ease-out",
|
||||
)}
|
||||
// src={`${__APP_ENV__.BASE_URL}/assets/linsi-bg.mp4`}
|
||||
>
|
||||
<source
|
||||
src={`${__APP_ENV__.BASE_URL}/assets/linsi-bg.mp4`}
|
||||
type="video/mp4"
|
||||
/>
|
||||
<img
|
||||
src={`${__APP_ENV__.BASE_URL}/assets/lingsi-bg.png`}
|
||||
alt=""
|
||||
/>
|
||||
</video>
|
||||
|
||||
{/* Overlay for better text visibility */}
|
||||
{/* <div className="absolute inset-0 bg-black/10" /> */}
|
||||
|
||||
{/* Animated Text Content */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<AnimatePresence mode="wait">
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
key={currentIndex}
|
||||
initial={{
|
||||
y: 100,
|
||||
opacity: 0,
|
||||
scale: 0.8,
|
||||
}}
|
||||
animate={{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
}}
|
||||
exit={{
|
||||
y: -20,
|
||||
opacity: 0,
|
||||
scale: 0.3,
|
||||
filter: "blur(4px)",
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: [0.25, 0.46, 0.45, 0.94],
|
||||
exit: {
|
||||
duration: 0,
|
||||
ease: [0.55, 0.085, 0.68, 0.53],
|
||||
},
|
||||
}}
|
||||
className="text-center px-4"
|
||||
>
|
||||
<motion.p
|
||||
className="text-#666 text-lg font-medium drop-shadow-lg"
|
||||
initial={{ letterSpacing: "0.1em" }}
|
||||
animate={{ letterSpacing: "0.05em" }}
|
||||
exit={{ letterSpacing: "0.2em" }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
{content[currentIndex]}
|
||||
</motion.p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Subtle vignette effect */}
|
||||
<div className="absolute inset-0 bg-gradient-radial from-transparent via-transparent to-black/10" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface LoadingDotsProps {
|
||||
className?: string
|
||||
size?: "sm" | "md" | "lg"
|
||||
|
||||
@@ -391,6 +391,8 @@ export const useGenerateSop = (versionId, setVersionId, setVersions) => {
|
||||
|
||||
updateLinsight(versionId, {
|
||||
status: SopStatus.SopGenerating,
|
||||
taskError: '',
|
||||
sopError: '',
|
||||
sop: ''
|
||||
})
|
||||
clearLinsightSubmission(versionId)
|
||||
|
||||
@@ -2797,7 +2797,7 @@ button.scroll-convo {
|
||||
|
||||
.linsight-vditor .vditor-reset {
|
||||
font-size: 14px;
|
||||
padding: 0 20px 160px 0 !important;
|
||||
padding: 0 20px 160px 14px !important;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
@@ -2827,6 +2827,23 @@ button.scroll-convo {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.linsight-vditor .vditor-wysiwyg>.vditor-reset>h1:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h2:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h3:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h4:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h5:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h6:before,
|
||||
.vditor-wysiwyg div.vditor-wysiwyg__block:before,
|
||||
.vditor-wysiwyg div[data-type="link-ref-defs-block"]:before,
|
||||
.vditor-wysiwyg div[data-type="footnotes-block"]:before,
|
||||
.vditor-wysiwyg .vditor-toc:before {
|
||||
margin-left: -20px;
|
||||
padding-right: 0;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.linsi-span {
|
||||
background: #E9F1FF;
|
||||
color: #2A3F73;
|
||||
|
||||
@@ -23,7 +23,7 @@ export default defineConfig({
|
||||
// changeOrigin: true,
|
||||
// },
|
||||
'/workspace/bisheng': {
|
||||
target: "http://192.168.106.120:3002",
|
||||
target: "http://192.168.106.20:3001",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => {
|
||||
@@ -31,7 +31,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
'/workspace/api': {
|
||||
target: 'http://192.168.106.120:3002',
|
||||
target: 'http://192.168.106.20:3001',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
ws: true,
|
||||
|
||||
@@ -292,8 +292,8 @@
|
||||
|
||||
.linsight-vditor .vditor-reset {
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
padding: 0 20px 160px 10px !important;
|
||||
font-size: 16px;
|
||||
padding: 0 20px 160px 14px !important;
|
||||
scrollbar-color: transparent transparent;
|
||||
}
|
||||
|
||||
@@ -304,7 +304,22 @@
|
||||
.linsight-vditor .vditor-wysiwyg pre.vditor-reset[contenteditable="false"] {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.linsight-vditor .vditor-wysiwyg>.vditor-reset>h1:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h2:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h3:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h4:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h5:before,
|
||||
.vditor-wysiwyg>.vditor-reset>h6:before,
|
||||
.vditor-wysiwyg div.vditor-wysiwyg__block:before,
|
||||
.vditor-wysiwyg div[data-type="link-ref-defs-block"]:before,
|
||||
.vditor-wysiwyg div[data-type="footnotes-block"]:before,
|
||||
.vditor-wysiwyg .vditor-toc:before {
|
||||
margin-left: -20px;
|
||||
padding-right: 0;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
left: 6px;
|
||||
}
|
||||
.linsi-span {
|
||||
background: #E9F1FF;
|
||||
color: #2A3F73;
|
||||
|
||||
@@ -164,7 +164,6 @@ export async function getAppsApi({ page = 1, pageSize = 20, keyword, tag_id = -1
|
||||
if (item.flow_type !== 5) return item
|
||||
return {
|
||||
...item,
|
||||
description: item.desc,
|
||||
version_list: item.version_list || [],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -173,7 +173,7 @@ ${t('build.exampleTwo', { ns: 'bs' })}
|
||||
// 使用默认模型 清空知识库和工具
|
||||
const [workflow, assistant] = await Promise.all([getLlmDefaultModel(), getAssistantModelConfig()])
|
||||
const workflowModelId = workflow.model_id
|
||||
const assistantModelId = assistant.llm_list.find(item => item.default).model_id
|
||||
const assistantModelId = assistant.llm_list.find(item => item.default)?.model_id
|
||||
delete tempDataRef.current.data.source
|
||||
|
||||
tempDataRef.current.data.nodes.forEach(node => {
|
||||
|
||||
@@ -110,16 +110,16 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||
const [localFileDialogOpen, setLocalFileDialogOpen] = useState(false);
|
||||
const [importFile, setImportFile] = useState<File | null>(null);
|
||||
const [importFiles, setImportFiles] = useState<File[]>([]);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [validationDialog, setValidationDialog] = useState({
|
||||
open: false,
|
||||
status_message: "共计划导入0条SOP,格式正确0条,错误0条"
|
||||
});
|
||||
const [importFilesData, setImportFilesData] = useState<File[]>([]);
|
||||
const [importFile, setImportFile] = useState<File | null>(null);
|
||||
const [importFiles, setImportFiles] = useState<File[]>([]);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [validationDialog, setValidationDialog] = useState({
|
||||
open: false,
|
||||
status_message: "共计划导入0条SOP,格式正确0条,错误0条"
|
||||
});
|
||||
const [importFilesData, setImportFilesData] = useState<File[]>([]);
|
||||
const [duplicateNames, setDuplicateNames] = useState<string[]>([]);
|
||||
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
||||
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
||||
const [deleteConfirmModal, setDeleteConfirmModal] = useState({
|
||||
open: false,
|
||||
title: '确认删除',
|
||||
@@ -165,7 +165,7 @@ const [validationDialog, setValidationDialog] = useState({
|
||||
prompt: ''
|
||||
},
|
||||
linsightConfig: {
|
||||
input_placeholder: '请输入你的任务目标,然后交给 BISHENG 灵思',
|
||||
input_placeholder: '灵思是一位擅长完成复杂任务的Agent助理,除了描述任务目标外,您还可以用通俗的语言描述希望如何实现,这将有助于得到符合您预期的结果~',
|
||||
tools: []
|
||||
}
|
||||
};
|
||||
@@ -175,7 +175,7 @@ const [validationDialog, setValidationDialog] = useState({
|
||||
api: [],
|
||||
mcp: []
|
||||
});
|
||||
const [importFormData, setImportFormData] = useState<FormData | null>(null);
|
||||
const [importFormData, setImportFormData] = useState<FormData | null>(null);
|
||||
|
||||
const fetchTools = async (type: 'builtin' | 'api' | 'mcp') => {
|
||||
setLoading(true);
|
||||
@@ -246,13 +246,13 @@ const [importFormData, setImportFormData] = useState<FormData | null>(null);
|
||||
.filter(Boolean);
|
||||
}, [toolsData, activeToolTab, toolSearchTerm]);
|
||||
|
||||
// const refreshSopList = () => {
|
||||
// fetchData({
|
||||
// page: page,
|
||||
// pageSize: 10,
|
||||
// keyword: keywords
|
||||
// });
|
||||
// };
|
||||
// const refreshSopList = () => {
|
||||
// fetchData({
|
||||
// page: page,
|
||||
// pageSize: 10,
|
||||
// keyword: keywords
|
||||
// });
|
||||
// };
|
||||
const fetchData = async (params: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
@@ -406,24 +406,24 @@ const [importFormData, setImportFormData] = useState<FormData | null>(null);
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: '' });
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useContext(locationContext)
|
||||
const handleSearch = (keyword: string, resetPage: boolean = false) => {
|
||||
const newKeywords = keyword;
|
||||
const newPage = resetPage || newKeywords.trim() === '' ? 1 : page;
|
||||
const { appConfig } = useContext(locationContext)
|
||||
const handleSearch = (keyword: string, resetPage: boolean = false) => {
|
||||
const newKeywords = keyword;
|
||||
const newPage = resetPage || newKeywords.trim() === '' ? 1 : page;
|
||||
|
||||
fetchData({
|
||||
keyword: newKeywords,
|
||||
page: newPage,
|
||||
pageSize
|
||||
});
|
||||
fetchData({
|
||||
keyword: newKeywords,
|
||||
page: newPage,
|
||||
pageSize
|
||||
});
|
||||
|
||||
// 更新状态
|
||||
setKeywords(newKeywords);
|
||||
setPageInputValue(newPage.toString());
|
||||
if (newPage !== page) {
|
||||
setPage(newPage);
|
||||
}
|
||||
};
|
||||
// 更新状态
|
||||
setKeywords(newKeywords);
|
||||
setPageInputValue(newPage.toString());
|
||||
if (newPage !== page) {
|
||||
setPage(newPage);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗刷新下数据
|
||||
useEffect(() => {
|
||||
@@ -710,94 +710,94 @@ const [importFormData, setImportFormData] = useState<FormData | null>(null);
|
||||
return [...tools];
|
||||
});
|
||||
}, []);
|
||||
const { getRootProps: getLocalFileRootProps, getInputProps: getLocalFileInputProps } = useDropzone({
|
||||
multiple: false,
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (acceptedFiles.length === 0) return;
|
||||
|
||||
const file = acceptedFiles[0];
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
if (ext !== 'xlsx') {
|
||||
message({ variant: 'warning', description: '请上传xlsx格式的文件' });
|
||||
return;
|
||||
const { getRootProps: getLocalFileRootProps, getInputProps: getLocalFileInputProps } = useDropzone({
|
||||
multiple: false,
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (acceptedFiles.length === 0) return;
|
||||
|
||||
const file = acceptedFiles[0];
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
if (ext !== 'xlsx') {
|
||||
message({ variant: 'warning', description: '请上传xlsx格式的文件' });
|
||||
return;
|
||||
}
|
||||
setImportFiles(acceptedFiles);
|
||||
}
|
||||
setImportFiles(acceptedFiles);
|
||||
}
|
||||
});
|
||||
const handleLocalFileImport = async () => {
|
||||
setIsImporting(true);
|
||||
try {
|
||||
});
|
||||
const handleLocalFileImport = async () => {
|
||||
setIsImporting(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', importFiles[0]);
|
||||
|
||||
setImportFilesData([importFiles[0]]); // 只保存一个文件
|
||||
const res = await sopApi.UploadSopRecord(formData);
|
||||
console.log('API Response:', res); // 调试用
|
||||
if (res.status_code === 11010) {
|
||||
setValidationDialog({
|
||||
open: true,
|
||||
status_message: res.status_message
|
||||
});
|
||||
}
|
||||
if (res.status_code === 200) {
|
||||
const formData = new FormData();
|
||||
importFilesData.forEach(file => {
|
||||
formData.append('file', file);
|
||||
});
|
||||
formData.append('ignore_error', 'false');
|
||||
formData.append('override', 'false');
|
||||
formData.append('save_new', 'false');
|
||||
const res = await sopApi.UploadSopRecord(formData);
|
||||
if (res?.repeat_name) {
|
||||
setImportDialogOpen(true)
|
||||
setDuplicateNames(res.repeat_name);
|
||||
setDuplicateDialogOpen(true);
|
||||
setImportFormData(formData);
|
||||
} else {
|
||||
sopApi.getSopList({
|
||||
page_size: pageSize,
|
||||
page: 1,
|
||||
keywords: keywords,
|
||||
})
|
||||
toast({ variant: 'success', description: '提交成功' });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
const handleValidationDialogConfirm = async () => {
|
||||
console.log(111);
|
||||
const formData = new FormData();
|
||||
formData.append('file', importFiles[0]);
|
||||
|
||||
setImportFilesData([importFiles[0]]); // 只保存一个文件
|
||||
const res = await sopApi.UploadSopRecord(formData);
|
||||
console.log('API Response:', res); // 调试用
|
||||
if(res.status_code === 11010){
|
||||
setValidationDialog({
|
||||
open: true,
|
||||
status_message: res.status_message
|
||||
});
|
||||
}
|
||||
if(res.status_code === 200){
|
||||
const formData = new FormData();
|
||||
importFilesData.forEach(file => {
|
||||
formData.append('file', file);
|
||||
});
|
||||
formData.append('ignore_error', 'false');
|
||||
formData.append('ignore_error', 'true');
|
||||
formData.append('override', 'false');
|
||||
formData.append('save_new', 'false');
|
||||
const res = await sopApi.UploadSopRecord(formData);
|
||||
if(res?.repeat_name){
|
||||
setImportDialogOpen(true)
|
||||
setDuplicateNames(res.repeat_name);
|
||||
setDuplicateDialogOpen(true);
|
||||
setImportFormData(formData);
|
||||
}else{
|
||||
sopApi.getSopList({
|
||||
page_size: pageSize,
|
||||
page: 1,
|
||||
keywords: keywords,
|
||||
})
|
||||
toast({ variant: 'success', description: '提交成功' });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
const handleValidationDialogConfirm = async () => {
|
||||
console.log(111);
|
||||
const formData = new FormData();
|
||||
importFilesData.forEach(file => {
|
||||
formData.append('file', file);
|
||||
});
|
||||
formData.append('ignore_error', 'true');
|
||||
formData.append('override', 'false');
|
||||
formData.append('save_new', 'false');
|
||||
|
||||
setValidationDialog(prev => ({...prev, open: false}));
|
||||
|
||||
|
||||
setValidationDialog(prev => ({ ...prev, open: false }));
|
||||
|
||||
|
||||
const res = await sopApi.UploadSopRecord(formData);
|
||||
if(res?.repeat_name){
|
||||
|
||||
const res = await captureAndAlertRequestErrorHoc(sopApi.UploadSopRecord(formData));
|
||||
if (res?.repeat_name) {
|
||||
setImportDialogOpen(true)
|
||||
setDuplicateNames(res.repeat_name);
|
||||
setDuplicateDialogOpen(true);
|
||||
setImportFormData(formData);
|
||||
return
|
||||
}else{
|
||||
sopApi.getSopList({
|
||||
page_size: pageSize,
|
||||
page: 1,
|
||||
keywords: keywords,
|
||||
})
|
||||
toast({ variant: 'success', description: '提交成功' });
|
||||
setDuplicateNames(res.repeat_name);
|
||||
setDuplicateDialogOpen(true);
|
||||
setImportFormData(formData);
|
||||
return
|
||||
} else {
|
||||
sopApi.getSopList({
|
||||
page_size: pageSize,
|
||||
page: 1,
|
||||
keywords: keywords,
|
||||
})
|
||||
toast({ variant: 'success', description: '提交成功' });
|
||||
}
|
||||
};
|
||||
};
|
||||
return (
|
||||
<div className=" h-full overflow-y-scroll scrollbar-hide relative bg-background-main">
|
||||
{loading && (
|
||||
@@ -871,18 +871,18 @@ const handleValidationDialogConfirm = async () => {
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => {setImportDialogOpen(true);setImportFilesData(null)}}
|
||||
onClick={() => { setImportDialogOpen(true); setImportFilesData(null) }}
|
||||
>
|
||||
从运行记录导入
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setLocalFileDialogOpen(true)}
|
||||
>
|
||||
从本地文件导入
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setLocalFileDialogOpen(true)}
|
||||
>
|
||||
从本地文件导入
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -932,12 +932,12 @@ const handleValidationDialogConfirm = async () => {
|
||||
tools={selectedTools}
|
||||
onOpenChange={setImportDialogOpen}
|
||||
// onSuccess={refreshSopList}
|
||||
setDuplicateNames={setDuplicateNames}
|
||||
duplicateNames={duplicateNames}
|
||||
duplicateDialogOpen={duplicateDialogOpen}
|
||||
setDuplicateDialogOpen={setDuplicateDialogOpen}
|
||||
importFormData={importFormData}
|
||||
/>
|
||||
setDuplicateNames={setDuplicateNames}
|
||||
duplicateNames={duplicateNames}
|
||||
duplicateDialogOpen={duplicateDialogOpen}
|
||||
setDuplicateDialogOpen={setDuplicateDialogOpen}
|
||||
importFormData={importFormData}
|
||||
/>
|
||||
{/* 表格区域 */}
|
||||
<SopTable datalist={datalist} selectedItems={selectedItems} handleSelectItem={handleSelectItem} handleSelectAll={handleSelectAll} handleSort={handleSort} handleEdit={handleEdit} handleDelete={handleDelete} page={page} pageSize={pageSize} total={total} loading={loading} pageInputValue={pageInputValue} handlePageChange={handlePageChange} handlePageInputChange={handlePageInputChange} handlePageInputConfirm={handlePageInputConfirm} handleKeyDown={handleKeyDown} />
|
||||
{deleteConfirmModal.open && (
|
||||
@@ -990,39 +990,41 @@ const handleValidationDialogConfirm = async () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Dialog open={localFileDialogOpen} onOpenChange={setLocalFileDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[1200px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>导入指导手册</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogContent className="sm:max-w-[1200px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>导入指导手册</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-red-500">*</span>
|
||||
<span>请上传文件</span>
|
||||
<button
|
||||
className="text-blue-600 hover:underline ml-auto"
|
||||
onClick={() => downloadJson(sampleData)}
|
||||
>
|
||||
<span className="text-black">示例文件:</span>
|
||||
用户指导手册格式示例.xlsx
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-red-500">*</span>
|
||||
<span>请上传文件</span>
|
||||
<button
|
||||
className="text-blue-600 hover:underline ml-auto"
|
||||
onClick={() => downloadJson(sampleData)}
|
||||
>
|
||||
<span className="text-black">示例文件:</span>
|
||||
用户指导手册格式示例.xlsx
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
{...getLocalFileRootProps()}
|
||||
className="group h-40 border border-dashed rounded-md flex flex-col justify-center items-center cursor-pointer gap-3 hover:border-primary"
|
||||
>
|
||||
<input {...getLocalFileInputProps()} />
|
||||
<UploadIcon className="group-hover:text-primary size-5" />
|
||||
<p className="text-sm">{t('code.clickOrDragHere')}</p>
|
||||
{importFiles.length > 0 && (
|
||||
<div className="w-full max-h-32 overflow-y-auto">
|
||||
{importFiles.slice(0, 1).map((file, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-1 bg-gray-200 mt-14">
|
||||
<span className="text-sm text-gray-600 truncate max-w-xs">
|
||||
{file.name}
|
||||
</span>
|
||||
<button
|
||||
<div
|
||||
{...getLocalFileRootProps()}
|
||||
className="group h-40 border border-dashed rounded-md flex flex-col justify-center items-center cursor-pointer gap-3 hover:border-primary"
|
||||
>
|
||||
<input {...getLocalFileInputProps()} />
|
||||
<UploadIcon className="group-hover:text-primary size-5" />
|
||||
<p className="text-sm">{t('code.clickOrDragHere')}</p>
|
||||
|
||||
</div>
|
||||
{importFiles.length > 0 && (
|
||||
<div className="text-sm text-start text-green-500 mt-2">
|
||||
{importFiles.slice(0, 1).map((file, index) => (
|
||||
<div key={index}>
|
||||
<span>
|
||||
{file.name}
|
||||
</span>
|
||||
{/* <button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setImportFiles([]);
|
||||
@@ -1030,74 +1032,98 @@ const handleValidationDialogConfirm = async () => {
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button> */}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setLocalFileDialogOpen(false);
|
||||
setImportFiles([]);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await handleLocalFileImport(); // 等待导入完成
|
||||
setImportFiles([])
|
||||
setLocalFileDialogOpen(false); // 关闭弹窗
|
||||
}}
|
||||
disabled={isImporting || importFiles.length === 0}
|
||||
>
|
||||
{isImporting ? "导入中..." : "提交"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={validationDialog.open} onOpenChange={(open) => setValidationDialog(prev => ({...prev, open}))}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<AlertTriangle className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<DialogTitle>文件导入结果</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setLocalFileDialogOpen(false);
|
||||
setImportFiles([]);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await handleLocalFileImport(); // 等待导入完成
|
||||
setImportFiles([])
|
||||
setLocalFileDialogOpen(false); // 关闭弹窗
|
||||
}}
|
||||
disabled={isImporting || importFiles.length === 0}
|
||||
>
|
||||
{isImporting ? "导入中..." : "提交"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={validationDialog.open} onOpenChange={(open) => setValidationDialog(prev => ({ ...prev, open }))}>
|
||||
<DialogContent className="sm:max-w-[500px] max-h-[80vh]" close={false}>
|
||||
{/* 静态蓝色竖条 - 始终显示 */}
|
||||
<div className="absolute left-0 top-0 h-full w-1.5 bg-blue-500"></div>
|
||||
|
||||
<div className="py-4">
|
||||
{validationDialog.status_message && (
|
||||
<>
|
||||
<p>
|
||||
{validationDialog.status_message.split(":")[0]}
|
||||
</p>
|
||||
{validationDialog.status_message.includes(":") && (
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
{validationDialog.status_message.split(":")[1].split("\n").map((line, index) => (
|
||||
<p key={index} className="mb-1">{line.trim()}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* 可滚动区域 - 仅在内容溢出时显示滚动条 */}
|
||||
<div
|
||||
className="pl-4 overflow-y-auto"
|
||||
style={{
|
||||
maxHeight: 'calc(80vh - 2rem)', // 减去padding等空间
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: '#3b82f6 transparent' // blue-500
|
||||
}}
|
||||
>
|
||||
{/* 自定义滚动条样式 (Webkit浏览器) */}
|
||||
<style jsx>{`
|
||||
.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background: #3b82f6; /* blue-500 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleValidationDialogConfirm}
|
||||
>
|
||||
知道了
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<span className="text-white font-bold text-lg">i</span>
|
||||
</div>
|
||||
<DialogTitle>文件格式不符合要求</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
{validationDialog.status_message && (
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">
|
||||
{validationDialog.status_message.split(":")[0]}
|
||||
</p>
|
||||
{validationDialog.status_message.includes(":") && (
|
||||
<div className="text-sm text-gray-600">
|
||||
{validationDialog.status_message.split(":")[1].split("\n").map((line, index) => (
|
||||
<p key={index}>{line.trim()}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleValidationDialogConfirm}>
|
||||
知道了
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user