mirror of https://github.com/chaitin/PandaWiki.git
Compare commits
22 Commits
349376f1fb
...
12b51f2b2b
| Author | SHA1 | Date |
|---|---|---|
|
|
12b51f2b2b | |
|
|
1ff013ac47 | |
|
|
c88bd58b6b | |
|
|
bddd515df2 | |
|
|
b1c564ff2a | |
|
|
e4dbfcb9fb | |
|
|
40c395400d | |
|
|
b7cdec0d4a | |
|
|
44126e0a11 | |
|
|
3375fcb643 | |
|
|
ef3bae6336 | |
|
|
d9d3bc4911 | |
|
|
546062470b | |
|
|
5a3b23ac75 | |
|
|
35cd94e342 | |
|
|
f7c0fe273b | |
|
|
0175624c84 | |
|
|
a6f4688b88 | |
|
|
575f51f0ea | |
|
|
83f6853716 | |
|
|
3dae8e8d01 | |
|
|
2e1e1848c4 |
|
|
@ -56,3 +56,11 @@ type NodePermissionEditReq struct {
|
||||||
|
|
||||||
type NodePermissionEditResp struct {
|
type NodePermissionEditResp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NodeRestudyReq struct {
|
||||||
|
NodeIds []string `json:"node_ids" validate:"required,min=1"`
|
||||||
|
KbId string `json:"kb_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeRestudyResp struct {
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,9 @@ func createApp() (*App, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
|
authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
|
||||||
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo)
|
systemSettingRepo := pg2.NewSystemSettingRepo(db, logger)
|
||||||
|
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository, systemSettingRepo)
|
||||||
|
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo, modelUsecase)
|
||||||
nodeHandler := v1.NewNodeHandler(baseHandler, echo, nodeUsecase, authMiddleware, logger)
|
nodeHandler := v1.NewNodeHandler(baseHandler, echo, nodeUsecase, authMiddleware, logger)
|
||||||
geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
|
geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
|
||||||
ipdbIPDB, err := ipdb.NewIPDB(configConfig, logger)
|
ipdbIPDB, err := ipdb.NewIPDB(configConfig, logger)
|
||||||
|
|
@ -105,7 +107,6 @@ func createApp() (*App, error) {
|
||||||
}
|
}
|
||||||
ipAddressRepo := ipdb2.NewIPAddressRepo(ipdbIPDB, logger)
|
ipAddressRepo := ipdb2.NewIPAddressRepo(ipdbIPDB, logger)
|
||||||
conversationUsecase := usecase.NewConversationUsecase(conversationRepository, nodeRepository, geoRepo, logger, ipAddressRepo, authRepo)
|
conversationUsecase := usecase.NewConversationUsecase(conversationRepository, nodeRepository, geoRepo, logger, ipAddressRepo, authRepo)
|
||||||
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository)
|
|
||||||
blockWordRepo := pg2.NewBlockWordRepo(db, logger)
|
blockWordRepo := pg2.NewBlockWordRepo(db, logger)
|
||||||
chatUsecase, err := usecase.NewChatUsecase(llmUsecase, knowledgeBaseRepository, conversationUsecase, modelUsecase, appRepository, blockWordRepo, authRepo, logger)
|
chatUsecase, err := usecase.NewChatUsecase(llmUsecase, knowledgeBaseRepository, conversationUsecase, modelUsecase, appRepository, blockWordRepo, authRepo, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/chaitin/panda-wiki/config"
|
"github.com/chaitin/panda-wiki/config"
|
||||||
mq2 "github.com/chaitin/panda-wiki/handler/mq"
|
mq3 "github.com/chaitin/panda-wiki/handler/mq"
|
||||||
"github.com/chaitin/panda-wiki/log"
|
"github.com/chaitin/panda-wiki/log"
|
||||||
"github.com/chaitin/panda-wiki/mq"
|
"github.com/chaitin/panda-wiki/mq"
|
||||||
cache2 "github.com/chaitin/panda-wiki/repo/cache"
|
cache2 "github.com/chaitin/panda-wiki/repo/cache"
|
||||||
ipdb2 "github.com/chaitin/panda-wiki/repo/ipdb"
|
ipdb2 "github.com/chaitin/panda-wiki/repo/ipdb"
|
||||||
mq3 "github.com/chaitin/panda-wiki/repo/mq"
|
mq2 "github.com/chaitin/panda-wiki/repo/mq"
|
||||||
pg2 "github.com/chaitin/panda-wiki/repo/pg"
|
pg2 "github.com/chaitin/panda-wiki/repo/pg"
|
||||||
"github.com/chaitin/panda-wiki/store/cache"
|
"github.com/chaitin/panda-wiki/store/cache"
|
||||||
"github.com/chaitin/panda-wiki/store/ipdb"
|
"github.com/chaitin/panda-wiki/store/ipdb"
|
||||||
|
|
@ -49,11 +49,18 @@ func createApp() (*App, error) {
|
||||||
modelRepository := pg2.NewModelRepository(db, logger)
|
modelRepository := pg2.NewModelRepository(db, logger)
|
||||||
promptRepo := pg2.NewPromptRepo(db, logger)
|
promptRepo := pg2.NewPromptRepo(db, logger)
|
||||||
llmUsecase := usecase.NewLLMUsecase(configConfig, ragService, conversationRepository, knowledgeBaseRepository, nodeRepository, modelRepository, promptRepo, logger)
|
llmUsecase := usecase.NewLLMUsecase(configConfig, ragService, conversationRepository, knowledgeBaseRepository, nodeRepository, modelRepository, promptRepo, logger)
|
||||||
ragmqHandler, err := mq2.NewRAGMQHandler(mqConsumer, logger, ragService, nodeRepository, knowledgeBaseRepository, llmUsecase, modelRepository)
|
mqProducer, err := mq.NewMQProducer(configConfig, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ragDocUpdateHandler, err := mq2.NewRagDocUpdateHandler(mqConsumer, logger, nodeRepository)
|
ragRepository := mq2.NewRAGRepository(mqProducer)
|
||||||
|
systemSettingRepo := pg2.NewSystemSettingRepo(db, logger)
|
||||||
|
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository, systemSettingRepo)
|
||||||
|
ragmqHandler, err := mq3.NewRAGMQHandler(mqConsumer, logger, ragService, nodeRepository, knowledgeBaseRepository, llmUsecase, modelUsecase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ragDocUpdateHandler, err := mq3.NewRagDocUpdateHandler(mqConsumer, logger, nodeRepository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -71,22 +78,17 @@ func createApp() (*App, error) {
|
||||||
geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
|
geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
|
||||||
authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
|
authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
|
||||||
statUseCase := usecase.NewStatUseCase(statRepository, nodeRepository, conversationRepository, appRepository, ipAddressRepo, geoRepo, authRepo, knowledgeBaseRepository, logger)
|
statUseCase := usecase.NewStatUseCase(statRepository, nodeRepository, conversationRepository, appRepository, ipAddressRepo, geoRepo, authRepo, knowledgeBaseRepository, logger)
|
||||||
mqProducer, err := mq.NewMQProducer(configConfig, logger)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ragRepository := mq3.NewRAGRepository(mqProducer)
|
|
||||||
userRepository := pg2.NewUserRepository(db, logger)
|
userRepository := pg2.NewUserRepository(db, logger)
|
||||||
minioClient, err := s3.NewMinioClient(configConfig)
|
minioClient, err := s3.NewMinioClient(configConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo)
|
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo, modelUsecase)
|
||||||
cronHandler, err := mq2.NewStatCronHandler(logger, statRepository, statUseCase, nodeUsecase)
|
cronHandler, err := mq3.NewStatCronHandler(logger, statRepository, statUseCase, nodeUsecase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mqHandlers := &mq2.MQHandlers{
|
mqHandlers := &mq3.MQHandlers{
|
||||||
RAGMQHandler: ragmqHandler,
|
RAGMQHandler: ragmqHandler,
|
||||||
RagDocUpdateHandler: ragDocUpdateHandler,
|
RagDocUpdateHandler: ragDocUpdateHandler,
|
||||||
StatCronHandler: cronHandler,
|
StatCronHandler: cronHandler,
|
||||||
|
|
@ -105,6 +107,6 @@ func createApp() (*App, error) {
|
||||||
type App struct {
|
type App struct {
|
||||||
MQConsumer mq.MQConsumer
|
MQConsumer mq.MQConsumer
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
MQHandlers *mq2.MQHandlers
|
MQHandlers *mq3.MQHandlers
|
||||||
StatCronHandler *mq2.CronHandler
|
StatCronHandler *mq3.CronHandler
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,9 @@ func createApp() (*App, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
|
authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
|
||||||
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo)
|
systemSettingRepo := pg2.NewSystemSettingRepo(db, logger)
|
||||||
|
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository, systemSettingRepo)
|
||||||
|
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo, modelUsecase)
|
||||||
kbRepo := cache2.NewKBRepo(cacheCache)
|
kbRepo := cache2.NewKBRepo(cacheCache)
|
||||||
knowledgeBaseUsecase, err := usecase.NewKnowledgeBaseUsecase(knowledgeBaseRepository, nodeRepository, ragRepository, userRepository, ragService, kbRepo, logger, configConfig)
|
knowledgeBaseUsecase, err := usecase.NewKnowledgeBaseUsecase(knowledgeBaseRepository, nodeRepository, ragRepository, userRepository, ragService, kbRepo, logger, configConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package consts
|
||||||
|
|
||||||
|
type AutoModeDefaultModel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AutoModeDefaultChatModel AutoModeDefaultModel = "deepseek-chat"
|
||||||
|
AutoModeDefaultEmbeddingModel AutoModeDefaultModel = "bge-m3"
|
||||||
|
AutoModeDefaultRerankModel AutoModeDefaultModel = "bge-reranker-v2-m3"
|
||||||
|
AutoModeDefaultAnalysisModel AutoModeDefaultModel = "qwen2.5-3b-instruct"
|
||||||
|
AutoModeDefaultAnalysisVLModel AutoModeDefaultModel = "qwen3-vl-max"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAutoModeDefaultModel(modelType string) string {
|
||||||
|
switch modelType {
|
||||||
|
case "chat":
|
||||||
|
return string(AutoModeDefaultChatModel)
|
||||||
|
case "embedding":
|
||||||
|
return string(AutoModeDefaultEmbeddingModel)
|
||||||
|
case "rerank":
|
||||||
|
return string(AutoModeDefaultRerankModel)
|
||||||
|
case "analysis":
|
||||||
|
return string(AutoModeDefaultAnalysisModel)
|
||||||
|
case "analysis-vl":
|
||||||
|
return string(AutoModeDefaultAnalysisVLModel)
|
||||||
|
default:
|
||||||
|
return string(AutoModeDefaultChatModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelSettingMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModelSettingModeManual ModelSettingMode = "manual"
|
||||||
|
ModelSettingModeAuto ModelSettingMode = "auto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AutoModeBaseURL = "https://model-square.app.baizhi.cloud/v1"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package consts
|
||||||
|
|
||||||
|
type SystemSettingKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SystemSettingModelMode SystemSettingKey = "model_setting_mode"
|
||||||
|
)
|
||||||
|
|
@ -1565,6 +1565,41 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/model/mode-setting": {
|
||||||
|
"get": {
|
||||||
|
"description": "get current model mode setting including mode, API key and chat model",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"model"
|
||||||
|
],
|
||||||
|
"summary": "get model mode setting",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/domain.ModelModeSetting"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/model/provider/supported": {
|
"/api/v1/model/provider/supported": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "get provider supported model list",
|
"description": "get provider supported model list",
|
||||||
|
|
@ -1611,6 +1646,52 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/model/switch-mode": {
|
||||||
|
"post": {
|
||||||
|
"description": "switch model mode between manual and auto",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"model"
|
||||||
|
],
|
||||||
|
"summary": "switch mode",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "switch mode request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.SwitchModeReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/domain.SwitchModeResp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/node": {
|
"/api/v1/node": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -2117,6 +2198,58 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/node/restudy": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "文档重新学习",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Node"
|
||||||
|
],
|
||||||
|
"summary": "文档重新学习",
|
||||||
|
"operationId": "v1-NodeRestudy",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "para",
|
||||||
|
"name": "param",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/v1.NodeRestudyReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/v1.NodeRestudyResp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/node/summary": {
|
"/api/v1/node/summary": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -3952,6 +4085,17 @@ const docTemplate = `{
|
||||||
"LicenseEditionEnterprise"
|
"LicenseEditionEnterprise"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"consts.ModelSettingMode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"manual",
|
||||||
|
"auto"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"ModelSettingModeManual",
|
||||||
|
"ModelSettingModeAuto"
|
||||||
|
]
|
||||||
|
},
|
||||||
"consts.NodeAccessPerm": {
|
"consts.NodeAccessPerm": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -6206,6 +6350,31 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.ModelModeSetting": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"auto_mode_api_key": {
|
||||||
|
"description": "百智云 API Key",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"chat_model": {
|
||||||
|
"description": "自定义对话模型名称",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_manual_embedding_updated": {
|
||||||
|
"description": "手动模式下嵌入模型是否更新",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"description": "模式: manual 或 auto",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.ModelSettingMode"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.ModelType": {
|
"domain.ModelType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -7045,6 +7214,37 @@ const docTemplate = `{
|
||||||
"StatPageSceneLogin"
|
"StatPageSceneLogin"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"domain.SwitchModeReq": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"mode"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"auto_mode_api_key": {
|
||||||
|
"description": "百智云 API Key",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"chat_model": {
|
||||||
|
"description": "自定义对话模型名称",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"manual",
|
||||||
|
"auto"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.SwitchModeResp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.TextConfig": {
|
"domain.TextConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -8271,6 +8471,28 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"v1.NodeRestudyReq": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"kb_id",
|
||||||
|
"node_ids"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"kb_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"node_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"v1.NodeRestudyResp": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"v1.ResetPasswordReq": {
|
"v1.ResetPasswordReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -1558,6 +1558,41 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/model/mode-setting": {
|
||||||
|
"get": {
|
||||||
|
"description": "get current model mode setting including mode, API key and chat model",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"model"
|
||||||
|
],
|
||||||
|
"summary": "get model mode setting",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/domain.ModelModeSetting"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/model/provider/supported": {
|
"/api/v1/model/provider/supported": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "get provider supported model list",
|
"description": "get provider supported model list",
|
||||||
|
|
@ -1604,6 +1639,52 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/model/switch-mode": {
|
||||||
|
"post": {
|
||||||
|
"description": "switch model mode between manual and auto",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"model"
|
||||||
|
],
|
||||||
|
"summary": "switch mode",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "switch mode request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.SwitchModeReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/domain.SwitchModeResp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/node": {
|
"/api/v1/node": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -2110,6 +2191,58 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/node/restudy": {
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "文档重新学习",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Node"
|
||||||
|
],
|
||||||
|
"summary": "文档重新学习",
|
||||||
|
"operationId": "v1-NodeRestudy",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "para",
|
||||||
|
"name": "param",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/v1.NodeRestudyReq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/v1.NodeRestudyResp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/node/summary": {
|
"/api/v1/node/summary": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -3945,6 +4078,17 @@
|
||||||
"LicenseEditionEnterprise"
|
"LicenseEditionEnterprise"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"consts.ModelSettingMode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"manual",
|
||||||
|
"auto"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"ModelSettingModeManual",
|
||||||
|
"ModelSettingModeAuto"
|
||||||
|
]
|
||||||
|
},
|
||||||
"consts.NodeAccessPerm": {
|
"consts.NodeAccessPerm": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -6199,6 +6343,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.ModelModeSetting": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"auto_mode_api_key": {
|
||||||
|
"description": "百智云 API Key",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"chat_model": {
|
||||||
|
"description": "自定义对话模型名称",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_manual_embedding_updated": {
|
||||||
|
"description": "手动模式下嵌入模型是否更新",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"description": "模式: manual 或 auto",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.ModelSettingMode"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.ModelType": {
|
"domain.ModelType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -7038,6 +7207,37 @@
|
||||||
"StatPageSceneLogin"
|
"StatPageSceneLogin"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"domain.SwitchModeReq": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"mode"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"auto_mode_api_key": {
|
||||||
|
"description": "百智云 API Key",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"chat_model": {
|
||||||
|
"description": "自定义对话模型名称",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"manual",
|
||||||
|
"auto"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domain.SwitchModeResp": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.TextConfig": {
|
"domain.TextConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -8264,6 +8464,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"v1.NodeRestudyReq": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"kb_id",
|
||||||
|
"node_ids"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"kb_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"node_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"v1.NodeRestudyResp": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"v1.ResetPasswordReq": {
|
"v1.ResetPasswordReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,14 @@ definitions:
|
||||||
- LicenseEditionFree
|
- LicenseEditionFree
|
||||||
- LicenseEditionContributor
|
- LicenseEditionContributor
|
||||||
- LicenseEditionEnterprise
|
- LicenseEditionEnterprise
|
||||||
|
consts.ModelSettingMode:
|
||||||
|
enum:
|
||||||
|
- manual
|
||||||
|
- auto
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- ModelSettingModeManual
|
||||||
|
- ModelSettingModeAuto
|
||||||
consts.NodeAccessPerm:
|
consts.NodeAccessPerm:
|
||||||
enum:
|
enum:
|
||||||
- open
|
- open
|
||||||
|
|
@ -1628,6 +1636,22 @@ definitions:
|
||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
domain.ModelModeSetting:
|
||||||
|
properties:
|
||||||
|
auto_mode_api_key:
|
||||||
|
description: 百智云 API Key
|
||||||
|
type: string
|
||||||
|
chat_model:
|
||||||
|
description: 自定义对话模型名称
|
||||||
|
type: string
|
||||||
|
is_manual_embedding_updated:
|
||||||
|
description: 手动模式下嵌入模型是否更新
|
||||||
|
type: boolean
|
||||||
|
mode:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/consts.ModelSettingMode'
|
||||||
|
description: '模式: manual 或 auto'
|
||||||
|
type: object
|
||||||
domain.ModelType:
|
domain.ModelType:
|
||||||
enum:
|
enum:
|
||||||
- chat
|
- chat
|
||||||
|
|
@ -2178,6 +2202,27 @@ definitions:
|
||||||
- StatPageSceneNodeDetail
|
- StatPageSceneNodeDetail
|
||||||
- StatPageSceneChat
|
- StatPageSceneChat
|
||||||
- StatPageSceneLogin
|
- StatPageSceneLogin
|
||||||
|
domain.SwitchModeReq:
|
||||||
|
properties:
|
||||||
|
auto_mode_api_key:
|
||||||
|
description: 百智云 API Key
|
||||||
|
type: string
|
||||||
|
chat_model:
|
||||||
|
description: 自定义对话模型名称
|
||||||
|
type: string
|
||||||
|
mode:
|
||||||
|
enum:
|
||||||
|
- manual
|
||||||
|
- auto
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- mode
|
||||||
|
type: object
|
||||||
|
domain.SwitchModeResp:
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.TextConfig:
|
domain.TextConfig:
|
||||||
properties:
|
properties:
|
||||||
title:
|
title:
|
||||||
|
|
@ -2986,6 +3031,21 @@ definitions:
|
||||||
$ref: '#/definitions/domain.NodeGroupDetail'
|
$ref: '#/definitions/domain.NodeGroupDetail'
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
|
v1.NodeRestudyReq:
|
||||||
|
properties:
|
||||||
|
kb_id:
|
||||||
|
type: string
|
||||||
|
node_ids:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- kb_id
|
||||||
|
- node_ids
|
||||||
|
type: object
|
||||||
|
v1.NodeRestudyResp:
|
||||||
|
type: object
|
||||||
v1.ResetPasswordReq:
|
v1.ResetPasswordReq:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
|
|
@ -4050,6 +4110,27 @@ paths:
|
||||||
summary: get model list
|
summary: get model list
|
||||||
tags:
|
tags:
|
||||||
- model
|
- model
|
||||||
|
/api/v1/model/mode-setting:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: get current model mode setting including mode, API key and chat
|
||||||
|
model
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/domain.Response'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/domain.ModelModeSetting'
|
||||||
|
type: object
|
||||||
|
summary: get model mode setting
|
||||||
|
tags:
|
||||||
|
- model
|
||||||
/api/v1/model/provider/supported:
|
/api/v1/model/provider/supported:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -4077,6 +4158,33 @@ paths:
|
||||||
summary: get provider supported model list
|
summary: get provider supported model list
|
||||||
tags:
|
tags:
|
||||||
- model
|
- model
|
||||||
|
/api/v1/model/switch-mode:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: switch model mode between manual and auto
|
||||||
|
parameters:
|
||||||
|
- description: switch mode request
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.SwitchModeReq'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/domain.Response'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/domain.SwitchModeResp'
|
||||||
|
type: object
|
||||||
|
summary: switch mode
|
||||||
|
tags:
|
||||||
|
- model
|
||||||
/api/v1/node:
|
/api/v1/node:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -4374,6 +4482,36 @@ paths:
|
||||||
summary: Recommend Nodes
|
summary: Recommend Nodes
|
||||||
tags:
|
tags:
|
||||||
- node
|
- node
|
||||||
|
/api/v1/node/restudy:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: 文档重新学习
|
||||||
|
operationId: v1-NodeRestudy
|
||||||
|
parameters:
|
||||||
|
- description: para
|
||||||
|
in: body
|
||||||
|
name: param
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/v1.NodeRestudyReq'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/domain.Response'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/v1.NodeRestudyResp'
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- bearerAuth: []
|
||||||
|
summary: 文档重新学习
|
||||||
|
tags:
|
||||||
|
- Node
|
||||||
/api/v1/node/summary:
|
/api/v1/node/summary:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -165,3 +165,13 @@ type ProviderModelListItem struct {
|
||||||
type ActivateModelReq struct {
|
type ActivateModelReq struct {
|
||||||
ModelID string `json:"model_id" validate:"required"`
|
ModelID string `json:"model_id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SwitchModeReq struct {
|
||||||
|
Mode string `json:"mode" validate:"required,oneof=manual auto"`
|
||||||
|
AutoModeAPIKey string `json:"auto_mode_api_key"` // 百智云 API Key
|
||||||
|
ChatModel string `json:"chat_model"` // 自定义对话模型名称
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwitchModeResp struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chaitin/panda-wiki/consts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// table: settings
|
||||||
|
type SystemSetting struct {
|
||||||
|
ID int `json:"id" gorm:"primary_key"`
|
||||||
|
Key consts.SystemSettingKey `json:"key"`
|
||||||
|
Value []byte `json:"value" gorm:"type:jsonb"` // JSON string
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SystemSetting) TableName() string {
|
||||||
|
return "system_settings"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelModeSetting 模型配置结构体
|
||||||
|
type ModelModeSetting struct {
|
||||||
|
Mode consts.ModelSettingMode `json:"mode"` // 模式: manual 或 auto
|
||||||
|
AutoModeAPIKey string `json:"auto_mode_api_key"` // 百智云 API Key
|
||||||
|
ChatModel string `json:"chat_model"` // 自定义对话模型名称
|
||||||
|
IsManualEmbeddingUpdated bool `json:"is_manual_embedding_updated"` // 手动模式下嵌入模型是否更新
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ var ProviderSet = wire.NewSet(
|
||||||
usecase.NewLLMUsecase,
|
usecase.NewLLMUsecase,
|
||||||
usecase.NewStatUseCase,
|
usecase.NewStatUseCase,
|
||||||
usecase.NewNodeUsecase,
|
usecase.NewNodeUsecase,
|
||||||
|
usecase.NewModelUsecase,
|
||||||
|
|
||||||
NewRAGMQHandler,
|
NewRAGMQHandler,
|
||||||
NewRagDocUpdateHandler,
|
NewRagDocUpdateHandler,
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ type RAGMQHandler struct {
|
||||||
rag rag.RAGService
|
rag rag.RAGService
|
||||||
nodeRepo *pg.NodeRepository
|
nodeRepo *pg.NodeRepository
|
||||||
kbRepo *pg.KnowledgeBaseRepository
|
kbRepo *pg.KnowledgeBaseRepository
|
||||||
modelRepo *pg.ModelRepository
|
|
||||||
llmUsecase *usecase.LLMUsecase
|
llmUsecase *usecase.LLMUsecase
|
||||||
|
modelUsecase *usecase.ModelUsecase
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRAGMQHandler(consumer mq.MQConsumer, logger *log.Logger, rag rag.RAGService, nodeRepo *pg.NodeRepository, kbRepo *pg.KnowledgeBaseRepository, llmUsecase *usecase.LLMUsecase, modelRepo *pg.ModelRepository) (*RAGMQHandler, error) {
|
func NewRAGMQHandler(consumer mq.MQConsumer, logger *log.Logger, rag rag.RAGService, nodeRepo *pg.NodeRepository, kbRepo *pg.KnowledgeBaseRepository, llmUsecase *usecase.LLMUsecase, modelUsecase *usecase.ModelUsecase) (*RAGMQHandler, error) {
|
||||||
h := &RAGMQHandler{
|
h := &RAGMQHandler{
|
||||||
consumer: consumer,
|
consumer: consumer,
|
||||||
logger: logger.WithModule("mq.rag"),
|
logger: logger.WithModule("mq.rag"),
|
||||||
|
|
@ -32,7 +32,7 @@ func NewRAGMQHandler(consumer mq.MQConsumer, logger *log.Logger, rag rag.RAGServ
|
||||||
nodeRepo: nodeRepo,
|
nodeRepo: nodeRepo,
|
||||||
kbRepo: kbRepo,
|
kbRepo: kbRepo,
|
||||||
llmUsecase: llmUsecase,
|
llmUsecase: llmUsecase,
|
||||||
modelRepo: modelRepo,
|
modelUsecase: modelUsecase,
|
||||||
}
|
}
|
||||||
if err := consumer.RegisterHandler(domain.VectorTaskTopic, h.HandleNodeContentVectorRequest); err != nil {
|
if err := consumer.RegisterHandler(domain.VectorTaskTopic, h.HandleNodeContentVectorRequest); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -134,11 +134,13 @@ func (h *RAGMQHandler) HandleNodeContentVectorRequest(ctx context.Context, msg t
|
||||||
h.logger.Info("node is folder, skip summary", log.Any("node_id", request.NodeID))
|
h.logger.Info("node is folder, skip summary", log.Any("node_id", request.NodeID))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
model, err := h.modelRepo.GetChatModel(ctx)
|
|
||||||
|
model, err := h.modelUsecase.GetChatModel(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("get chat model failed", log.Error(err))
|
h.logger.Error("get chat model failed", log.Error(err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := h.llmUsecase.SummaryNode(ctx, model, node.Name, node.Content)
|
summary, err := h.llmUsecase.SummaryNode(ctx, model, node.Name, node.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("summary node content failed", log.Error(err))
|
h.logger.Error("summary node content failed", log.Error(err))
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
|
||||||
group.POST("/check", handler.CheckModel)
|
group.POST("/check", handler.CheckModel)
|
||||||
group.POST("/provider/supported", handler.GetProviderSupportedModelList)
|
group.POST("/provider/supported", handler.GetProviderSupportedModelList)
|
||||||
group.PUT("", handler.UpdateModel)
|
group.PUT("", handler.UpdateModel)
|
||||||
|
group.POST("/switch-mode", handler.SwitchMode)
|
||||||
|
group.GET("/mode-setting", handler.GetModelModeSetting)
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
@ -211,3 +213,58 @@ func (h *ModelHandler) GetProviderSupportedModelList(c echo.Context) error {
|
||||||
}
|
}
|
||||||
return h.NewResponseWithData(c, models)
|
return h.NewResponseWithData(c, models)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SwitchMode
|
||||||
|
//
|
||||||
|
// @Summary switch mode
|
||||||
|
// @Description switch model mode between manual and auto
|
||||||
|
// @Tags model
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body domain.SwitchModeReq true "switch mode request"
|
||||||
|
// @Success 200 {object} domain.Response{data=domain.SwitchModeResp}
|
||||||
|
// @Router /api/v1/model/switch-mode [post]
|
||||||
|
func (h *ModelHandler) SwitchMode(c echo.Context) error {
|
||||||
|
var req domain.SwitchModeReq
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return h.NewResponseWithError(c, "bind request failed", err)
|
||||||
|
}
|
||||||
|
if err := c.Validate(&req); err != nil {
|
||||||
|
return h.NewResponseWithError(c, "validate request failed", err)
|
||||||
|
}
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
|
if err := h.usecase.SwitchMode(ctx, &req); err != nil {
|
||||||
|
return h.NewResponseWithError(c, err.Error(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &domain.SwitchModeResp{
|
||||||
|
Message: "模式切换成功",
|
||||||
|
}
|
||||||
|
return h.NewResponseWithData(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModelModeSetting
|
||||||
|
//
|
||||||
|
// @Summary get model mode setting
|
||||||
|
// @Description get current model mode setting including mode, API key and chat model
|
||||||
|
// @Tags model
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} domain.Response{data=domain.ModelModeSetting}
|
||||||
|
// @Router /api/v1/model/mode-setting [get]
|
||||||
|
func (h *ModelHandler) GetModelModeSetting(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
setting, err := h.usecase.GetModelModeSetting(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// 如果获取失败,返回默认值(手动模式)
|
||||||
|
h.logger.Warn("failed to get model mode setting, return default", log.Error(err))
|
||||||
|
defaultSetting := domain.ModelModeSetting{
|
||||||
|
Mode: consts.ModelSettingModeManual,
|
||||||
|
AutoModeAPIKey: "",
|
||||||
|
ChatModel: "",
|
||||||
|
}
|
||||||
|
return h.NewResponseWithData(c, defaultSetting)
|
||||||
|
}
|
||||||
|
return h.NewResponseWithData(c, setting)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ func NewNodeHandler(
|
||||||
group.POST("/batch_move", h.BatchMoveNode)
|
group.POST("/batch_move", h.BatchMoveNode)
|
||||||
|
|
||||||
group.GET("/recommend_nodes", h.RecommendNodes)
|
group.GET("/recommend_nodes", h.RecommendNodes)
|
||||||
|
group.POST("/restudy", h.NodeRestudy)
|
||||||
|
|
||||||
// node permission
|
// node permission
|
||||||
group.GET("/permission", h.NodePermission)
|
group.GET("/permission", h.NodePermission)
|
||||||
|
|
@ -384,3 +385,32 @@ func (h *NodeHandler) NodePermissionEdit(c echo.Context) error {
|
||||||
}
|
}
|
||||||
return h.NewResponseWithData(c, nil)
|
return h.NewResponseWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NodeRestudy 文档重新学习
|
||||||
|
//
|
||||||
|
// @Tags Node
|
||||||
|
// @Summary 文档重新学习
|
||||||
|
// @Description 文档重新学习
|
||||||
|
// @ID v1-NodeRestudy
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security bearerAuth
|
||||||
|
// @Param param body v1.NodeRestudyReq true "para"
|
||||||
|
// @Success 200 {object} domain.Response{data=v1.NodeRestudyResp}
|
||||||
|
// @Router /api/v1/node/restudy [post]
|
||||||
|
func (h *NodeHandler) NodeRestudy(c echo.Context) error {
|
||||||
|
var req v1.NodeRestudyReq
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Validate(req); err != nil {
|
||||||
|
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.usecase.NodeRestudy(c.Request().Context(), &req); err != nil {
|
||||||
|
return h.NewResponseWithError(c, "node restudy failed", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.NewResponseWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,5 @@ var ProviderSet = wire.NewSet(
|
||||||
NewAuthRepo,
|
NewAuthRepo,
|
||||||
NewWechatRepository,
|
NewWechatRepository,
|
||||||
NewAPITokenRepo,
|
NewAPITokenRepo,
|
||||||
|
NewSystemSettingRepo,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package pg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/chaitin/panda-wiki/domain"
|
||||||
|
"github.com/chaitin/panda-wiki/log"
|
||||||
|
"github.com/chaitin/panda-wiki/store/pg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SystemSettingRepo struct {
|
||||||
|
db *pg.DB
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSystemSettingRepo(db *pg.DB, logger *log.Logger) *SystemSettingRepo {
|
||||||
|
return &SystemSettingRepo{
|
||||||
|
db: db,
|
||||||
|
logger: logger.WithModule("repo.pg.system_setting"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SystemSettingRepo) GetSystemSetting(ctx context.Context, key string) (*domain.SystemSetting, error) {
|
||||||
|
var setting domain.SystemSetting
|
||||||
|
result := r.db.WithContext(ctx).Where("key = ?", key).First(&setting)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return &setting, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SystemSettingRepo) UpdateSystemSetting(ctx context.Context, key, value string) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&domain.SystemSetting{}).Where("key = ?", key).Update("value", value).Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
-- Drop settings table
|
||||||
|
DROP TABLE IF EXISTS system_settings;
|
||||||
|
-- drop index
|
||||||
|
DROP INDEX IF EXISTS idx_system_settings_key;
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- Create settings table
|
||||||
|
CREATE TABLE IF NOT EXISTS system_settings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
key TEXT NOT NULL,
|
||||||
|
value JSONB NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_uniq_system_settings_key ON system_settings(key);
|
||||||
|
|
||||||
|
-- Insert model_setting_mode setting
|
||||||
|
-- If there are existing knowledge bases, set mode to 'manual', otherwise set to 'auto'
|
||||||
|
INSERT INTO system_settings (key, value, description)
|
||||||
|
SELECT
|
||||||
|
'model_setting_mode',
|
||||||
|
jsonb_build_object(
|
||||||
|
'mode', CASE
|
||||||
|
WHEN EXISTS (SELECT 1 FROM knowledge_bases LIMIT 1) THEN 'manual'
|
||||||
|
ELSE 'auto'
|
||||||
|
END,
|
||||||
|
'auto_mode_api_key', '',
|
||||||
|
'chat_model', '',
|
||||||
|
'is_manual_embedding_updated', false
|
||||||
|
),
|
||||||
|
'Model setting mode configuration'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM system_settings WHERE key = 'model_setting_mode'
|
||||||
|
);
|
||||||
|
|
@ -84,7 +84,7 @@ func (u *CreationUsecase) TextCreation(ctx context.Context, req *domain.TextReq,
|
||||||
func (u *CreationUsecase) TabComplete(ctx context.Context, req *domain.CompleteReq) (string, error) {
|
func (u *CreationUsecase) TabComplete(ctx context.Context, req *domain.CompleteReq) (string, error) {
|
||||||
// For FIM (Fill in Middle) style completion, we need to handle prefix and suffix
|
// For FIM (Fill in Middle) style completion, we need to handle prefix and suffix
|
||||||
if req.Prefix != "" || req.Suffix != "" {
|
if req.Prefix != "" || req.Suffix != "" {
|
||||||
model, err := u.model.GetModelByType(ctx, domain.ModelTypeChat)
|
model, err := u.model.GetChatModel(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.logger.Error("get chat model failed", log.Error(err))
|
u.logger.Error("get chat model failed", log.Error(err))
|
||||||
return "", domain.ErrModelNotConfigured
|
return "", domain.ErrModelNotConfigured
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -16,8 +15,6 @@ import (
|
||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
"github.com/pkoukk/tiktoken-go"
|
"github.com/pkoukk/tiktoken-go"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
"github.com/samber/lo/parallel"
|
|
||||||
"golang.org/x/sync/semaphore"
|
|
||||||
|
|
||||||
"github.com/chaitin/panda-wiki/config"
|
"github.com/chaitin/panda-wiki/config"
|
||||||
"github.com/chaitin/panda-wiki/domain"
|
"github.com/chaitin/panda-wiki/domain"
|
||||||
|
|
@ -39,6 +36,11 @@ type LLMUsecase struct {
|
||||||
modelkit *modelkit.ModelKit
|
modelkit *modelkit.ModelKit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
summaryChunkTokenLimit = 30720 // 30KB tokens per chunk
|
||||||
|
summaryMaxChunks = 4 // max chunks to process for summary
|
||||||
|
)
|
||||||
|
|
||||||
func NewLLMUsecase(config *config.Config, rag rag.RAGService, conversationRepo *pg.ConversationRepository, kbRepo *pg.KnowledgeBaseRepository, nodeRepo *pg.NodeRepository, modelRepo *pg.ModelRepository, promptRepo *pg.PromptRepo, logger *log.Logger) *LLMUsecase {
|
func NewLLMUsecase(config *config.Config, rag rag.RAGService, conversationRepo *pg.ConversationRepository, kbRepo *pg.KnowledgeBaseRepository, nodeRepo *pg.NodeRepository, modelRepo *pg.ModelRepository, promptRepo *pg.PromptRepo, logger *log.Logger) *LLMUsecase {
|
||||||
tiktoken.SetBpeLoader(&utils.Localloader{})
|
tiktoken.SetBpeLoader(&utils.Localloader{})
|
||||||
modelkit := modelkit.NewModelKit(logger.Logger)
|
modelkit := modelkit.NewModelKit(logger.Logger)
|
||||||
|
|
@ -197,52 +199,59 @@ func (u *LLMUsecase) SummaryNode(ctx context.Context, model *domain.Model, name,
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks, err := u.SplitByTokenLimit(content, int(math.Floor(1024*32*0.95)))
|
chunks, err := u.SplitByTokenLimit(content, summaryChunkTokenLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
sem := semaphore.NewWeighted(int64(10))
|
if len(chunks) > summaryMaxChunks {
|
||||||
summaries := parallel.Map(chunks, func(chunk string, _ int) string {
|
u.logger.Debug("trim summary chunks for large document", log.String("node", name), log.Int("original_chunks", len(chunks)), log.Int("used_chunks", summaryMaxChunks))
|
||||||
if err := sem.Acquire(ctx, 1); err != nil {
|
chunks = chunks[:summaryMaxChunks]
|
||||||
u.logger.Error("Failed to acquire semaphore for chunk: ", log.Error(err))
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
defer sem.Release(1)
|
|
||||||
summary, err := u.Generate(ctx, chatModel, []*schema.Message{
|
|
||||||
{
|
|
||||||
Role: "system",
|
|
||||||
Content: "你是文档总结助手,请根据文档内容总结出文档的摘要。摘要是纯文本,应该简洁明了,不要超过160个字。",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Role: "user",
|
|
||||||
Content: fmt.Sprintf("文档名称:%s\n文档内容:%s", name, chunk),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
u.logger.Error("Failed to generate summary for chunk: ", log.Error(err))
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(summary, "<think>") {
|
|
||||||
// remove <think> body </think>
|
|
||||||
endIndex := strings.Index(summary, "</think>")
|
|
||||||
if endIndex != -1 {
|
|
||||||
summary = strings.TrimSpace(summary[endIndex+8:]) // 8 is length of "</think>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return summary
|
|
||||||
})
|
|
||||||
// 使用lo.Filter处理错误
|
|
||||||
defeatSummary := lo.Filter(summaries, func(summary string, index int) bool {
|
|
||||||
return summary == ""
|
|
||||||
})
|
|
||||||
if len(defeatSummary) > 0 {
|
|
||||||
return "", fmt.Errorf("failed to generate summaries for all chunks: %d/%d", len(defeatSummary), len(chunks))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contents, err := u.SplitByTokenLimit(strings.Join(summaries, "\n\n"), int(math.Floor(1024*32*0.95)))
|
summaries := make([]string, 0, len(chunks))
|
||||||
|
for idx, chunk := range chunks {
|
||||||
|
summary, err := u.requestSummary(ctx, chatModel, name, chunk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
u.logger.Error("Failed to generate summary for chunk", log.Int("chunk_index", idx), log.Error(err))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
if summary == "" {
|
||||||
|
u.logger.Warn("Empty summary returned for chunk", log.Int("chunk_index", idx))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
summaries = append(summaries, summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(summaries) == 0 {
|
||||||
|
return "", fmt.Errorf("failed to generate summary for document %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join all summaries and generate final summary
|
||||||
|
joined := strings.Join(summaries, "\n\n")
|
||||||
|
finalSummary, err := u.requestSummary(ctx, chatModel, name, joined)
|
||||||
|
if err != nil {
|
||||||
|
u.logger.Error("Failed to generate final summary, using aggregated summaries", log.Error(err))
|
||||||
|
// Fallback: return the joined summaries directly
|
||||||
|
if len(joined) > 500 {
|
||||||
|
return joined[:500] + "...", nil
|
||||||
|
}
|
||||||
|
return joined, nil
|
||||||
|
}
|
||||||
|
return finalSummary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *LLMUsecase) trimThinking(summary string) string {
|
||||||
|
if !strings.HasPrefix(summary, "<think>") {
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
endIndex := strings.Index(summary, "</think>")
|
||||||
|
if endIndex == -1 {
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(summary[endIndex+len("</think>"):])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *LLMUsecase) requestSummary(ctx context.Context, chatModel model.BaseChatModel, name, content string) (string, error) {
|
||||||
summary, err := u.Generate(ctx, chatModel, []*schema.Message{
|
summary, err := u.Generate(ctx, chatModel, []*schema.Message{
|
||||||
{
|
{
|
||||||
Role: "system",
|
Role: "system",
|
||||||
|
|
@ -250,20 +259,13 @@ func (u *LLMUsecase) SummaryNode(ctx context.Context, model *domain.Model, name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: fmt.Sprintf("文档名称:%s\n文档内容:%s", name, contents[0]),
|
Content: fmt.Sprintf("文档名称:%s\n文档内容:%s", name, content),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(summary, "<think>") {
|
return strings.TrimSpace(u.trimThinking(summary)), nil
|
||||||
// remove <think> body </think>
|
|
||||||
endIndex := strings.Index(summary, "</think>")
|
|
||||||
if endIndex != -1 {
|
|
||||||
summary = strings.TrimSpace(summary[endIndex+8:]) // 8 is length of "</think>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return summary, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *LLMUsecase) SplitByTokenLimit(text string, maxTokens int) ([]string, error) {
|
func (u *LLMUsecase) SplitByTokenLimit(text string, maxTokens int) ([]string, error) {
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@ package usecase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cloudwego/eino/schema"
|
"github.com/cloudwego/eino/schema"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/samber/lo"
|
|
||||||
|
|
||||||
|
modelkitDomain "github.com/chaitin/ModelKit/v2/domain"
|
||||||
|
modelkit "github.com/chaitin/ModelKit/v2/usecase"
|
||||||
"github.com/chaitin/panda-wiki/config"
|
"github.com/chaitin/panda-wiki/config"
|
||||||
|
"github.com/chaitin/panda-wiki/consts"
|
||||||
"github.com/chaitin/panda-wiki/domain"
|
"github.com/chaitin/panda-wiki/domain"
|
||||||
"github.com/chaitin/panda-wiki/log"
|
"github.com/chaitin/panda-wiki/log"
|
||||||
"github.com/chaitin/panda-wiki/repo/mq"
|
"github.com/chaitin/panda-wiki/repo/mq"
|
||||||
|
|
@ -25,9 +26,12 @@ type ModelUsecase struct {
|
||||||
ragRepo *mq.RAGRepository
|
ragRepo *mq.RAGRepository
|
||||||
ragStore rag.RAGService
|
ragStore rag.RAGService
|
||||||
kbRepo *pg.KnowledgeBaseRepository
|
kbRepo *pg.KnowledgeBaseRepository
|
||||||
|
systemSettingRepo *pg.SystemSettingRepo
|
||||||
|
modelkit *modelkit.ModelKit
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModelUsecase(modelRepo *pg.ModelRepository, nodeRepo *pg.NodeRepository, ragRepo *mq.RAGRepository, ragStore rag.RAGService, logger *log.Logger, config *config.Config, kbRepo *pg.KnowledgeBaseRepository) *ModelUsecase {
|
func NewModelUsecase(modelRepo *pg.ModelRepository, nodeRepo *pg.NodeRepository, ragRepo *mq.RAGRepository, ragStore rag.RAGService, logger *log.Logger, config *config.Config, kbRepo *pg.KnowledgeBaseRepository, settingRepo *pg.SystemSettingRepo) *ModelUsecase {
|
||||||
|
modelkit := modelkit.NewModelKit(logger.Logger)
|
||||||
u := &ModelUsecase{
|
u := &ModelUsecase{
|
||||||
modelRepo: modelRepo,
|
modelRepo: modelRepo,
|
||||||
logger: logger.WithModule("usecase.model"),
|
logger: logger.WithModule("usecase.model"),
|
||||||
|
|
@ -36,100 +40,26 @@ func NewModelUsecase(modelRepo *pg.ModelRepository, nodeRepo *pg.NodeRepository,
|
||||||
ragRepo: ragRepo,
|
ragRepo: ragRepo,
|
||||||
ragStore: ragStore,
|
ragStore: ragStore,
|
||||||
kbRepo: kbRepo,
|
kbRepo: kbRepo,
|
||||||
}
|
systemSettingRepo: settingRepo,
|
||||||
if err := u.initEmbeddingAndRerankModel(context.Background()); err != nil {
|
modelkit: modelkit,
|
||||||
logger.Error("init embedding & rerank & analysis model failed", log.Any("error", err))
|
|
||||||
}
|
}
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ModelUsecase) initEmbeddingAndRerankModel(ctx context.Context) error {
|
|
||||||
isReady := false
|
|
||||||
// wait for raglite to be ready
|
|
||||||
for range 60 {
|
|
||||||
models, err := u.ragStore.GetModelList(ctx)
|
|
||||||
if err != nil {
|
|
||||||
u.logger.Error("wait for raglite to be ready", log.Any("error", err))
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
isReady = true
|
|
||||||
if len(models) > 0 {
|
|
||||||
// init analysis model for old user
|
|
||||||
hasAnalysis := false
|
|
||||||
for _, m := range models {
|
|
||||||
if m.Type == domain.ModelTypeAnalysis {
|
|
||||||
hasAnalysis = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasAnalysis {
|
|
||||||
if err := u.createAndSyncModelToRAGLite(ctx, "qwen2.5-3b-instruct", domain.ModelTypeAnalysis); err != nil {
|
|
||||||
return fmt.Errorf("add analysis model err: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !isReady {
|
|
||||||
return fmt.Errorf("raglite is not ready")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.createAndSyncModelToRAGLite(ctx, "bge-m3", domain.ModelTypeEmbedding); err != nil {
|
|
||||||
return fmt.Errorf("create and sync model err: %v", err)
|
|
||||||
}
|
|
||||||
if err := u.createAndSyncModelToRAGLite(ctx, "bge-reranker-v2-m3", domain.ModelTypeRerank); err != nil {
|
|
||||||
return fmt.Errorf("create and sync model err: %v", err)
|
|
||||||
}
|
|
||||||
if err := u.createAndSyncModelToRAGLite(ctx, "qwen2.5-3b-instruct", domain.ModelTypeAnalysis); err != nil {
|
|
||||||
return fmt.Errorf("create and sync model err: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ModelUsecase) createAndSyncModelToRAGLite(ctx context.Context, modelName string, modelType domain.ModelType) error {
|
|
||||||
// FIXME: just for test, remove it later
|
|
||||||
// shared_key by BaiZhiCloud
|
|
||||||
sharedKey := "sk-r8tmBtcU1JotPDPnlgZLOY4Z6Dbb7FufcSeTkFpRWA5v4Llr"
|
|
||||||
baseURL := "https://model-square.app.baizhi.cloud/v1"
|
|
||||||
model := &domain.Model{
|
|
||||||
ID: uuid.New().String(),
|
|
||||||
Provider: domain.ModelProviderBrandBaiZhiCloud,
|
|
||||||
Model: modelName,
|
|
||||||
APIKey: sharedKey,
|
|
||||||
APIHeader: "",
|
|
||||||
BaseURL: baseURL,
|
|
||||||
IsActive: true,
|
|
||||||
APIVersion: "",
|
|
||||||
Type: modelType,
|
|
||||||
}
|
|
||||||
id, err := u.ragStore.AddModel(ctx, model)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("init %s model failed: %w", modelName, err)
|
|
||||||
}
|
|
||||||
model.ID = id
|
|
||||||
if err := u.modelRepo.Create(ctx, model); err != nil {
|
|
||||||
return fmt.Errorf("create %s model failed: %w", modelName, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ModelUsecase) Create(ctx context.Context, model *domain.Model) error {
|
func (u *ModelUsecase) Create(ctx context.Context, model *domain.Model) error {
|
||||||
|
var updatedEmbeddingModel bool
|
||||||
|
if model.Type == domain.ModelTypeEmbedding {
|
||||||
|
updatedEmbeddingModel = true
|
||||||
|
}
|
||||||
if err := u.modelRepo.Create(ctx, model); err != nil {
|
if err := u.modelRepo.Create(ctx, model); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if model.Type == domain.ModelTypeEmbedding || model.Type == domain.ModelTypeRerank || model.Type == domain.ModelTypeAnalysis || model.Type == domain.ModelTypeAnalysisVL {
|
// 模型更新成功后,如果更新嵌入模型,则触发记录更新
|
||||||
if id, err := u.ragStore.AddModel(ctx, model); err != nil {
|
if updatedEmbeddingModel {
|
||||||
|
if _, err := u.updateModeSettingConfig(ctx, "", "", "", true); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
|
||||||
model.ID = id
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if model.Type == domain.ModelTypeEmbedding {
|
|
||||||
return u.TriggerUpsertRecords(ctx)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,44 +108,50 @@ func (u *ModelUsecase) TriggerUpsertRecords(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ModelUsecase) Update(ctx context.Context, req *domain.UpdateModelReq) error {
|
func (u *ModelUsecase) Update(ctx context.Context, req *domain.UpdateModelReq) error {
|
||||||
|
var updatedEmbeddingModel bool
|
||||||
|
if req.Type == domain.ModelTypeEmbedding {
|
||||||
|
updatedEmbeddingModel = true
|
||||||
|
}
|
||||||
if err := u.modelRepo.Update(ctx, req); err != nil {
|
if err := u.modelRepo.Update(ctx, req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ragModelTypes := []domain.ModelType{
|
// 模型更新成功后,如果更新嵌入模型,则触发记录更新
|
||||||
domain.ModelTypeEmbedding,
|
if updatedEmbeddingModel {
|
||||||
domain.ModelTypeRerank,
|
if _, err := u.updateModeSettingConfig(ctx, "", "", "", true); err != nil {
|
||||||
domain.ModelTypeAnalysis,
|
|
||||||
domain.ModelTypeAnalysisVL,
|
|
||||||
}
|
|
||||||
if lo.Contains(ragModelTypes, req.Type) {
|
|
||||||
updateModel := &domain.Model{
|
|
||||||
ID: req.ID,
|
|
||||||
Model: req.Model,
|
|
||||||
Type: req.Type,
|
|
||||||
BaseURL: req.BaseURL,
|
|
||||||
APIKey: req.APIKey,
|
|
||||||
IsActive: true,
|
|
||||||
}
|
|
||||||
if req.Parameters != nil {
|
|
||||||
updateModel.Parameters = *req.Parameters
|
|
||||||
}
|
|
||||||
// update is active flag for analysis models
|
|
||||||
if (req.Type == domain.ModelTypeAnalysis || req.Type == domain.ModelTypeAnalysisVL) && req.IsActive != nil {
|
|
||||||
updateModel.IsActive = *req.IsActive
|
|
||||||
}
|
|
||||||
if err := u.ragStore.UpdateModel(ctx, updateModel); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// update all records when embedding model is updated
|
|
||||||
if req.Type == domain.ModelTypeEmbedding {
|
|
||||||
return u.TriggerUpsertRecords(ctx)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ModelUsecase) GetChatModel(ctx context.Context) (*domain.Model, error) {
|
func (u *ModelUsecase) GetChatModel(ctx context.Context) (*domain.Model, error) {
|
||||||
return u.modelRepo.GetChatModel(ctx)
|
var model *domain.Model
|
||||||
|
modelModeSetting, err := u.GetModelModeSetting(ctx)
|
||||||
|
// 获取不到模型模式时,使用手动模式, 不返回错误
|
||||||
|
if err != nil {
|
||||||
|
u.logger.Error("get model mode setting failed, use manual mode", log.Error(err))
|
||||||
|
}
|
||||||
|
if err == nil && modelModeSetting.Mode == consts.ModelSettingModeAuto && modelModeSetting.AutoModeAPIKey != "" {
|
||||||
|
modelName := modelModeSetting.ChatModel
|
||||||
|
if modelName == "" {
|
||||||
|
modelName = string(consts.AutoModeDefaultChatModel)
|
||||||
|
}
|
||||||
|
model = &domain.Model{
|
||||||
|
Model: modelName,
|
||||||
|
Type: domain.ModelTypeChat,
|
||||||
|
IsActive: true,
|
||||||
|
BaseURL: consts.AutoModeBaseURL,
|
||||||
|
APIKey: modelModeSetting.AutoModeAPIKey,
|
||||||
|
Provider: domain.ModelProviderBrandBaiZhiCloud,
|
||||||
|
}
|
||||||
|
return model, nil
|
||||||
|
}
|
||||||
|
model, err = u.modelRepo.GetChatModel(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return model, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ModelUsecase) GetModelByType(ctx context.Context, modelType domain.ModelType) (*domain.Model, error) {
|
func (u *ModelUsecase) GetModelByType(ctx context.Context, modelType domain.ModelType) (*domain.Model, error) {
|
||||||
|
|
@ -225,3 +161,175 @@ func (u *ModelUsecase) GetModelByType(ctx context.Context, modelType domain.Mode
|
||||||
func (u *ModelUsecase) UpdateUsage(ctx context.Context, modelID string, usage *schema.TokenUsage) error {
|
func (u *ModelUsecase) UpdateUsage(ctx context.Context, modelID string, usage *schema.TokenUsage) error {
|
||||||
return u.modelRepo.UpdateUsage(ctx, modelID, usage)
|
return u.modelRepo.UpdateUsage(ctx, modelID, usage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ModelUsecase) SwitchMode(ctx context.Context, req *domain.SwitchModeReq) error {
|
||||||
|
// 只有配置正确才能切换模式
|
||||||
|
if req.Mode == string(consts.ModelSettingModeAuto) {
|
||||||
|
if req.AutoModeAPIKey == "" {
|
||||||
|
return fmt.Errorf("auto mode api key is required")
|
||||||
|
}
|
||||||
|
modelName := req.ChatModel
|
||||||
|
if modelName == "" {
|
||||||
|
modelName = consts.GetAutoModeDefaultModel(string(domain.ModelTypeChat))
|
||||||
|
}
|
||||||
|
// 检查 API Key 是否有效
|
||||||
|
check, err := u.modelkit.CheckModel(ctx, &modelkitDomain.CheckModelReq{
|
||||||
|
Provider: string(domain.ModelProviderBrandBaiZhiCloud),
|
||||||
|
Model: modelName,
|
||||||
|
BaseURL: consts.AutoModeBaseURL,
|
||||||
|
APIKey: req.AutoModeAPIKey,
|
||||||
|
Type: string(domain.ModelTypeChat),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("百智云模型 API Key 检查失败: %w", err)
|
||||||
|
}
|
||||||
|
if check.Error != "" {
|
||||||
|
return fmt.Errorf("百智云模型 API Key 检查失败: %s", check.Error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
needModelTypes := []domain.ModelType{
|
||||||
|
domain.ModelTypeChat,
|
||||||
|
domain.ModelTypeEmbedding,
|
||||||
|
domain.ModelTypeRerank,
|
||||||
|
domain.ModelTypeAnalysis,
|
||||||
|
}
|
||||||
|
for _, modelType := range needModelTypes {
|
||||||
|
if _, err := u.modelRepo.GetModelByType(ctx, modelType); err != nil {
|
||||||
|
return fmt.Errorf("需要配置 %s 模型", modelType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldModelModeSetting, err := u.GetModelModeSetting(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var isResetEmbeddingUpdateFlag = true
|
||||||
|
// 只有切换手动模式时,重置isManualEmbeddingUpdated为false
|
||||||
|
if req.Mode == string(consts.ModelSettingModeManual) {
|
||||||
|
isResetEmbeddingUpdateFlag = false
|
||||||
|
}
|
||||||
|
|
||||||
|
modelModeSetting, err := u.updateModeSettingConfig(ctx, req.Mode, req.AutoModeAPIKey, req.ChatModel, isResetEmbeddingUpdateFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.updateRAGModelsByMode(ctx, req.Mode, modelModeSetting.AutoModeAPIKey, oldModelModeSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateModeSettingConfig 读取当前设置并更新,然后持久化
|
||||||
|
func (u *ModelUsecase) updateModeSettingConfig(ctx context.Context, mode, apiKey, chatModel string, isManualEmbeddingUpdated bool) (*domain.ModelModeSetting, error) {
|
||||||
|
// 读取当前设置
|
||||||
|
setting, err := u.systemSettingRepo.GetSystemSetting(ctx, string(consts.SystemSettingModelMode))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get current model setting: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config domain.ModelModeSetting
|
||||||
|
if err := json.Unmarshal(setting.Value, &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse current model setting: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设置
|
||||||
|
if apiKey != "" {
|
||||||
|
config.AutoModeAPIKey = apiKey
|
||||||
|
}
|
||||||
|
if chatModel != "" {
|
||||||
|
config.ChatModel = chatModel
|
||||||
|
}
|
||||||
|
if mode != "" {
|
||||||
|
config.Mode = consts.ModelSettingMode(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.IsManualEmbeddingUpdated = isManualEmbeddingUpdated
|
||||||
|
|
||||||
|
// 持久化设置
|
||||||
|
updatedValue, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal updated model setting: %w", err)
|
||||||
|
}
|
||||||
|
if err := u.systemSettingRepo.UpdateSystemSetting(ctx, string(consts.SystemSettingModelMode), string(updatedValue)); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update model setting: %w", err)
|
||||||
|
}
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ModelUsecase) GetModelModeSetting(ctx context.Context) (domain.ModelModeSetting, error) {
|
||||||
|
setting, err := u.systemSettingRepo.GetSystemSetting(ctx, string(consts.SystemSettingModelMode))
|
||||||
|
if err != nil {
|
||||||
|
return domain.ModelModeSetting{}, fmt.Errorf("failed to get model mode setting: %w", err)
|
||||||
|
}
|
||||||
|
var config domain.ModelModeSetting
|
||||||
|
if err := json.Unmarshal(setting.Value, &config); err != nil {
|
||||||
|
return domain.ModelModeSetting{}, fmt.Errorf("failed to parse model mode setting: %w", err)
|
||||||
|
}
|
||||||
|
// 无效设置检查
|
||||||
|
if config == (domain.ModelModeSetting{}) || config.Mode == "" {
|
||||||
|
return domain.ModelModeSetting{}, fmt.Errorf("model mode setting is invalid")
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateRAGModelsByMode 根据模式更新 RAG 模型(embedding、rerank、analysis、analysisVL)
|
||||||
|
func (u *ModelUsecase) updateRAGModelsByMode(ctx context.Context, mode, autoModeAPIKey string, oldModelModeSetting domain.ModelModeSetting) error {
|
||||||
|
var isTriggerUpsertRecords = true
|
||||||
|
|
||||||
|
// 手动切换到手动模式, 根据IsManualEmbeddingUpdated字段决定
|
||||||
|
if oldModelModeSetting.Mode == consts.ModelSettingModeManual && mode == string(consts.ModelSettingModeManual) {
|
||||||
|
isTriggerUpsertRecords = oldModelModeSetting.IsManualEmbeddingUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
ragModelTypes := []domain.ModelType{
|
||||||
|
domain.ModelTypeEmbedding,
|
||||||
|
domain.ModelTypeRerank,
|
||||||
|
domain.ModelTypeAnalysis,
|
||||||
|
domain.ModelTypeAnalysisVL,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, modelType := range ragModelTypes {
|
||||||
|
var model *domain.Model
|
||||||
|
|
||||||
|
if mode == string(consts.ModelSettingModeManual) {
|
||||||
|
// 获取该类型的活跃模型
|
||||||
|
m, err := u.modelRepo.GetModelByType(ctx, modelType)
|
||||||
|
if err != nil {
|
||||||
|
u.logger.Warn("failed to get model by type", log.String("type", string(modelType)), log.Any("error", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m == nil || !m.IsActive {
|
||||||
|
u.logger.Warn("no active model found for type", log.String("type", string(modelType)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
model = m
|
||||||
|
} else {
|
||||||
|
modelName := consts.GetAutoModeDefaultModel(string(modelType))
|
||||||
|
model = &domain.Model{
|
||||||
|
Model: modelName,
|
||||||
|
Type: modelType,
|
||||||
|
IsActive: true,
|
||||||
|
BaseURL: consts.AutoModeBaseURL,
|
||||||
|
APIKey: autoModeAPIKey,
|
||||||
|
Provider: domain.ModelProviderBrandBaiZhiCloud,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新RAG存储中的模型
|
||||||
|
if model != nil {
|
||||||
|
// rag store中更新失败不影响其他模型更新
|
||||||
|
if err := u.ragStore.UpdateModel(ctx, model); err != nil {
|
||||||
|
u.logger.Error("failed to update model in RAG store", log.String("model_id", model.ID), log.String("type", string(modelType)), log.Any("error", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.logger.Info("successfully updated RAG model", log.String("model name: ", string(model.Model)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发记录更新
|
||||||
|
if isTriggerUpsertRecords {
|
||||||
|
u.logger.Info("embedding model updated, triggering upsert records")
|
||||||
|
return u.TriggerUpsertRecords(ctx)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ type NodeUsecase struct {
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
s3Client *s3.MinioClient
|
s3Client *s3.MinioClient
|
||||||
rAGService rag.RAGService
|
rAGService rag.RAGService
|
||||||
|
modelUsecase *ModelUsecase
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNodeUsecase(
|
func NewNodeUsecase(
|
||||||
|
|
@ -52,6 +53,7 @@ func NewNodeUsecase(
|
||||||
s3Client *s3.MinioClient,
|
s3Client *s3.MinioClient,
|
||||||
modelRepo *pg.ModelRepository,
|
modelRepo *pg.ModelRepository,
|
||||||
authRepo *pg.AuthRepo,
|
authRepo *pg.AuthRepo,
|
||||||
|
modelUsecase *ModelUsecase,
|
||||||
) *NodeUsecase {
|
) *NodeUsecase {
|
||||||
return &NodeUsecase{
|
return &NodeUsecase{
|
||||||
nodeRepo: nodeRepo,
|
nodeRepo: nodeRepo,
|
||||||
|
|
@ -65,6 +67,7 @@ func NewNodeUsecase(
|
||||||
modelRepo: modelRepo,
|
modelRepo: modelRepo,
|
||||||
logger: logger.WithModule("usecase.node"),
|
logger: logger.WithModule("usecase.node"),
|
||||||
s3Client: s3Client,
|
s3Client: s3Client,
|
||||||
|
modelUsecase: modelUsecase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +223,7 @@ func (u *NodeUsecase) MoveNode(ctx context.Context, req *domain.MoveNodeReq) err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *NodeUsecase) SummaryNode(ctx context.Context, req *domain.NodeSummaryReq) (string, error) {
|
func (u *NodeUsecase) SummaryNode(ctx context.Context, req *domain.NodeSummaryReq) (string, error) {
|
||||||
model, err := u.modelRepo.GetChatModel(ctx)
|
model, err := u.modelUsecase.GetChatModel(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
return "", domain.ErrModelNotConfigured
|
return "", domain.ErrModelNotConfigured
|
||||||
|
|
@ -573,3 +576,30 @@ func (u *NodeUsecase) SyncRagNodeStatus(ctx context.Context) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *NodeUsecase) NodeRestudy(ctx context.Context, req *v1.NodeRestudyReq) error {
|
||||||
|
nodeReleases, err := u.nodeRepo.GetLatestNodeReleaseByNodeIDs(ctx, req.KbId, req.NodeIds)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get latest node release failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nodeRelease := range nodeReleases {
|
||||||
|
if nodeRelease.DocID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := u.ragRepo.AsyncUpdateNodeReleaseVector(ctx, []*domain.NodeReleaseVectorRequest{
|
||||||
|
{
|
||||||
|
KBID: nodeRelease.KBID,
|
||||||
|
NodeReleaseID: nodeRelease.ID,
|
||||||
|
Action: "upsert",
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
u.logger.Error("async update node release vector failed",
|
||||||
|
log.String("node_release_id", nodeRelease.ID),
|
||||||
|
log.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Box, Stepper, Step, StepLabel } from '@mui/material';
|
import { Box, Stepper, Step, StepLabel } from '@mui/material';
|
||||||
import { Modal } from '@ctzhian/ui';
|
import { Modal, message } from '@ctzhian/ui';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
setKbC,
|
setKbC,
|
||||||
|
|
@ -10,18 +10,20 @@ import {
|
||||||
import { useAppSelector, useAppDispatch } from '@/store';
|
import { useAppSelector, useAppDispatch } from '@/store';
|
||||||
import { postApiV1KnowledgeBaseRelease } from '@/request/KnowledgeBase';
|
import { postApiV1KnowledgeBaseRelease } from '@/request/KnowledgeBase';
|
||||||
import {
|
import {
|
||||||
Step1Config,
|
Step1Model,
|
||||||
Step2Import,
|
Step2Config,
|
||||||
Step3Publish,
|
Step3Import,
|
||||||
Step4Test,
|
Step4Publish,
|
||||||
Step5Decorate,
|
Step5Test,
|
||||||
Step6Complete,
|
Step6Decorate,
|
||||||
|
Step7Complete,
|
||||||
} from './steps';
|
} from './steps';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// Remove interface as we're using Redux state
|
// Remove interface as we're using Redux state
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
|
'模型配置',
|
||||||
'配置监听',
|
'配置监听',
|
||||||
'录入文档',
|
'录入文档',
|
||||||
'发布内容',
|
'发布内容',
|
||||||
|
|
@ -31,20 +33,19 @@ const steps = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const CreateWikiModal = () => {
|
const CreateWikiModal = () => {
|
||||||
const { kb_c, kb_id, kbList, modelStatus } = useAppSelector(
|
const { kb_c, kb_id, kbList } = useAppSelector(state => state.config);
|
||||||
state => state.config,
|
|
||||||
);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [nodeIds, setNodeIds] = useState<string[]>([]);
|
const [nodeIds, setNodeIds] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const step1ConfigRef = useRef<{ onSubmit: () => Promise<void> }>(null);
|
const Step1ModelRef = useRef<{ onSubmit: () => Promise<void> }>(null);
|
||||||
const step2ImportRef = useRef<{
|
const step2ConfigRef = useRef<{ onSubmit: () => Promise<void> }>(null);
|
||||||
|
const step3ImportRef = useRef<{
|
||||||
onSubmit: () => Promise<Record<'id', string>[]>;
|
onSubmit: () => Promise<Record<'id', string>[]>;
|
||||||
}>(null);
|
}>(null);
|
||||||
const step5DecorateRef = useRef<{ onSubmit: () => Promise<void> }>(null);
|
const step6DecorateRef = useRef<{ onSubmit: () => Promise<void> }>(null);
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
dispatch(setKbC(false));
|
dispatch(setKbC(false));
|
||||||
|
|
@ -66,7 +67,20 @@ const CreateWikiModal = () => {
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (activeStep === 0) {
|
if (activeStep === 0) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
step1ConfigRef.current
|
Step1ModelRef.current
|
||||||
|
?.onSubmit?.()
|
||||||
|
.then(() => {
|
||||||
|
setActiveStep(prev => prev + 1);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
message.error(error.message || '模型配置验证失败');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
} else if (activeStep === 1) {
|
||||||
|
setLoading(true);
|
||||||
|
step2ConfigRef.current
|
||||||
?.onSubmit?.()
|
?.onSubmit?.()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setActiveStep(prev => prev + 1);
|
setActiveStep(prev => prev + 1);
|
||||||
|
|
@ -74,9 +88,9 @@ const CreateWikiModal = () => {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
} else if (activeStep === 1) {
|
} else if (activeStep === 2) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
step2ImportRef.current
|
step3ImportRef.current
|
||||||
?.onSubmit?.()
|
?.onSubmit?.()
|
||||||
.then(res => {
|
.then(res => {
|
||||||
setNodeIds(res.map(item => item.id));
|
setNodeIds(res.map(item => item.id));
|
||||||
|
|
@ -85,17 +99,17 @@ const CreateWikiModal = () => {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
} else if (activeStep === 2) {
|
} else if (activeStep === 3) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
onPublish().finally(() => {
|
onPublish().finally(() => {
|
||||||
setActiveStep(prev => prev + 1);
|
setActiveStep(prev => prev + 1);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
} else if (activeStep === 3) {
|
|
||||||
setActiveStep(prev => prev + 1);
|
|
||||||
} else if (activeStep === 4) {
|
} else if (activeStep === 4) {
|
||||||
|
setActiveStep(prev => prev + 1);
|
||||||
|
} else if (activeStep === 5) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
step5DecorateRef.current
|
step6DecorateRef.current
|
||||||
?.onSubmit?.()
|
?.onSubmit?.()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setActiveStep(prev => prev + 1);
|
setActiveStep(prev => prev + 1);
|
||||||
|
|
@ -103,7 +117,7 @@ const CreateWikiModal = () => {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
} else if (activeStep === 5) {
|
} else if (activeStep === 6) {
|
||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -117,17 +131,19 @@ const CreateWikiModal = () => {
|
||||||
const renderStepContent = () => {
|
const renderStepContent = () => {
|
||||||
switch (activeStep) {
|
switch (activeStep) {
|
||||||
case 0:
|
case 0:
|
||||||
return <Step1Config ref={step1ConfigRef} />;
|
return <Step1Model ref={Step1ModelRef} />;
|
||||||
case 1:
|
case 1:
|
||||||
return <Step2Import ref={step2ImportRef} />;
|
return <Step2Config ref={step2ConfigRef} />;
|
||||||
case 2:
|
case 2:
|
||||||
return <Step3Publish />;
|
return <Step3Import ref={step3ImportRef} />;
|
||||||
case 3:
|
case 3:
|
||||||
return <Step4Test />;
|
return <Step4Publish />;
|
||||||
case 4:
|
case 4:
|
||||||
return <Step5Decorate ref={step5DecorateRef} nodeIds={nodeIds} />;
|
return <Step5Test />;
|
||||||
case 5:
|
case 5:
|
||||||
return <Step6Complete />;
|
return <Step6Decorate ref={step6DecorateRef} nodeIds={nodeIds} />;
|
||||||
|
case 6:
|
||||||
|
return <Step7Complete />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -148,8 +164,8 @@ const CreateWikiModal = () => {
|
||||||
}, [kb_c]);
|
}, [kb_c]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (kbList?.length === 0 && modelStatus) setOpen(true);
|
if (kbList?.length === 0) setOpen(true);
|
||||||
}, [kbList, modelStatus]);
|
}, [kbList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
useImperativeHandle,
|
||||||
|
Ref,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import { useAppSelector, useAppDispatch } from '@/store';
|
||||||
|
import { setModelList } from '@/store/slices/config';
|
||||||
|
import { getApiV1ModelList, getApiV1ModelModeSetting } from '@/request/Model';
|
||||||
|
import { GithubComChaitinPandaWikiDomainModelListItem } from '@/request/types';
|
||||||
|
import ModelConfig, {
|
||||||
|
ModelConfigRef,
|
||||||
|
} from '@/components/System/component/ModelConfig';
|
||||||
|
|
||||||
|
interface Step1ModelProps {
|
||||||
|
ref: Ref<{ onSubmit: () => Promise<void> }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Step1Model: React.FC<Step1ModelProps> = ({ ref }) => {
|
||||||
|
const { modelList } = useAppSelector(state => state.config);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const modelConfigRef = useRef<ModelConfigRef>(null);
|
||||||
|
|
||||||
|
const [chatModelData, setChatModelData] =
|
||||||
|
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
|
||||||
|
const [embeddingModelData, setEmbeddingModelData] =
|
||||||
|
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
|
||||||
|
const [rerankModelData, setRerankModelData] =
|
||||||
|
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
|
||||||
|
const [analysisModelData, setAnalysisModelData] =
|
||||||
|
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
|
||||||
|
const [analysisVLModelData, setAnalysisVLModelData] =
|
||||||
|
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
|
||||||
|
|
||||||
|
const getModelList = () => {
|
||||||
|
getApiV1ModelList().then(res => {
|
||||||
|
dispatch(
|
||||||
|
setModelList(res as GithubComChaitinPandaWikiDomainModelListItem[]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModelList = (
|
||||||
|
list: GithubComChaitinPandaWikiDomainModelListItem[],
|
||||||
|
) => {
|
||||||
|
const chat = list.find(it => it.type === 'chat') || null;
|
||||||
|
const embedding = list.find(it => it.type === 'embedding') || null;
|
||||||
|
const rerank = list.find(it => it.type === 'rerank') || null;
|
||||||
|
const analysis = list.find(it => it.type === 'analysis') || null;
|
||||||
|
const analysisVL = list.find(it => it.type === 'analysis-vl') || null;
|
||||||
|
setChatModelData(chat);
|
||||||
|
setEmbeddingModelData(embedding);
|
||||||
|
setRerankModelData(rerank);
|
||||||
|
setAnalysisModelData(analysis);
|
||||||
|
setAnalysisVLModelData(analysisVL);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modelList) {
|
||||||
|
handleModelList(modelList);
|
||||||
|
}
|
||||||
|
}, [modelList]);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
// 检查模型模式设置
|
||||||
|
try {
|
||||||
|
const modeSetting = await getApiV1ModelModeSetting();
|
||||||
|
|
||||||
|
// 如果是 auto 模式,检查是否配置了 API key
|
||||||
|
if (modeSetting?.mode === 'auto') {
|
||||||
|
if (!modeSetting.auto_mode_api_key) {
|
||||||
|
return Promise.reject(new Error('请点击应用完成模型配置'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 手动模式检查
|
||||||
|
if (
|
||||||
|
!chatModelData ||
|
||||||
|
!embeddingModelData ||
|
||||||
|
!rerankModelData ||
|
||||||
|
!analysisModelData
|
||||||
|
) {
|
||||||
|
return Promise.reject(new Error('请配置必要的模型后点击应用'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('配置模型失败'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
onSubmit,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<ModelConfig
|
||||||
|
ref={modelConfigRef}
|
||||||
|
onCloseModal={() => {}}
|
||||||
|
chatModelData={chatModelData}
|
||||||
|
embeddingModelData={embeddingModelData}
|
||||||
|
rerankModelData={rerankModelData}
|
||||||
|
analysisModelData={analysisModelData}
|
||||||
|
analysisVLModelData={analysisVLModelData}
|
||||||
|
getModelList={getModelList}
|
||||||
|
hideDocumentationHint={true}
|
||||||
|
showTip={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Step1Model;
|
||||||
|
|
@ -73,11 +73,11 @@ const VALIDATION_RULES = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Step1ConfigProps {
|
interface Step2ConfigProps {
|
||||||
ref: Ref<{ onSubmit: () => Promise<unknown> }>;
|
ref: Ref<{ onSubmit: () => Promise<unknown> }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Step1Config: React.FC<Step1ConfigProps> = ({ ref }) => {
|
const Step2Config: React.FC<Step2ConfigProps> = ({ ref }) => {
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
|
|
@ -358,4 +358,4 @@ const Step1Config: React.FC<Step1ConfigProps> = ({ ref }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Step1Config;
|
export default Step2Config;
|
||||||
|
|
@ -5,11 +5,11 @@ import { postApiV1Node } from '@/request/Node';
|
||||||
import { INIT_DOC_DATA } from './initData';
|
import { INIT_DOC_DATA } from './initData';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
|
||||||
interface Step2ImportProps {
|
interface Step3ImportProps {
|
||||||
ref: Ref<{ onSubmit: () => Promise<Record<'id', string>[]> }>;
|
ref: Ref<{ onSubmit: () => Promise<Record<'id', string>[]> }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Step2Import: React.FC<Step2ImportProps> = ({ ref }) => {
|
const Step3Import: React.FC<Step3ImportProps> = ({ ref }) => {
|
||||||
const { kb_id } = useAppSelector(state => state.config);
|
const { kb_id } = useAppSelector(state => state.config);
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
|
|
@ -42,4 +42,4 @@ const Step2Import: React.FC<Step2ImportProps> = ({ ref }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Step2Import;
|
export default Step3Import;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Box, Stack, FormControlLabel, Checkbox } from '@mui/material';
|
import { Box, Stack, FormControlLabel, Checkbox } from '@mui/material';
|
||||||
import publish from '@/assets/images/init/publish.png';
|
import publish from '@/assets/images/init/publish.png';
|
||||||
|
|
||||||
const Step3Publish = () => {
|
const Step4Publish = () => {
|
||||||
return (
|
return (
|
||||||
<Stack gap={2} sx={{ textAlign: 'center', py: 4 }}>
|
<Stack gap={2} sx={{ textAlign: 'center', py: 4 }}>
|
||||||
<Box component='img' src={publish} sx={{ width: '100%' }}></Box>
|
<Box component='img' src={publish} sx={{ width: '100%' }}></Box>
|
||||||
|
|
@ -18,4 +18,4 @@ const Step3Publish = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Step3Publish;
|
export default Step4Publish;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Box, Stack } from '@mui/material';
|
import { Box, Stack } from '@mui/material';
|
||||||
import test from '@/assets/images/init/test.png';
|
import test from '@/assets/images/init/test.png';
|
||||||
|
|
||||||
const Step4Test = () => {
|
const Step5Test = () => {
|
||||||
return (
|
return (
|
||||||
<Stack gap={2} sx={{ textAlign: 'center', py: 4 }}>
|
<Stack gap={2} sx={{ textAlign: 'center', py: 4 }}>
|
||||||
<Box component='img' src={test} sx={{ width: '100%' }}></Box>
|
<Box component='img' src={test} sx={{ width: '100%' }}></Box>
|
||||||
|
|
@ -9,4 +9,4 @@ const Step4Test = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Step4Test;
|
export default Step5Test;
|
||||||
|
|
@ -5,12 +5,12 @@ import { INIT_LADING_DATA } from './initData';
|
||||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
|
||||||
interface Step5DecorateProps {
|
interface Step6DecorateProps {
|
||||||
ref: Ref<{ onSubmit: () => void }>;
|
ref: Ref<{ onSubmit: () => void }>;
|
||||||
nodeIds: string[];
|
nodeIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Step5Decorate: React.FC<Step5DecorateProps> = ({ ref, nodeIds }) => {
|
const Step6Decorate: React.FC<Step6DecorateProps> = ({ ref, nodeIds }) => {
|
||||||
const { kb_id } = useAppSelector(state => state.config);
|
const { kb_id } = useAppSelector(state => state.config);
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
return getApiV1AppDetail({
|
return getApiV1AppDetail({
|
||||||
|
|
@ -60,4 +60,4 @@ const Step5Decorate: React.FC<Step5DecorateProps> = ({ ref, nodeIds }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Step5Decorate;
|
export default Step6Decorate;
|
||||||
|
|
@ -3,7 +3,7 @@ import { Box, Stack, Button } from '@mui/material';
|
||||||
import complete from '@/assets/images/init/complete.png';
|
import complete from '@/assets/images/init/complete.png';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
|
||||||
const Step6Complete = () => {
|
const Step7Complete = () => {
|
||||||
const { kbDetail } = useAppSelector(state => state.config);
|
const { kbDetail } = useAppSelector(state => state.config);
|
||||||
|
|
||||||
const wikiUrl = useMemo(() => {
|
const wikiUrl = useMemo(() => {
|
||||||
|
|
@ -56,4 +56,4 @@ const Step6Complete = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Step6Complete;
|
export default Step7Complete;
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export { default as Step1Config } from './Step1Config';
|
export { default as Step1Model } from './Step1Model';
|
||||||
export { default as Step2Import } from './Step2Import';
|
export { default as Step2Config } from './Step2Config';
|
||||||
export { default as Step3Publish } from './Step3Publish';
|
export { default as Step3Import } from './Step3Import';
|
||||||
export { default as Step4Test } from './Step4Test';
|
export { default as Step4Publish } from './Step4Publish';
|
||||||
export { default as Step5Decorate } from './Step5Decorate';
|
export { default as Step5Test } from './Step5Test';
|
||||||
export { default as Step6Complete } from './Step6Complete';
|
export { default as Step6Decorate } from './Step6Decorate';
|
||||||
|
export { default as Step7Complete } from './Step7Complete';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { FooterSetting } from '@/api/type';
|
import { FooterSetting } from '@/api/type';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store';
|
import { Icon } from '@ctzhian/ui';
|
||||||
import {
|
import {
|
||||||
closestCenter,
|
closestCenter,
|
||||||
DndContext,
|
DndContext,
|
||||||
|
|
@ -18,22 +18,15 @@ import {
|
||||||
useSortable,
|
useSortable,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { Box, Button, IconButton, Stack, TextField } from '@mui/material';
|
import { Box, IconButton, Stack, TextField } from '@mui/material';
|
||||||
import { Icon } from '@ctzhian/ui';
|
|
||||||
import {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
HTMLAttributes,
|
HTMLAttributes,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {
|
import { Control, Controller, FieldErrors } from 'react-hook-form';
|
||||||
Control,
|
|
||||||
Controller,
|
|
||||||
FieldErrors,
|
|
||||||
useFieldArray,
|
|
||||||
} from 'react-hook-form';
|
|
||||||
import { BrandGroup } from '.';
|
import { BrandGroup } from '.';
|
||||||
|
|
||||||
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
||||||
|
|
@ -122,7 +115,7 @@ const LinkItem = forwardRef<HTMLDivElement, LinkItemProps>(
|
||||||
size='small'
|
size='small'
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
':hover': { color: 'error.main' },
|
':hover': { color: 'error.main' },
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
width: '28px',
|
width: '28px',
|
||||||
|
|
@ -395,7 +388,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
|
||||||
size='small'
|
size='small'
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
':hover': { color: 'error.main' },
|
':hover': { color: 'error.main' },
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
width: '28px',
|
width: '28px',
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import UploadFile from '@/components/UploadFile';
|
import UploadFile from '@/components/UploadFile';
|
||||||
import { DomainSocialMediaAccount } from '@/request/types';
|
import { DomainSocialMediaAccount } from '@/request/types';
|
||||||
|
import { Icon } from '@ctzhian/ui';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
FormControl,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -11,7 +11,6 @@ import {
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Icon } from '@ctzhian/ui';
|
|
||||||
import {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
|
|
@ -113,7 +112,7 @@ const Item = forwardRef<HTMLDivElement, SocialInfoProps>(
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
':hover': { color: 'error.main' },
|
':hover': { color: 'error.main' },
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
width: '28px',
|
width: '28px',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
import {
|
||||||
|
closestCenter,
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
DragOverlay,
|
||||||
|
DragStartEvent,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
rectSortingStrategy,
|
||||||
|
SortableContext,
|
||||||
|
SortingStrategy,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { Stack, SxProps, Theme } from '@mui/material';
|
||||||
|
import {
|
||||||
|
ComponentType,
|
||||||
|
CSSProperties,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
export interface DragListProps<T extends { id?: string | null }> {
|
||||||
|
data: T[];
|
||||||
|
onChange: (data: T[]) => void;
|
||||||
|
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||||
|
SortableItemComponent: ComponentType<{
|
||||||
|
id: string;
|
||||||
|
item: T;
|
||||||
|
handleRemove: (id: string) => void;
|
||||||
|
handleUpdateItem: (item: T) => void;
|
||||||
|
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}>;
|
||||||
|
ItemComponent: ComponentType<{
|
||||||
|
isDragging?: boolean;
|
||||||
|
item: T;
|
||||||
|
style?: CSSProperties;
|
||||||
|
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||||
|
handleUpdateItem?: (item: T) => void;
|
||||||
|
}>;
|
||||||
|
containerSx?: SxProps<Theme>;
|
||||||
|
sortingStrategy?: SortingStrategy;
|
||||||
|
direction?: 'row' | 'column';
|
||||||
|
gap?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DragList<T extends { id?: string | null }>({
|
||||||
|
data,
|
||||||
|
onChange,
|
||||||
|
setIsEdit,
|
||||||
|
SortableItemComponent,
|
||||||
|
ItemComponent,
|
||||||
|
containerSx,
|
||||||
|
sortingStrategy = rectSortingStrategy,
|
||||||
|
direction = 'row',
|
||||||
|
gap = 2,
|
||||||
|
}: DragListProps<T>) {
|
||||||
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
|
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
||||||
|
const dataRef = useRef(data);
|
||||||
|
|
||||||
|
// 保持 ref 与 data 同步
|
||||||
|
useEffect(() => {
|
||||||
|
dataRef.current = data;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||||
|
setActiveId(event.active.id as string);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (active.id !== over?.id) {
|
||||||
|
const currentData = dataRef.current;
|
||||||
|
const oldIndex = currentData.findIndex(
|
||||||
|
item => (item.id || '') === active.id,
|
||||||
|
);
|
||||||
|
const newIndex = currentData.findIndex(
|
||||||
|
item => (item.id || '') === over!.id,
|
||||||
|
);
|
||||||
|
const newData = arrayMove(currentData, oldIndex, newIndex);
|
||||||
|
onChange(newData);
|
||||||
|
}
|
||||||
|
setActiveId(null);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDragCancel = useCallback(() => {
|
||||||
|
setActiveId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRemove = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const currentData = dataRef.current;
|
||||||
|
const newData = currentData.filter(item => (item.id || '') !== id);
|
||||||
|
onChange(newData);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateItem = useCallback(
|
||||||
|
(updatedItem: T) => {
|
||||||
|
const currentData = dataRef.current;
|
||||||
|
const newData = currentData.map(item =>
|
||||||
|
(item.id || '') === (updatedItem.id || '') ? updatedItem : item,
|
||||||
|
);
|
||||||
|
onChange(newData);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragCancel={handleDragCancel}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={data.map(item => item.id || '')}
|
||||||
|
strategy={sortingStrategy}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={direction}
|
||||||
|
flexWrap={'wrap'}
|
||||||
|
gap={gap}
|
||||||
|
sx={containerSx}
|
||||||
|
>
|
||||||
|
{data.map(item => (
|
||||||
|
<SortableItemComponent
|
||||||
|
key={item.id || ''}
|
||||||
|
id={item.id || ''}
|
||||||
|
item={item}
|
||||||
|
handleRemove={handleRemove}
|
||||||
|
handleUpdateItem={handleUpdateItem}
|
||||||
|
setIsEdit={setIsEdit}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</SortableContext>
|
||||||
|
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
||||||
|
{activeId ? (
|
||||||
|
<ItemComponent
|
||||||
|
isDragging
|
||||||
|
item={data.find(item => (item.id || '') === activeId)!}
|
||||||
|
setIsEdit={setIsEdit}
|
||||||
|
handleUpdateItem={handleUpdateItem}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DragList;
|
||||||
|
|
@ -1,11 +1,22 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { FC } from 'react';
|
import { ComponentType } from 'react';
|
||||||
import FaqItem, { ItemTypeProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemTypeProps & {};
|
export interface SortableItemProps<T extends { id?: string | null }> {
|
||||||
|
id: string;
|
||||||
|
item: T;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
ItemComponent: ComponentType<any>;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
function SortableItem<T extends { id?: string | null }>({
|
||||||
|
id,
|
||||||
|
item,
|
||||||
|
ItemComponent,
|
||||||
|
...rest
|
||||||
|
}: SortableItemProps<T>) {
|
||||||
const {
|
const {
|
||||||
isDragging,
|
isDragging,
|
||||||
attributes,
|
attributes,
|
||||||
|
|
@ -13,7 +24,7 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
||||||
setNodeRef,
|
setNodeRef,
|
||||||
transform,
|
transform,
|
||||||
transition,
|
transition,
|
||||||
} = useSortable({ id: item.id });
|
} = useSortable({ id });
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
|
|
@ -21,7 +32,7 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FaqItem
|
<ItemComponent
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
withOpacity={isDragging}
|
withOpacity={isDragging}
|
||||||
|
|
@ -33,6 +44,6 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default SortableItem;
|
export default SortableItem;
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export { default as DragList } from './DragList';
|
||||||
|
export type { DragListProps } from './DragList';
|
||||||
|
|
||||||
|
export type { SortableItemProps } from './SortableItem';
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item from './Item';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
type ItemType = {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
type: 'contained' | 'outlined' | 'text';
|
|
||||||
href: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: ItemType[];
|
|
||||||
onChange: (data: ItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
type: 'contained' | 'outlined' | 'text';
|
|
||||||
href: string;
|
|
||||||
}) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { Box, IconButton, Stack, TextField } from '@mui/material';
|
||||||
|
import { Icon } from '@ctzhian/ui';
|
||||||
|
import {
|
||||||
|
CSSProperties,
|
||||||
|
Dispatch,
|
||||||
|
forwardRef,
|
||||||
|
HTMLAttributes,
|
||||||
|
SetStateAction,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
type HotSearchItem = {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HotSearchItemProps = Omit<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
'onChange'
|
||||||
|
> & {
|
||||||
|
item: HotSearchItem;
|
||||||
|
withOpacity?: boolean;
|
||||||
|
isDragging?: boolean;
|
||||||
|
dragHandleProps?: React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
handleRemove?: (id: string) => void;
|
||||||
|
handleUpdateItem?: (item: HotSearchItem) => void;
|
||||||
|
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HotSearchItem = forwardRef<HTMLDivElement, HotSearchItemProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
item,
|
||||||
|
withOpacity,
|
||||||
|
isDragging,
|
||||||
|
style,
|
||||||
|
dragHandleProps,
|
||||||
|
handleRemove,
|
||||||
|
handleUpdateItem,
|
||||||
|
setIsEdit,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const inlineStyles: CSSProperties = {
|
||||||
|
opacity: withOpacity ? '0.5' : '1',
|
||||||
|
borderRadius: '10px',
|
||||||
|
cursor: isDragging ? 'grabbing' : 'grab',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
width: '100%',
|
||||||
|
...style,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box ref={ref} style={inlineStyles} {...props}>
|
||||||
|
<Stack
|
||||||
|
direction={'row'}
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'space-between'}
|
||||||
|
gap={0.5}
|
||||||
|
sx={{
|
||||||
|
py: 1.5,
|
||||||
|
px: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={'column'}
|
||||||
|
gap={'20px'}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
p: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
label='搜索关键词'
|
||||||
|
slotProps={{
|
||||||
|
inputLabel: {
|
||||||
|
shrink: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
height: '36px',
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
height: '36px',
|
||||||
|
padding: '0 12px',
|
||||||
|
'& .MuiOutlinedInput-input': {
|
||||||
|
padding: '8px 0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
placeholder='请输入搜索关键词'
|
||||||
|
variant='outlined'
|
||||||
|
value={item.text}
|
||||||
|
onChange={e => {
|
||||||
|
const updatedItem = { ...item, text: e.target.value };
|
||||||
|
handleUpdateItem?.(updatedItem);
|
||||||
|
setIsEdit(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction={'column'}
|
||||||
|
sx={{ justifyContent: 'space-between', alignSelf: 'stretch' }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemove?.(item.id);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'text.tertiary',
|
||||||
|
':hover': { color: 'error.main' },
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type='icon-shanchu2' sx={{ fontSize: '12px' }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
sx={{
|
||||||
|
cursor: 'grab',
|
||||||
|
color: 'text.secondary',
|
||||||
|
'&:hover': { color: 'primary.main' },
|
||||||
|
}}
|
||||||
|
{...(dragHandleProps as any)}
|
||||||
|
>
|
||||||
|
<Icon type='icon-drag' />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default HotSearchItem;
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import Item, { ItemProps } from './Item';
|
|
||||||
|
|
||||||
type FaqSortableItemProps = ItemProps & {};
|
|
||||||
|
|
||||||
const FaqSortableItem: FC<FaqSortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaqSortableItem;
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useRef, useMemo } from 'react';
|
||||||
import { TextField, Chip, Autocomplete, Box } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import DragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
|
import HotSearchItem from './HotSearchItem';
|
||||||
import UploadFile from '@/components/UploadFile';
|
import UploadFile from '@/components/UploadFile';
|
||||||
import { DEFAULT_DATA } from '../../../constants';
|
import { DEFAULT_DATA } from '../../../constants';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
import { handleLandingConfigs, findConfigById } from '../../../utils';
|
import { handleLandingConfigs, findConfigById } from '../../../utils';
|
||||||
|
import { Empty } from '@ctzhian/ui';
|
||||||
|
|
||||||
const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
||||||
const { appPreviewData } = useAppSelector(state => state.config);
|
const { appPreviewData } = useAppSelector(state => state.config);
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const { control, watch, setValue, subscribe } = useForm<
|
const { control, watch, setValue, subscribe } = useForm<
|
||||||
typeof DEFAULT_DATA.banner
|
typeof DEFAULT_DATA.banner
|
||||||
>({
|
>({
|
||||||
|
|
@ -24,6 +27,49 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
||||||
|
|
||||||
const debouncedDispatch = useDebounceAppPreviewData();
|
const debouncedDispatch = useDebounceAppPreviewData();
|
||||||
const btns = watch('btns') || [];
|
const btns = watch('btns') || [];
|
||||||
|
const hotSearch = watch('hot_search') || [];
|
||||||
|
|
||||||
|
// 使用 ref 来维护稳定的 ID 映射
|
||||||
|
const idMapRef = useRef<Map<number, string>>(new Map());
|
||||||
|
|
||||||
|
// 将string[]转换为对象数组用于显示,保持 ID 稳定
|
||||||
|
const hotSearchList = Array.isArray(hotSearch)
|
||||||
|
? hotSearch.map((text, index) => {
|
||||||
|
// 如果该索引没有 ID,生成一个新的
|
||||||
|
if (!idMapRef.current.has(index)) {
|
||||||
|
idMapRef.current.set(
|
||||||
|
index,
|
||||||
|
`${Date.now()}-${index}-${Math.random()}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: idMapRef.current.get(index)!,
|
||||||
|
text: String(text),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 清理不再使用的 ID,并确保所有索引都有 ID
|
||||||
|
useEffect(() => {
|
||||||
|
const currentIndexes = new Set(hotSearch.map((_, index) => index));
|
||||||
|
|
||||||
|
// 清理不存在的索引
|
||||||
|
const keysToDelete: number[] = [];
|
||||||
|
idMapRef.current.forEach((_, key) => {
|
||||||
|
if (!currentIndexes.has(key)) {
|
||||||
|
keysToDelete.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
keysToDelete.forEach(key => idMapRef.current.delete(key));
|
||||||
|
|
||||||
|
// 确保每个索引都有 ID
|
||||||
|
hotSearch.forEach((_, index) => {
|
||||||
|
if (!idMapRef.current.has(index)) {
|
||||||
|
idMapRef.current.set(index, `${Date.now()}-${index}-${Math.random()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hotSearch.length]);
|
||||||
|
|
||||||
const handleAddButton = () => {
|
const handleAddButton = () => {
|
||||||
const nextId = `${Date.now()}`;
|
const nextId = `${Date.now()}`;
|
||||||
|
|
@ -33,6 +79,44 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddHotSearch = () => {
|
||||||
|
const newIndex = hotSearch.length;
|
||||||
|
const nextId = `${Date.now()}-${newIndex}-${Math.random()}`;
|
||||||
|
idMapRef.current.set(newIndex, nextId);
|
||||||
|
// 转换回string[]格式
|
||||||
|
setValue('hot_search', [...hotSearch, '']);
|
||||||
|
setIsEdit(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHotSearchChange = (newList: { id: string; text: string }[]) => {
|
||||||
|
// 重建 ID 映射关系
|
||||||
|
const newIdMap = new Map<number, string>();
|
||||||
|
newList.forEach((item, index) => {
|
||||||
|
newIdMap.set(index, item.id);
|
||||||
|
});
|
||||||
|
idMapRef.current = newIdMap;
|
||||||
|
|
||||||
|
// 转换回string[]格式
|
||||||
|
setValue(
|
||||||
|
'hot_search',
|
||||||
|
newList.map(item => item.text),
|
||||||
|
);
|
||||||
|
setIsEdit(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const HotSearchSortableItem = useMemo(
|
||||||
|
() => (props: any) => (
|
||||||
|
<SortableItem {...props} ItemComponent={HotSearchItem} />
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const ButtonSortableItem = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const callback = subscribe({
|
const callback = subscribe({
|
||||||
formState: {
|
formState: {
|
||||||
|
|
@ -59,6 +143,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
||||||
return () => {
|
return () => {
|
||||||
callback();
|
callback();
|
||||||
};
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [subscribe]);
|
}, [subscribe]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -132,60 +217,18 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
||||||
render={({ field }) => <TextField {...field} placeholder='请输入' />}
|
render={({ field }) => <TextField {...field} placeholder='请输入' />}
|
||||||
/>
|
/>
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
<CommonItem title='热门搜索'>
|
<CommonItem title='热门搜索' onAdd={handleAddHotSearch}>
|
||||||
<Controller
|
{hotSearchList.length === 0 ? (
|
||||||
control={control}
|
<Empty />
|
||||||
name='hot_search'
|
) : (
|
||||||
render={({ field }) => (
|
<DragList
|
||||||
<Autocomplete
|
data={hotSearchList}
|
||||||
{...field}
|
onChange={handleHotSearchChange}
|
||||||
value={field.value || []}
|
setIsEdit={setIsEdit}
|
||||||
multiple
|
SortableItemComponent={HotSearchSortableItem}
|
||||||
freeSolo
|
ItemComponent={HotSearchItem}
|
||||||
fullWidth
|
|
||||||
options={[]}
|
|
||||||
inputValue={inputValue}
|
|
||||||
onInputChange={(_, newInputValue) => setInputValue(newInputValue)}
|
|
||||||
onChange={(_, newValue) => {
|
|
||||||
setIsEdit(true);
|
|
||||||
const newValues = [...new Set(newValue as string[])];
|
|
||||||
field.onChange(newValues);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setIsEdit(true);
|
|
||||||
const trimmedValue = inputValue.trim();
|
|
||||||
if (trimmedValue && !field.value?.includes(trimmedValue)) {
|
|
||||||
field.onChange([...(field.value || []), trimmedValue]);
|
|
||||||
}
|
|
||||||
setInputValue('');
|
|
||||||
}}
|
|
||||||
renderValue={(value, getTagProps) => {
|
|
||||||
return value.map((option, index: number) => {
|
|
||||||
return (
|
|
||||||
<Chip
|
|
||||||
variant='outlined'
|
|
||||||
size='small'
|
|
||||||
label={
|
|
||||||
<Box sx={{ fontSize: '12px' }}>
|
|
||||||
{option as React.ReactNode}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
{...getTagProps({ index })}
|
|
||||||
key={index}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
renderInput={params => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder='回车确认,填写下一个热门搜索'
|
|
||||||
variant='outlined'
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
<CommonItem title='主按钮' onAdd={handleAddButton}>
|
<CommonItem title='主按钮' onAdd={handleAddButton}>
|
||||||
<DragList
|
<DragList
|
||||||
|
|
@ -195,6 +238,8 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
}}
|
}}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ButtonSortableItem}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
</StyledCommonWrapper>
|
</StyledCommonWrapper>
|
||||||
|
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item from './Item';
|
|
||||||
import { DomainRecommendNodeListResp } from '@/request/types';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: DomainRecommendNodeListResp[];
|
|
||||||
onChange: (data: DomainRecommendNodeListResp[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: DomainRecommendNodeListResp) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id!)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
// handleUpdateItem={handleUpdateItem}
|
|
||||||
// setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
// setIsEdit={setIsEdit}
|
|
||||||
// handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import FaqItem, { ItemProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemProps & {};
|
|
||||||
|
|
||||||
const FaqSortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id! });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FaqItem
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaqSortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import BasicDocDragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { Empty } from '@ctzhian/ui';
|
import { Empty } from '@ctzhian/ui';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
|
@ -40,6 +42,12 @@ const BasicDocConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -116,13 +124,15 @@ const BasicDocConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
{nodes.length === 0 ? (
|
{nodes.length === 0 ? (
|
||||||
<Empty />
|
<Empty />
|
||||||
) : (
|
) : (
|
||||||
<BasicDocDragList
|
<DragList
|
||||||
data={nodes}
|
data={nodes}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
setValue('nodes', value);
|
setValue('nodes', value);
|
||||||
}}
|
}}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item, { type ItemType } from './Item';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: ItemType[];
|
|
||||||
onChange: (data: ItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: ItemType) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import Item, { ItemProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemProps & {};
|
|
||||||
|
|
||||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import DragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
|
@ -36,6 +38,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -91,6 +99,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
data={list}
|
data={list}
|
||||||
onChange={handleListChange}
|
onChange={handleListChange}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item from './Item';
|
|
||||||
import FaqSortableItem from './SortableItem';
|
|
||||||
|
|
||||||
type ItemType = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
desc: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: ItemType[];
|
|
||||||
onChange: (data: ItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: ItemType) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<FaqSortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import FaqItem, { ItemProps } from './Item';
|
|
||||||
|
|
||||||
type FaqSortableItemProps = ItemProps & {};
|
|
||||||
|
|
||||||
const FaqSortableItem: FC<FaqSortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FaqItem
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaqSortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import DragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
|
@ -37,6 +39,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -121,6 +129,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
data={list}
|
data={list}
|
||||||
onChange={handleListChange}
|
onChange={handleListChange}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item, { type ItemType } from './Item';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: ItemType[];
|
|
||||||
onChange: (data: ItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: ItemType) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import DragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
|
@ -34,6 +36,12 @@ const CaseConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -90,6 +98,8 @@ const CaseConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
data={list}
|
data={list}
|
||||||
onChange={handleListChange}
|
onChange={handleListChange}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item, { type ItemType } from './Item';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: ItemType[];
|
|
||||||
onChange: (data: ItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: ItemType) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import FaqItem, { ItemTypeProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemTypeProps & {};
|
|
||||||
|
|
||||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FaqItem
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import DragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
|
@ -37,6 +39,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -93,6 +101,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
data={list}
|
data={list}
|
||||||
onChange={handleListChange}
|
onChange={handleListChange}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item from './Item';
|
|
||||||
import { DomainRecommendNodeListResp } from '@/request/types';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: DomainRecommendNodeListResp[];
|
|
||||||
onChange: (data: DomainRecommendNodeListResp[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: DomainRecommendNodeListResp) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id!)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
// handleUpdateItem={handleUpdateItem}
|
|
||||||
// setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
// setIsEdit={setIsEdit}
|
|
||||||
// handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import Item, { ItemProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemProps & {};
|
|
||||||
|
|
||||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id! });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import BasicDocDragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import { Empty } from '@ctzhian/ui';
|
import { Empty } from '@ctzhian/ui';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
|
@ -40,6 +42,12 @@ const DirDocConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
nodeRec(newList);
|
nodeRec(newList);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -117,13 +125,15 @@ const DirDocConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
{nodes.length === 0 ? (
|
{nodes.length === 0 ? (
|
||||||
<Empty />
|
<Empty />
|
||||||
) : (
|
) : (
|
||||||
<BasicDocDragList
|
<DragList
|
||||||
data={nodes}
|
data={nodes}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
setValue('nodes', value);
|
setValue('nodes', value);
|
||||||
}}
|
}}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import FaqItem from './FaqItem';
|
|
||||||
import FaqSortableItem from './FaqSortableItem';
|
|
||||||
|
|
||||||
type FaqItemType = {
|
|
||||||
id: string;
|
|
||||||
question: string;
|
|
||||||
link: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FaqDragListProps {
|
|
||||||
data: FaqItemType[];
|
|
||||||
onChange: (data: FaqItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FaqDragList: FC<FaqDragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: FaqItemType) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<FaqSortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<FaqItem
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaqDragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import FaqItem, { FaqItemProps } from './FaqItem';
|
|
||||||
|
|
||||||
type FaqSortableItemProps = FaqItemProps & {};
|
|
||||||
|
|
||||||
const FaqSortableItem: FC<FaqSortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FaqItem
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaqSortableItem;
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import FaqDragList from './FaqDragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import FaqItem from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
import { Empty } from '@ctzhian/ui';
|
import { Empty } from '@ctzhian/ui';
|
||||||
import { DEFAULT_DATA } from '../../../constants';
|
import { DEFAULT_DATA } from '../../../constants';
|
||||||
import ColorPickerField from '../../components/ColorPickerField';
|
|
||||||
import { findConfigById, handleLandingConfigs } from '../../../utils';
|
import { findConfigById, handleLandingConfigs } from '../../../utils';
|
||||||
|
|
||||||
const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
|
|
@ -35,6 +36,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const FaqSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={FaqItem} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -111,10 +118,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
{list.length === 0 ? (
|
{list.length === 0 ? (
|
||||||
<Empty />
|
<Empty />
|
||||||
) : (
|
) : (
|
||||||
<FaqDragList
|
<DragList
|
||||||
data={list}
|
data={list}
|
||||||
onChange={handleListChange}
|
onChange={handleListChange}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={FaqSortableComponent}
|
||||||
|
ItemComponent={FaqItem}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item, { type ItemType } from './Item';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: ItemType[];
|
|
||||||
onChange: (data: ItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: ItemType) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import Item, { ItemProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemProps & {};
|
|
||||||
|
|
||||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import DragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
|
@ -34,6 +36,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -89,6 +97,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
data={list}
|
data={list}
|
||||||
onChange={handleListChange}
|
onChange={handleListChange}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item, { type ItemType } from './Item';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: ItemType[];
|
|
||||||
onChange: (data: ItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: ItemType) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import FaqItem, { ItemTypeProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemTypeProps & {};
|
|
||||||
|
|
||||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FaqItem
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import DragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
|
@ -34,6 +36,12 @@ const MetricsConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -90,6 +98,8 @@ const MetricsConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
data={list}
|
data={list}
|
||||||
onChange={handleListChange}
|
onChange={handleListChange}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item, { ItemType } from './Item';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface FaqDragListProps {
|
|
||||||
data: ItemType[];
|
|
||||||
onChange: (data: ItemType[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FaqDragList: FC<FaqDragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: ItemType) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
setIsEdit={setIsEdit}
|
|
||||||
handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaqDragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import Item, { ItemProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemProps & {};
|
|
||||||
|
|
||||||
const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import FaqDragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
|
||||||
|
|
@ -36,6 +38,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -88,10 +96,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
{list.length === 0 ? (
|
{list.length === 0 ? (
|
||||||
<Empty />
|
<Empty />
|
||||||
) : (
|
) : (
|
||||||
<FaqDragList
|
<DragList
|
||||||
data={list}
|
data={list}
|
||||||
onChange={handleListChange}
|
onChange={handleListChange}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
import {
|
|
||||||
closestCenter,
|
|
||||||
DndContext,
|
|
||||||
DragEndEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
MouseSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
rectSortingStrategy,
|
|
||||||
SortableContext,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
import { Stack } from '@mui/material';
|
|
||||||
import { Dispatch, FC, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import Item from './Item';
|
|
||||||
import { DomainRecommendNodeListResp } from '@/request/types';
|
|
||||||
import SortableItem from './SortableItem';
|
|
||||||
|
|
||||||
interface DragListProps {
|
|
||||||
data: DomainRecommendNodeListResp[];
|
|
||||||
onChange: (data: DomainRecommendNodeListResp[]) => void;
|
|
||||||
setIsEdit: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragList: FC<DragListProps> = ({ data, onChange, setIsEdit }) => {
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
|
||||||
setActiveId(event.active.id as string);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
|
||||||
(event: DragEndEvent) => {
|
|
||||||
const { active, over } = event;
|
|
||||||
if (active.id !== over?.id) {
|
|
||||||
const oldIndex = data.findIndex(item => item.id === active.id);
|
|
||||||
const newIndex = data.findIndex(item => item.id === over!.id);
|
|
||||||
const newData = arrayMove(data, oldIndex, newIndex);
|
|
||||||
onChange(newData);
|
|
||||||
}
|
|
||||||
setActiveId(null);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragCancel = useCallback(() => {
|
|
||||||
setActiveId(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleRemove = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const newData = data.filter(item => item.id !== id);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateItem = useCallback(
|
|
||||||
(updatedItem: DomainRecommendNodeListResp) => {
|
|
||||||
const newData = data.map(item =>
|
|
||||||
item.id === updatedItem.id ? updatedItem : item,
|
|
||||||
);
|
|
||||||
onChange(newData);
|
|
||||||
},
|
|
||||||
[data, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext
|
|
||||||
sensors={sensors}
|
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragStart={handleDragStart}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
onDragCancel={handleDragCancel}
|
|
||||||
>
|
|
||||||
<SortableContext
|
|
||||||
items={data.map(item => item.id!)}
|
|
||||||
strategy={rectSortingStrategy}
|
|
||||||
>
|
|
||||||
<Stack direction={'row'} flexWrap={'wrap'} gap={2}>
|
|
||||||
{data.map(item => (
|
|
||||||
<SortableItem
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
item={item}
|
|
||||||
handleRemove={handleRemove}
|
|
||||||
// handleUpdateItem={handleUpdateItem}
|
|
||||||
// setIsEdit={setIsEdit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</SortableContext>
|
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: '0 0' }}>
|
|
||||||
{activeId ? (
|
|
||||||
<Item
|
|
||||||
isDragging
|
|
||||||
item={data.find(item => item.id === activeId)!}
|
|
||||||
// setIsEdit={setIsEdit}
|
|
||||||
// handleUpdateItem={handleUpdateItem}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</DragOverlay>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragList;
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import FaqItem, { ItemProps } from './Item';
|
|
||||||
|
|
||||||
type SortableItemProps = ItemProps & {};
|
|
||||||
|
|
||||||
const FaqSortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
|
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
} = useSortable({ id: item.id! });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition: transition || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FaqItem
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
withOpacity={isDragging}
|
|
||||||
dragHandleProps={{
|
|
||||||
...attributes,
|
|
||||||
...listeners,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FaqSortableItem;
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import DragList from './DragList';
|
import DragList from '../../components/DragList';
|
||||||
|
import SortableItem from '../../components/SortableItem';
|
||||||
|
import Item from './Item';
|
||||||
import { Empty } from '@ctzhian/ui';
|
import { Empty } from '@ctzhian/ui';
|
||||||
import type { ConfigProps } from '../type';
|
import type { ConfigProps } from '../type';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
|
@ -40,6 +42,12 @@ const SimpleDocConfigConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稳定的 SortableItemComponent 引用
|
||||||
|
const ItemSortableComponent = useMemo(
|
||||||
|
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(
|
reset(
|
||||||
findConfigById(
|
findConfigById(
|
||||||
|
|
@ -124,6 +132,8 @@ const SimpleDocConfigConfig = ({ setIsEdit, id }: ConfigProps) => {
|
||||||
setValue('nodes', value);
|
setValue('nodes', value);
|
||||||
}}
|
}}
|
||||||
setIsEdit={setIsEdit}
|
setIsEdit={setIsEdit}
|
||||||
|
SortableItemComponent={ItemSortableComponent}
|
||||||
|
ItemComponent={Item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CommonItem>
|
</CommonItem>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ const dark = {
|
||||||
text: {
|
text: {
|
||||||
primary: '#fff',
|
primary: '#fff',
|
||||||
secondary: 'rgba(255,255,255,0.7)',
|
secondary: 'rgba(255,255,255,0.7)',
|
||||||
auxiliary: 'rgba(255,255,255,0.5)',
|
tertiary: 'rgba(255,255,255,0.5)',
|
||||||
disabled: 'rgba(255,255,255,0.26)',
|
disabled: 'rgba(255,255,255,0.26)',
|
||||||
slave: 'rgba(255,255,255,0.05)',
|
slave: 'rgba(255,255,255,0.05)',
|
||||||
inverseAuxiliary: 'rgba(0,0,0,0.5)',
|
inverseAuxiliary: 'rgba(0,0,0,0.5)',
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ const light = {
|
||||||
text: {
|
text: {
|
||||||
primary: '#21222D',
|
primary: '#21222D',
|
||||||
secondary: 'rgba(33,34,35,0.7)',
|
secondary: 'rgba(33,34,35,0.7)',
|
||||||
auxiliary: 'rgba(33,34,35,0.5)',
|
tertiary: 'rgba(33,34,35,0.5)',
|
||||||
slave: 'rgba(33,34,35,0.3)',
|
slave: 'rgba(33,34,35,0.3)',
|
||||||
disabled: 'rgba(33,34,35,0.2)',
|
disabled: 'rgba(33,34,35,0.2)',
|
||||||
inverse: '#FFFFFF',
|
inverse: '#FFFFFF',
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
import RAG_SOURCES from '@/constant/rag';
|
import RAG_SOURCES from '@/constant/rag';
|
||||||
import { treeSx } from '@/constant/styles';
|
import { treeSx } from '@/constant/styles';
|
||||||
import { postApiV1Node, putApiV1NodeDetail } from '@/request/Node';
|
import { postApiV1Node, putApiV1NodeDetail } from '@/request/Node';
|
||||||
import { ConstsNodeAccessPerm, ConstsNodeRagInfoStatus } from '@/request/types';
|
import { ConstsNodeAccessPerm } from '@/request/types';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import { AppContext, updateTree } from '@/utils/drag';
|
import { AppContext, updateTree } from '@/utils/drag';
|
||||||
import { handleMultiSelect, updateAllParentStatus } from '@/utils/tree';
|
import { handleMultiSelect, updateAllParentStatus } from '@/utils/tree';
|
||||||
|
|
@ -522,12 +522,7 @@ const TreeItem = React.forwardRef<
|
||||||
gap={1}
|
gap={1}
|
||||||
sx={{ flexShrink: 0, fontSize: 12 }}
|
sx={{ flexShrink: 0, fontSize: 12 }}
|
||||||
>
|
>
|
||||||
{item.type === 2 &&
|
{item.type === 2 && item.rag_status && (
|
||||||
item.rag_status &&
|
|
||||||
![
|
|
||||||
ConstsNodeRagInfoStatus.NodeRagStatusBasicSucceeded,
|
|
||||||
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceSucceeded,
|
|
||||||
].includes(item.rag_status) && (
|
|
||||||
<Tooltip title={item.rag_message}>
|
<Tooltip title={item.rag_message}>
|
||||||
<StyledTag
|
<StyledTag
|
||||||
color={RAG_SOURCES[item.rag_status].color as any}
|
color={RAG_SOURCES[item.rag_status].color as any}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { useURLSearchParams } from '@/hooks';
|
||||||
import { ConstsUserRole } from '@/request/types';
|
import { ConstsUserRole } from '@/request/types';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store';
|
import { useAppDispatch, useAppSelector } from '@/store';
|
||||||
import { setKbC, setKbId } from '@/store/slices/config';
|
import { setKbC, setKbId } from '@/store/slices/config';
|
||||||
import custom from '@/themes/custom';
|
|
||||||
import { Ellipsis, Icon, message } from '@ctzhian/ui';
|
import { Ellipsis, Icon, message } from '@ctzhian/ui';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -117,7 +116,7 @@ const KBSelect = () => {
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
bgcolor: 'background.paper3',
|
bgcolor: 'background.paper3',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
bgcolor: custom.selectedMenuItemBgColor,
|
bgcolor: 'rgba(50,72,242,0.1)',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { Icon, message } from '@ctzhian/ui';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
InputAdornment,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import Visibility from '@mui/icons-material/Visibility';
|
||||||
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||||
|
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||||
|
|
||||||
|
export interface AutoModelConfigRef {
|
||||||
|
getFormData: () => {
|
||||||
|
apiKey: string;
|
||||||
|
selectedModel: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoModelConfigProps {
|
||||||
|
showTip?: boolean;
|
||||||
|
initialApiKey?: string;
|
||||||
|
initialChatModel?: string;
|
||||||
|
onDataChange?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
|
||||||
|
(props, ref) => {
|
||||||
|
const {
|
||||||
|
showTip = false,
|
||||||
|
initialApiKey = '',
|
||||||
|
initialChatModel = '',
|
||||||
|
onDataChange,
|
||||||
|
} = props;
|
||||||
|
const [autoConfigApiKey, setAutoConfigApiKey] = useState(initialApiKey);
|
||||||
|
const [selectedAutoChatModel, setSelectedAutoChatModel] =
|
||||||
|
useState(initialChatModel);
|
||||||
|
const [showApiKey, setShowApiKey] = useState(false);
|
||||||
|
|
||||||
|
// 默认百智云 Chat 模型列表
|
||||||
|
const DEFAULT_BAIZHI_CLOUD_CHAT_MODELS: string[] = [
|
||||||
|
'deepseek-chat',
|
||||||
|
'deepseek-r1',
|
||||||
|
'kimi-k2-0711-preview',
|
||||||
|
'qwen-vl-max-latest',
|
||||||
|
'glm-4.5',
|
||||||
|
];
|
||||||
|
|
||||||
|
const modelList = DEFAULT_BAIZHI_CLOUD_CHAT_MODELS;
|
||||||
|
|
||||||
|
// 当从父组件接收到新的初始值时,更新状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialApiKey) {
|
||||||
|
setAutoConfigApiKey(initialApiKey);
|
||||||
|
}
|
||||||
|
}, [initialApiKey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialChatModel) {
|
||||||
|
setSelectedAutoChatModel(initialChatModel);
|
||||||
|
}
|
||||||
|
}, [initialChatModel]);
|
||||||
|
|
||||||
|
// 如果没有选中模型且有可用模型,默认选择第一个
|
||||||
|
useEffect(() => {
|
||||||
|
if (modelList.length && !selectedAutoChatModel) {
|
||||||
|
setSelectedAutoChatModel(modelList[0]);
|
||||||
|
}
|
||||||
|
}, [modelList, selectedAutoChatModel]);
|
||||||
|
|
||||||
|
// 暴露给父组件的方法
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getFormData: () => ({
|
||||||
|
apiKey: autoConfigApiKey,
|
||||||
|
selectedModel: selectedAutoChatModel,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
p: 2,
|
||||||
|
pl: 2,
|
||||||
|
pr: 0,
|
||||||
|
pt: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
overflowY: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
{/* 提示信息 */}
|
||||||
|
{showTip && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 1,
|
||||||
|
p: 1.5,
|
||||||
|
mb: 2,
|
||||||
|
bgcolor: 'rgba(25, 118, 210, 0.08)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(25, 118, 210, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
type='icon-info-circle'
|
||||||
|
sx={{
|
||||||
|
fontSize: 16,
|
||||||
|
color: 'primary.main',
|
||||||
|
flexShrink: 0,
|
||||||
|
mt: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: 'text.secondary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
通过 API Key 连接百智云提供平台后,PandaWiki
|
||||||
|
会自动配置好系统所需的问答模型、向量模型、重排序模型、文档分析模型。充分利用平台配置,无需逐个手动配置。
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
mb: '16px',
|
||||||
|
pt: '32px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 4,
|
||||||
|
height: 10,
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
borderRadius: '30%',
|
||||||
|
mr: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
API Key
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component='a'
|
||||||
|
href='https://model-square.app.baizhi.cloud/token'
|
||||||
|
target='_blank'
|
||||||
|
sx={{
|
||||||
|
color: 'primary.main',
|
||||||
|
fontSize: 12,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type='icon-key' sx={{ fontSize: 14 }} />
|
||||||
|
获取百智云 API Key
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size='medium'
|
||||||
|
type={showApiKey ? 'text' : 'password'}
|
||||||
|
value={autoConfigApiKey}
|
||||||
|
onChange={e => {
|
||||||
|
setAutoConfigApiKey(e.target.value);
|
||||||
|
onDataChange?.();
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position='end'>
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={() => setShowApiKey(s => !s)}
|
||||||
|
>
|
||||||
|
{showApiKey ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
'& .MuiInputBase-root': {
|
||||||
|
borderRadius: '10px',
|
||||||
|
height: '52px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{!showTip && (
|
||||||
|
<Box sx={{ mt: 0 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'text.primary',
|
||||||
|
mb: '16px',
|
||||||
|
pt: '32px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 4,
|
||||||
|
height: 10,
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
borderRadius: '30%',
|
||||||
|
mr: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
模型选择
|
||||||
|
</Box>
|
||||||
|
<Stack direction='row' alignItems='center' gap={2}>
|
||||||
|
<Box sx={{ fontSize: 12, color: 'text.secondary', minWidth: 80 }}>
|
||||||
|
对话模型
|
||||||
|
</Box>
|
||||||
|
<Select
|
||||||
|
size='medium'
|
||||||
|
displayEmpty
|
||||||
|
value={selectedAutoChatModel}
|
||||||
|
onChange={e => {
|
||||||
|
setSelectedAutoChatModel(e.target.value as string);
|
||||||
|
onDataChange?.();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: 300,
|
||||||
|
height: '52px',
|
||||||
|
'& .MuiInputBase-root': {
|
||||||
|
borderRadius: '10px',
|
||||||
|
bgcolor: '#F8F8FA',
|
||||||
|
height: '52px',
|
||||||
|
},
|
||||||
|
'& .MuiSelect-select': {
|
||||||
|
height: '52px',
|
||||||
|
lineHeight: '52px',
|
||||||
|
padding: '0 14px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
renderValue={sel =>
|
||||||
|
sel && (sel as string).length
|
||||||
|
? (sel as string)
|
||||||
|
: '请选择聊天模型'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{modelList.map((model: string) => (
|
||||||
|
<MenuItem key={model} value={model}>
|
||||||
|
{model}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AutoModelConfig;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -83,7 +83,7 @@ export default function ContributePreviewModal(
|
||||||
? '新增'
|
? '新增'
|
||||||
: '修改'}
|
: '修改'}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ fontSize: 14, color: 'text.auxiliary', fontWeight: 400 }}>
|
<Box sx={{ fontSize: 14, color: 'text.tertiary', fontWeight: 400 }}>
|
||||||
{dayjs(row?.created_at).fromNow()}
|
{dayjs(row?.created_at).fromNow()}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ const MarkdownPreviewModal = ({
|
||||||
? '新增'
|
? '新增'
|
||||||
: '修改'}
|
: '修改'}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ fontSize: 14, color: 'text.auxiliary', fontWeight: 400 }}>
|
<Box sx={{ fontSize: 14, color: 'text.tertiary', fontWeight: 400 }}>
|
||||||
{dayjs(row?.created_at).fromNow()}
|
{dayjs(row?.created_at).fromNow()}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { ITreeItem } from '@/api';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import DragTree from '@/components/Drag/DragTree';
|
||||||
|
import { postApiV1NodeRestudy } from '@/request';
|
||||||
|
import { getApiV1NodeList } from '@/request/Node';
|
||||||
|
import {
|
||||||
|
ConstsNodeRagInfoStatus,
|
||||||
|
DomainNodeListItemResp,
|
||||||
|
} from '@/request/types';
|
||||||
|
import { useAppSelector } from '@/store';
|
||||||
|
import { convertToTree } from '@/utils/drag';
|
||||||
|
import { message, Modal } from '@ctzhian/ui';
|
||||||
|
import { Box, Checkbox, Stack } from '@mui/material';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
interface RagErrorReStartProps {
|
||||||
|
open: boolean;
|
||||||
|
defaultSelected?: string[];
|
||||||
|
onClose: () => void;
|
||||||
|
refresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RagErrorReStart = ({
|
||||||
|
open,
|
||||||
|
defaultSelected = [],
|
||||||
|
onClose,
|
||||||
|
refresh,
|
||||||
|
}: RagErrorReStartProps) => {
|
||||||
|
const { kb_id } = useAppSelector(state => state.config);
|
||||||
|
|
||||||
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
|
const [treeList, setTreeList] = useState<ITreeItem[]>([]);
|
||||||
|
const [list, setList] = useState<DomainNodeListItemResp[]>([]);
|
||||||
|
|
||||||
|
const getData = () => {
|
||||||
|
getApiV1NodeList({ kb_id }).then(res => {
|
||||||
|
const ragErrorData =
|
||||||
|
res?.filter(
|
||||||
|
item =>
|
||||||
|
item.rag_info?.status &&
|
||||||
|
[
|
||||||
|
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
|
||||||
|
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
|
||||||
|
].includes(item.rag_info.status),
|
||||||
|
) || [];
|
||||||
|
setList(ragErrorData);
|
||||||
|
setSelected(
|
||||||
|
defaultSelected.length > 0
|
||||||
|
? defaultSelected
|
||||||
|
: ragErrorData.map(it => it.id!),
|
||||||
|
);
|
||||||
|
const showTreeData = convertToTree(ragErrorData || []);
|
||||||
|
setTreeList(showTreeData);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
if (selected.length > 0) {
|
||||||
|
postApiV1NodeRestudy({
|
||||||
|
kb_id,
|
||||||
|
node_ids: [...selected],
|
||||||
|
}).then(() => {
|
||||||
|
message.success('正在重新学习');
|
||||||
|
setSelected([]);
|
||||||
|
onClose();
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(
|
||||||
|
list.length > 0 ? '请选择要重新学习的文档' : '暂无学习失败的文档',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
}, [open, kb_id]);
|
||||||
|
|
||||||
|
const selectedTotal = useMemo(() => {
|
||||||
|
return list.filter(item => selected.includes(item.id!)).length;
|
||||||
|
}, [selected, list]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title='重新学习' open={open} onCancel={onClose} onOk={onSubmit}>
|
||||||
|
<Stack
|
||||||
|
direction='row'
|
||||||
|
component='label'
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'space-between'}
|
||||||
|
gap={1}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: '10px',
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
学习失败文档
|
||||||
|
<Box
|
||||||
|
component='span'
|
||||||
|
sx={{ color: 'text.tertiary', fontSize: 12, pl: 1 }}
|
||||||
|
>
|
||||||
|
共 {list.length} 个,已选中 {selectedTotal} 个
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Stack direction='row' alignItems={'center'}>
|
||||||
|
<Box sx={{ color: 'text.tertiary', fontSize: 12 }}>全选</Box>
|
||||||
|
<Checkbox
|
||||||
|
size='small'
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
color: 'text.disabled',
|
||||||
|
width: '35px',
|
||||||
|
height: '35px',
|
||||||
|
}}
|
||||||
|
checked={selectedTotal === list.length}
|
||||||
|
onChange={() => {
|
||||||
|
setSelected(
|
||||||
|
selectedTotal === list.length ? [] : list.map(item => item.id!),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Card sx={{ bgcolor: 'background.paper3', py: 1 }}>
|
||||||
|
<Stack
|
||||||
|
gap={0.25}
|
||||||
|
sx={{
|
||||||
|
fontSize: 14,
|
||||||
|
maxHeight: 'calc(100vh - 520px)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
px: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DragTree
|
||||||
|
ui='select'
|
||||||
|
readOnly
|
||||||
|
selected={selected}
|
||||||
|
data={treeList}
|
||||||
|
refresh={getData}
|
||||||
|
onSelectChange={ids => setSelected(ids)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RagErrorReStart;
|
||||||
|
|
@ -8,7 +8,11 @@ import {
|
||||||
} from '@/components/Drag/DragTree/TreeMenu';
|
} from '@/components/Drag/DragTree/TreeMenu';
|
||||||
import { useURLSearchParams } from '@/hooks';
|
import { useURLSearchParams } from '@/hooks';
|
||||||
import { getApiV1NodeList } from '@/request/Node';
|
import { getApiV1NodeList } from '@/request/Node';
|
||||||
import { ConstsCrawlerSource, DomainNodeListItemResp } from '@/request/types';
|
import {
|
||||||
|
ConstsCrawlerSource,
|
||||||
|
ConstsNodeRagInfoStatus,
|
||||||
|
DomainNodeListItemResp,
|
||||||
|
} from '@/request/types';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store';
|
import { useAppDispatch, useAppSelector } from '@/store';
|
||||||
import { setIsRefreshDocList } from '@/store/slices/config';
|
import { setIsRefreshDocList } from '@/store/slices/config';
|
||||||
import { addOpacityToColor } from '@/utils';
|
import { addOpacityToColor } from '@/utils';
|
||||||
|
|
@ -32,6 +36,7 @@ import DocSearch from './component/DocSearch';
|
||||||
import DocStatus from './component/DocStatus';
|
import DocStatus from './component/DocStatus';
|
||||||
import DocSummary from './component/DocSummary';
|
import DocSummary from './component/DocSummary';
|
||||||
import MoveDocs from './component/MoveDocs';
|
import MoveDocs from './component/MoveDocs';
|
||||||
|
import RagErrorReStart from './component/RagErrorReStart';
|
||||||
import Summary from './component/Summary';
|
import Summary from './component/Summary';
|
||||||
|
|
||||||
const Content = () => {
|
const Content = () => {
|
||||||
|
|
@ -44,10 +49,15 @@ const Content = () => {
|
||||||
const search = searchParams.get('search') || '';
|
const search = searchParams.get('search') || '';
|
||||||
const [supportSelect, setBatchOpen] = useState(false);
|
const [supportSelect, setBatchOpen] = useState(false);
|
||||||
|
|
||||||
|
const [ragErrorCount, setRagErrorCount] = useState(0);
|
||||||
|
const [ragErrorIds, setRagErrorIds] = useState<string[]>([]);
|
||||||
|
const [ragErrorOpen, setRagErrorOpen] = useState(false);
|
||||||
const [publish, setPublish] = useState({
|
const [publish, setPublish] = useState({
|
||||||
// published: 0,
|
// published: 0,
|
||||||
unpublished: 0,
|
unpublished: 0,
|
||||||
});
|
});
|
||||||
|
const [publishIds, setPublishIds] = useState<string[]>([]);
|
||||||
|
const [publishOpen, setPublishOpen] = useState(false);
|
||||||
const [list, setList] = useState<DomainNodeListItemResp[]>([]);
|
const [list, setList] = useState<DomainNodeListItemResp[]>([]);
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
const [data, setData] = useState<ITreeItem[]>([]);
|
const [data, setData] = useState<ITreeItem[]>([]);
|
||||||
|
|
@ -58,8 +68,6 @@ const Content = () => {
|
||||||
const [moreSummaryOpen, setMoreSummaryOpen] = useState(false);
|
const [moreSummaryOpen, setMoreSummaryOpen] = useState(false);
|
||||||
const [moveOpen, setMoveOpen] = useState(false);
|
const [moveOpen, setMoveOpen] = useState(false);
|
||||||
const [urlOpen, setUrlOpen] = useState(false);
|
const [urlOpen, setUrlOpen] = useState(false);
|
||||||
const [publishIds, setPublishIds] = useState<string[]>([]);
|
|
||||||
const [publishOpen, setPublishOpen] = useState(false);
|
|
||||||
const [key, setKey] = useState<ConstsCrawlerSource | null>(null);
|
const [key, setKey] = useState<ConstsCrawlerSource | null>(null);
|
||||||
const [propertiesOpen, setPropertiesOpen] = useState(false);
|
const [propertiesOpen, setPropertiesOpen] = useState(false);
|
||||||
const [isBatch, setIsBatch] = useState(false);
|
const [isBatch, setIsBatch] = useState(false);
|
||||||
|
|
@ -119,6 +127,11 @@ const Content = () => {
|
||||||
setPublishIds([item.id]);
|
setPublishIds([item.id]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRestudy = (item: ITreeItem) => {
|
||||||
|
setRagErrorOpen(true);
|
||||||
|
setRagErrorIds([item.id]);
|
||||||
|
};
|
||||||
|
|
||||||
const handleProperties = (item: ITreeItem) => {
|
const handleProperties = (item: ITreeItem) => {
|
||||||
setPropertiesOpen(true);
|
setPropertiesOpen(true);
|
||||||
setOpraData(getOperationData(item));
|
setOpraData(getOperationData(item));
|
||||||
|
|
@ -252,6 +265,19 @@ const Content = () => {
|
||||||
// },
|
// },
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(item?.rag_status &&
|
||||||
|
[
|
||||||
|
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
|
||||||
|
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
|
||||||
|
].includes(item.rag_status)
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: '重新学习',
|
||||||
|
key: 'restudy',
|
||||||
|
onClick: () => handleRestudy(item),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
...(!isEditing
|
...(!isEditing
|
||||||
? [{ label: '重命名', key: 'rename', onClick: renameItem }]
|
? [{ label: '重命名', key: 'rename', onClick: renameItem }]
|
||||||
: []),
|
: []),
|
||||||
|
|
@ -308,8 +334,17 @@ const Content = () => {
|
||||||
setList(res || []);
|
setList(res || []);
|
||||||
setPublish({
|
setPublish({
|
||||||
unpublished: res.filter(it => it.status === 1).length,
|
unpublished: res.filter(it => it.status === 1).length,
|
||||||
// published: res.filter(it => it.status === 2).length,
|
|
||||||
});
|
});
|
||||||
|
setRagErrorCount(
|
||||||
|
res.filter(
|
||||||
|
it =>
|
||||||
|
it.rag_info?.status &&
|
||||||
|
[
|
||||||
|
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
|
||||||
|
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
|
||||||
|
].includes(it.rag_info.status),
|
||||||
|
).length,
|
||||||
|
);
|
||||||
const collapsedAll = collapseAllFolders(convertToTree(res || []), true);
|
const collapsedAll = collapseAllFolders(convertToTree(res || []), true);
|
||||||
const next = openIds.size
|
const next = openIds.size
|
||||||
? reopenFolders(collapsedAll, openIds)
|
? reopenFolders(collapsedAll, openIds)
|
||||||
|
|
@ -386,6 +421,29 @@ const Content = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{ragErrorCount > 0 && (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
color: 'error.main',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
ml: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ragErrorCount} 个文档学习失败,
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
||||||
|
onClick={() => {
|
||||||
|
setRagErrorOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重新学习
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction={'row'} alignItems={'center'} gap={2}>
|
<Stack direction={'row'} alignItems={'center'} gap={2}>
|
||||||
<DocSearch />
|
<DocSearch />
|
||||||
|
|
@ -670,6 +728,15 @@ const Content = () => {
|
||||||
}}
|
}}
|
||||||
refresh={getData}
|
refresh={getData}
|
||||||
/>
|
/>
|
||||||
|
<RagErrorReStart
|
||||||
|
open={ragErrorOpen}
|
||||||
|
defaultSelected={ragErrorIds}
|
||||||
|
onClose={() => {
|
||||||
|
setRagErrorOpen(false);
|
||||||
|
setRagErrorIds([]);
|
||||||
|
}}
|
||||||
|
refresh={getData}
|
||||||
|
/>
|
||||||
<MoveDocs
|
<MoveDocs
|
||||||
open={moveOpen}
|
open={moveOpen}
|
||||||
data={list}
|
data={list}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,11 @@ import {
|
||||||
DomainCreateModelReq,
|
DomainCreateModelReq,
|
||||||
DomainGetProviderModelListReq,
|
DomainGetProviderModelListReq,
|
||||||
DomainGetProviderModelListResp,
|
DomainGetProviderModelListResp,
|
||||||
|
DomainModelModeSetting,
|
||||||
DomainPWResponse,
|
DomainPWResponse,
|
||||||
DomainResponse,
|
DomainResponse,
|
||||||
|
DomainSwitchModeReq,
|
||||||
|
DomainSwitchModeResp,
|
||||||
DomainUpdateModelReq,
|
DomainUpdateModelReq,
|
||||||
GithubComChaitinPandaWikiDomainCheckModelReq,
|
GithubComChaitinPandaWikiDomainCheckModelReq,
|
||||||
GithubComChaitinPandaWikiDomainCheckModelResp,
|
GithubComChaitinPandaWikiDomainCheckModelResp,
|
||||||
|
|
@ -124,6 +127,32 @@ export const getApiV1ModelList = (params: RequestParams = {}) =>
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get current model mode setting including mode, API key and chat model
|
||||||
|
*
|
||||||
|
* @tags model
|
||||||
|
* @name GetApiV1ModelModeSetting
|
||||||
|
* @summary get model mode setting
|
||||||
|
* @request GET:/api/v1/model/mode-setting
|
||||||
|
* @response `200` `(DomainResponse & {
|
||||||
|
data?: DomainModelModeSetting,
|
||||||
|
|
||||||
|
})` OK
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const getApiV1ModelModeSetting = (params: RequestParams = {}) =>
|
||||||
|
httpRequest<
|
||||||
|
DomainResponse & {
|
||||||
|
data?: DomainModelModeSetting;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/api/v1/model/mode-setting`,
|
||||||
|
method: "GET",
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get provider supported model list
|
* @description get provider supported model list
|
||||||
*
|
*
|
||||||
|
|
@ -153,3 +182,33 @@ export const postApiV1ModelProviderSupported = (
|
||||||
format: "json",
|
format: "json",
|
||||||
...requestParams,
|
...requestParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description switch model mode between manual and auto
|
||||||
|
*
|
||||||
|
* @tags model
|
||||||
|
* @name PostApiV1ModelSwitchMode
|
||||||
|
* @summary switch mode
|
||||||
|
* @request POST:/api/v1/model/switch-mode
|
||||||
|
* @response `200` `(DomainResponse & {
|
||||||
|
data?: DomainSwitchModeResp,
|
||||||
|
|
||||||
|
})` OK
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const postApiV1ModelSwitchMode = (
|
||||||
|
request: DomainSwitchModeReq,
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
httpRequest<
|
||||||
|
DomainResponse & {
|
||||||
|
data?: DomainSwitchModeResp;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/api/v1/model/switch-mode`,
|
||||||
|
method: "POST",
|
||||||
|
body: request,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||||
|
* ## ##
|
||||||
|
* ## AUTHOR: acacode ##
|
||||||
|
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||||
|
import { DomainResponse, V1NodeRestudyReq, V1NodeRestudyResp } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 文档重新学习
|
||||||
|
*
|
||||||
|
* @tags NodeRestudy
|
||||||
|
* @name PostApiV1NodeRestudy
|
||||||
|
* @summary 文档重新学习
|
||||||
|
* @request POST:/api/v1/node/restudy
|
||||||
|
* @secure
|
||||||
|
* @response `200` `(DomainResponse & {
|
||||||
|
data?: V1NodeRestudyResp,
|
||||||
|
|
||||||
|
})` OK
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const postApiV1NodeRestudy = (
|
||||||
|
param: V1NodeRestudyReq,
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
httpRequest<
|
||||||
|
DomainResponse & {
|
||||||
|
data?: V1NodeRestudyResp;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/api/v1/node/restudy`,
|
||||||
|
method: "POST",
|
||||||
|
body: param,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
@ -10,6 +10,7 @@ export * from './Message'
|
||||||
export * from './Model'
|
export * from './Model'
|
||||||
export * from './Node'
|
export * from './Node'
|
||||||
export * from './NodePermission'
|
export * from './NodePermission'
|
||||||
|
export * from './NodeRestudy'
|
||||||
export * from './Stat'
|
export * from './Stat'
|
||||||
export * from './User'
|
export * from './User'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
|
||||||
|
|
@ -922,6 +922,15 @@ export interface DomainMetricsConfig {
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DomainModelModeSetting {
|
||||||
|
/** 百智云 API Key */
|
||||||
|
auto_mode_api_key?: string;
|
||||||
|
/** 自定义对话模型名称 */
|
||||||
|
chat_model?: string;
|
||||||
|
/** 模式: manual 或 auto */
|
||||||
|
mode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DomainMoveNodeReq {
|
export interface DomainMoveNodeReq {
|
||||||
id: string;
|
id: string;
|
||||||
kb_id: string;
|
kb_id: string;
|
||||||
|
|
@ -1195,6 +1204,18 @@ export interface DomainStatPageReq {
|
||||||
scene: 1 | 2 | 3 | 4;
|
scene: 1 | 2 | 3 | 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DomainSwitchModeReq {
|
||||||
|
/** 百智云 API Key */
|
||||||
|
auto_mode_api_key?: string;
|
||||||
|
/** 自定义对话模型名称 */
|
||||||
|
chat_model?: string;
|
||||||
|
mode: "manual" | "auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainSwitchModeResp {
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DomainTextConfig {
|
export interface DomainTextConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
@ -1623,12 +1644,39 @@ export interface V1NodePermissionResp {
|
||||||
visitable_groups?: DomainNodeGroupDetail[];
|
visitable_groups?: DomainNodeGroupDetail[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface V1NodeRestudyReq {
|
||||||
|
kb_id?: string;
|
||||||
|
node_ids?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type V1NodeRestudyResp = Record<string, any>;
|
||||||
|
|
||||||
export interface V1ResetPasswordReq {
|
export interface V1ResetPasswordReq {
|
||||||
id: string;
|
id: string;
|
||||||
/** @minLength 8 */
|
/** @minLength 8 */
|
||||||
new_password: string;
|
new_password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface V1ShareNodeDetailResp {
|
||||||
|
content?: string;
|
||||||
|
created_at?: string;
|
||||||
|
creator_account?: string;
|
||||||
|
creator_id?: string;
|
||||||
|
editor_account?: string;
|
||||||
|
editor_id?: string;
|
||||||
|
id?: string;
|
||||||
|
kb_id?: string;
|
||||||
|
meta?: DomainNodeMeta;
|
||||||
|
name?: string;
|
||||||
|
parent_id?: string;
|
||||||
|
permissions?: DomainNodePermissions;
|
||||||
|
publisher_account?: string;
|
||||||
|
publisher_id?: string;
|
||||||
|
status?: DomainNodeStatus;
|
||||||
|
type?: DomainNodeType;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface V1StatCountResp {
|
export interface V1StatCountResp {
|
||||||
conversation_count?: number;
|
conversation_count?: number;
|
||||||
ip_count?: number;
|
ip_count?: number;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import custom from './custom';
|
|
||||||
import dark from './dark';
|
|
||||||
import light from './light';
|
|
||||||
|
|
||||||
export { custom, dark, light };
|
|
||||||
export type ThemeColor = typeof light;
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export default {
|
|
||||||
selectPopupBoxShadow: '0px 10px 20px 0px rgba(54,59,76,0.2)',
|
|
||||||
selectedMenuItemBgColor: 'rgba(50,72,242,0.1)',
|
|
||||||
};
|
|
||||||
|
|
@ -51,7 +51,7 @@ const dark = {
|
||||||
text: {
|
text: {
|
||||||
primary: '#fff',
|
primary: '#fff',
|
||||||
secondary: 'rgba(255,255,255,0.7)',
|
secondary: 'rgba(255,255,255,0.7)',
|
||||||
auxiliary: 'rgba(255,255,255,0.5)',
|
tertiary: 'rgba(255,255,255,0.5)',
|
||||||
disabled: 'rgba(255,255,255,0.26)',
|
disabled: 'rgba(255,255,255,0.26)',
|
||||||
slave: 'rgba(255,255,255,0.05)',
|
slave: 'rgba(255,255,255,0.05)',
|
||||||
inverseAuxiliary: 'rgba(0,0,0,0.5)',
|
inverseAuxiliary: 'rgba(0,0,0,0.5)',
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ const light = {
|
||||||
text: {
|
text: {
|
||||||
primary: '#21222D',
|
primary: '#21222D',
|
||||||
secondary: 'rgba(33,34,35,0.7)',
|
secondary: 'rgba(33,34,35,0.7)',
|
||||||
auxiliary: 'rgba(33,34,35,0.5)',
|
tertiary: '#646a73',
|
||||||
slave: 'rgba(33,34,35,0.3)',
|
slave: 'rgba(33,34,35,0.3)',
|
||||||
disabled: 'rgba(33,34,35,0.2)',
|
disabled: 'rgba(33,34,35,0.2)',
|
||||||
inverse: '#FFFFFF',
|
inverse: '#FFFFFF',
|
||||||
|
|
|
||||||
|
|
@ -1,460 +0,0 @@
|
||||||
import { addOpacityToColor } from '@/utils';
|
|
||||||
import { custom, ThemeColor } from './color';
|
|
||||||
|
|
||||||
declare module '@mui/material/styles' {}
|
|
||||||
|
|
||||||
declare module '@mui/material/styles' {
|
|
||||||
interface TypeBackground {
|
|
||||||
paper0?: string;
|
|
||||||
paper2?: string;
|
|
||||||
chip?: string;
|
|
||||||
circle?: string;
|
|
||||||
hover?: string;
|
|
||||||
focus?: string;
|
|
||||||
disabled?: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentStyleOverrides = (theme: ThemeColor) => {
|
|
||||||
return {
|
|
||||||
MuiTabs: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: '10px !important',
|
|
||||||
overflow: 'hidden',
|
|
||||||
minHeight: '36px',
|
|
||||||
height: '36px',
|
|
||||||
padding: '0px !important',
|
|
||||||
},
|
|
||||||
indicator: {
|
|
||||||
borderRadius: '0px !important',
|
|
||||||
overflow: 'hidden',
|
|
||||||
backgroundColor: '#21222D !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiTab: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: '0px !important',
|
|
||||||
fontWeight: 'normal',
|
|
||||||
fontSize: '14px !important',
|
|
||||||
lineHeight: '34px',
|
|
||||||
padding: '0 16px !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiFormLabel: {
|
|
||||||
styleOverrides: {
|
|
||||||
asterisk: {
|
|
||||||
color: theme.error.main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiButton: {
|
|
||||||
styleOverrides: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
root: ({ ownerState }: { ownerState: any }) => {
|
|
||||||
return {
|
|
||||||
height: '36px',
|
|
||||||
fontSize: 14,
|
|
||||||
lineHeight: '36px',
|
|
||||||
paddingLeft: '16px',
|
|
||||||
paddingRight: '16px',
|
|
||||||
boxShadow: 'none',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
borderRadius: '10px',
|
|
||||||
fontWeight: '400',
|
|
||||||
...(ownerState.variant === 'contained' && {
|
|
||||||
color: theme.text.inverse,
|
|
||||||
backgroundColor: theme.text.primary,
|
|
||||||
}),
|
|
||||||
...(ownerState.variant === 'text' && {}),
|
|
||||||
...(ownerState.variant === 'outlined' && {
|
|
||||||
color: theme.text.primary,
|
|
||||||
border: `1px solid ${theme.text.primary}`,
|
|
||||||
}),
|
|
||||||
...(ownerState.disabled === true && {
|
|
||||||
cursor: 'not-allowed !important',
|
|
||||||
}),
|
|
||||||
...(ownerState.size === 'small' && {
|
|
||||||
height: '32px',
|
|
||||||
lineHeight: '32px',
|
|
||||||
}),
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: 'none',
|
|
||||||
...(ownerState.variant === 'contained' && {
|
|
||||||
backgroundColor: addOpacityToColor(theme.text.primary, 0.9),
|
|
||||||
}),
|
|
||||||
...(ownerState.variant === 'text' && {
|
|
||||||
backgroundColor: theme.background.paper3,
|
|
||||||
}),
|
|
||||||
...(ownerState.variant === 'outlined' && {
|
|
||||||
backgroundColor: theme.background.paper3,
|
|
||||||
}),
|
|
||||||
...(ownerState.color === 'neutral' && {
|
|
||||||
color: theme.text.primary,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
startIcon: {
|
|
||||||
marginLeft: 0,
|
|
||||||
marginRight: 8,
|
|
||||||
'>*:nth-of-type(1)': {
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiTooltip: {
|
|
||||||
styleOverrides: {
|
|
||||||
tooltip: {
|
|
||||||
borderRadius: '10px',
|
|
||||||
maxWidth: '600px',
|
|
||||||
padding: '8px 16px',
|
|
||||||
backgroundColor: theme.text.primary,
|
|
||||||
fontSize: '12px',
|
|
||||||
lineHeight: '20px',
|
|
||||||
color: theme.primary.contrastText,
|
|
||||||
},
|
|
||||||
arrow: {
|
|
||||||
color: theme.text.primary,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiFormHelperText: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
color: theme.error.main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiFormControl: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
'.MuiFormLabel-asterisk': {
|
|
||||||
color: theme.error.main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiFormControlLabel: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
marginLeft: '0 !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiTableBody: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
'.MuiTableRow-root:hover': {
|
|
||||||
'.MuiTableCell-root:not(.cx-table-empty-td)': {
|
|
||||||
backgroundColor: theme.table.row.hoverColor,
|
|
||||||
overflowX: 'hidden',
|
|
||||||
'.primary-color': {
|
|
||||||
color: theme.primary.main,
|
|
||||||
},
|
|
||||||
'.no-title-url': {
|
|
||||||
color: `${theme.primary.main} !important`,
|
|
||||||
},
|
|
||||||
'.error-color': {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiCheckbox: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
padding: 0,
|
|
||||||
svg: {
|
|
||||||
fontSize: '18px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiTableCell: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
background: theme.background.paper,
|
|
||||||
lineHeight: 1.5,
|
|
||||||
height: theme.table.cell.height,
|
|
||||||
fontSize: '14px',
|
|
||||||
paddingTop: '16px !important',
|
|
||||||
paddingBottom: '16px !important',
|
|
||||||
paddingLeft: 0,
|
|
||||||
'&:first-of-type': {
|
|
||||||
paddingLeft: '0px',
|
|
||||||
},
|
|
||||||
'&:not(:first-of-type)': {
|
|
||||||
paddingLeft: '0px',
|
|
||||||
},
|
|
||||||
'.MuiCheckbox-root': {
|
|
||||||
color: '#CCCCCC',
|
|
||||||
svg: {
|
|
||||||
fontSize: '16px',
|
|
||||||
},
|
|
||||||
'&.Mui-checked': {
|
|
||||||
color: theme.text.primary,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
head: {
|
|
||||||
backgroundColor: theme.background.paper3,
|
|
||||||
color: theme.table.head.color,
|
|
||||||
fontSize: '12px',
|
|
||||||
height: theme.table.head.height,
|
|
||||||
paddingTop: '0 !important',
|
|
||||||
paddingBottom: '0 !important',
|
|
||||||
borderSpacing: '12px',
|
|
||||||
zIndex: 100,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
borderBottom: '1px dashed',
|
|
||||||
borderColor: theme.table.cell.borderColor,
|
|
||||||
borderSpacing: '12px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiPopover: {
|
|
||||||
styleOverrides: {
|
|
||||||
paper: {
|
|
||||||
borderRadius: '10px',
|
|
||||||
boxShadow: custom.selectPopupBoxShadow,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiMenu: {
|
|
||||||
styleOverrides: {
|
|
||||||
paper: {
|
|
||||||
padding: '4px',
|
|
||||||
borderRadius: '10px',
|
|
||||||
backgroundColor: theme.background.paper,
|
|
||||||
boxShadow: custom.selectPopupBoxShadow,
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
paddingTop: '0px !important',
|
|
||||||
paddingBottom: '0px !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultProps: {
|
|
||||||
elevation: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiMenuItem: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
height: '40px',
|
|
||||||
borderRadius: '5px',
|
|
||||||
':hover': {
|
|
||||||
backgroundColor: theme.background.paper3,
|
|
||||||
},
|
|
||||||
'&.Mui-selected': {
|
|
||||||
fontWeight: '500',
|
|
||||||
backgroundColor: `${custom.selectedMenuItemBgColor} !important`,
|
|
||||||
color: theme.primary.main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiPaper: {
|
|
||||||
defaultProps: {
|
|
||||||
elevation: 1,
|
|
||||||
},
|
|
||||||
styleOverrides: {
|
|
||||||
root: ({ ownerState }: { ownerState: { elevation?: number } }) => {
|
|
||||||
return {
|
|
||||||
...(ownerState.elevation === 0 && {
|
|
||||||
backgroundColor: theme.background.paper2,
|
|
||||||
}),
|
|
||||||
...(ownerState.elevation === 2 && {
|
|
||||||
backgroundColor: theme.background.paper3,
|
|
||||||
}),
|
|
||||||
backgroundImage: 'none',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiChip: {
|
|
||||||
styleOverrides: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
root: ({ ownerState }: { ownerState: any }) => {
|
|
||||||
return {
|
|
||||||
height: '24px',
|
|
||||||
lineHeight: '24px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
'.MuiChip-label': {
|
|
||||||
padding: '0 8px 0 4px',
|
|
||||||
},
|
|
||||||
...(ownerState.color === 'default' && {
|
|
||||||
backgroundColor: theme.background.chip,
|
|
||||||
borderColor: theme.text.disabled,
|
|
||||||
'.Mui-focusVisible': {
|
|
||||||
backgroundColor: theme.background.chip,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
...(ownerState.color === 'error' && {
|
|
||||||
backgroundColor: addOpacityToColor(theme.error.main, 0.1),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
label: ({ ownerState }: { ownerState: any }) => {
|
|
||||||
return {
|
|
||||||
padding: '0 14px',
|
|
||||||
fontSize: '14px',
|
|
||||||
lineHeight: '24px',
|
|
||||||
...(ownerState.color === 'default' && {
|
|
||||||
color: theme.text.primary,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
deleteIcon: {
|
|
||||||
fontSize: '14px',
|
|
||||||
color: theme.text.disabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiAppBar: {
|
|
||||||
defaultProps: {
|
|
||||||
elevation: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiDialog: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
'h2.MuiTypography-root button': {
|
|
||||||
marginRight: '2px',
|
|
||||||
},
|
|
||||||
'.MuiDialogActions-root': {
|
|
||||||
paddingTop: '24px',
|
|
||||||
button: {
|
|
||||||
width: '88px',
|
|
||||||
height: '36px !important',
|
|
||||||
},
|
|
||||||
'.MuiButton-text': {
|
|
||||||
width: 'auto',
|
|
||||||
minWidth: 'auto',
|
|
||||||
color: `${theme.text.primary} !important`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
height: '100vh',
|
|
||||||
bgcolor: theme.text.secondary,
|
|
||||||
backdropFilter: 'blur(5px)',
|
|
||||||
},
|
|
||||||
paper: {
|
|
||||||
pb: 1,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: theme.divider,
|
|
||||||
borderRadius: '10px',
|
|
||||||
backgroundColor: theme.background.paper,
|
|
||||||
textarea: {
|
|
||||||
borderRadius: '8px 8px 0 8px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiDialogTitle: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
paddingTop: '24px',
|
|
||||||
'> button': {
|
|
||||||
top: '20px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiAlert: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
lineHeight: '22px',
|
|
||||||
paddingTop: '1px',
|
|
||||||
paddingBottom: '1px',
|
|
||||||
borderRadius: '10px',
|
|
||||||
boxShadow: 'none',
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
padding: '10px 0',
|
|
||||||
},
|
|
||||||
standardInfo: {
|
|
||||||
backgroundColor: addOpacityToColor(theme.primary.main, 0.1),
|
|
||||||
color: theme.text.primary,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiRadio: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
padding: 0,
|
|
||||||
marginRight: '8px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiTextField: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
label: {
|
|
||||||
color: theme.text.secondary,
|
|
||||||
},
|
|
||||||
'label.Mui-focused': {
|
|
||||||
color: theme.text.primary,
|
|
||||||
},
|
|
||||||
'& .MuiInputBase-input::placeholder': {
|
|
||||||
fontSize: '12px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiInputBase: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
borderRadius: '10px !important',
|
|
||||||
backgroundColor: theme.background.paper3,
|
|
||||||
'.MuiOutlinedInput-notchedOutline': {
|
|
||||||
borderColor: `${theme.background.paper3} !important`,
|
|
||||||
borderWidth: '1px !important',
|
|
||||||
},
|
|
||||||
'&.Mui-focused': {
|
|
||||||
'.MuiOutlinedInput-notchedOutline': {
|
|
||||||
borderColor: `${theme.text.primary} !important`,
|
|
||||||
borderWidth: '1px !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'&:hover': {
|
|
||||||
'.MuiOutlinedInput-notchedOutline': {
|
|
||||||
borderColor: `${theme.text.primary} !important`,
|
|
||||||
borderWidth: '1px !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
height: '19px',
|
|
||||||
'&.Mui-disabled': {
|
|
||||||
color: `${theme.text.secondary} !important`,
|
|
||||||
WebkitTextFillColor: `${theme.text.secondary} !important`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
MuiSelect: {
|
|
||||||
styleOverrides: {
|
|
||||||
root: {
|
|
||||||
height: '36px',
|
|
||||||
borderRadius: '10px !important',
|
|
||||||
backgroundColor: theme.background.paper3,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
paddingRight: '0 !important',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default componentStyleOverrides;
|
|
||||||
|
|
@ -25,7 +25,6 @@
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"html-react-parser": "^5.2.5",
|
"html-react-parser": "^5.2.5",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"import-in-the-middle": "^1.14.2",
|
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"katex": "^0.16.22",
|
"katex": "^0.16.22",
|
||||||
"markdown-it": "13.0.1",
|
"markdown-it": "13.0.1",
|
||||||
|
|
@ -42,7 +41,6 @@
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import SSEClient from '@/utils/fetch';
|
import SSEClient from '@/utils/fetch';
|
||||||
import { Box, Divider, Stack } from '@mui/material';
|
|
||||||
import { Editor, useTiptap, UseTiptapReturn } from '@ctzhian/tiptap';
|
import { Editor, useTiptap, UseTiptapReturn } from '@ctzhian/tiptap';
|
||||||
import { Modal } from '@ctzhian/ui';
|
import { Modal } from '@ctzhian/ui';
|
||||||
|
import { Box, Divider, Stack } from '@mui/material';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
interface AIGenerateProps {
|
interface AIGenerateProps {
|
||||||
|
|
@ -126,7 +126,7 @@ const AIGenerate = ({
|
||||||
ml: 1,
|
ml: 1,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
原文
|
原文
|
||||||
|
|
@ -150,7 +150,7 @@ const AIGenerate = ({
|
||||||
ml: 1,
|
ml: 1,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
润色后
|
润色后
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { V1NodeDetailResp } from '@/request/types';
|
import { V1NodeDetailResp } from '@/request/types';
|
||||||
|
import { Ellipsis, Icon } from '@ctzhian/ui';
|
||||||
|
import { Box, Button, Skeleton, Stack } from '@mui/material';
|
||||||
import { IconBaocun } from '@panda-wiki/icons';
|
import { IconBaocun } from '@panda-wiki/icons';
|
||||||
import { Box, Button, IconButton, Skeleton, Stack } from '@mui/material';
|
|
||||||
import { Ellipsis, Icon, message } from '@ctzhian/ui';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useWrapContext } from '..';
|
import { useWrapContext } from '..';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
|
|
@ -46,7 +46,7 @@ const Header = ({ edit, detail, handleSave }: HeaderProps) => {
|
||||||
onClick={() => setCatalogOpen(true)}
|
onClick={() => setCatalogOpen(true)}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
':hover': {
|
':hover': {
|
||||||
color: 'text.primary',
|
color: 'text.primary',
|
||||||
},
|
},
|
||||||
|
|
@ -78,7 +78,7 @@ const Header = ({ edit, detail, handleSave }: HeaderProps) => {
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
gap={0.5}
|
gap={0.5}
|
||||||
sx={{ fontSize: 12, color: 'text.auxiliary' }}
|
sx={{ fontSize: 12, color: 'text.tertiary' }}
|
||||||
>
|
>
|
||||||
<IconBaocun sx={{ fontSize: 12 }} />
|
<IconBaocun sx={{ fontSize: 12 }} />
|
||||||
{nodeDetail?.updated_at ? (
|
{nodeDetail?.updated_at ? (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Box, Skeleton, Stack } from '@mui/material';
|
|
||||||
import { useTiptap } from '@ctzhian/tiptap';
|
import { useTiptap } from '@ctzhian/tiptap';
|
||||||
import { Icon } from '@ctzhian/ui';
|
import { Icon } from '@ctzhian/ui';
|
||||||
|
import { Box, Skeleton, Stack } from '@mui/material';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Toolbar from './Toolbar';
|
import Toolbar from './Toolbar';
|
||||||
|
|
@ -61,11 +61,11 @@ const LoadingEditorWrap = () => {
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction={'row'} alignItems={'center'} gap={2} sx={{ mb: 4 }}>
|
<Stack direction={'row'} alignItems={'center'} gap={2} sx={{ mb: 4 }}>
|
||||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||||
<Icon type='icon-a-shijian2' sx={{ color: 'text.auxiliary' }} />
|
<Icon type='icon-a-shijian2' sx={{ color: 'text.tertiary' }} />
|
||||||
<Skeleton variant='text' width={130} height={24} />
|
<Skeleton variant='text' width={130} height={24} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||||
<Icon type='icon-ziti' sx={{ color: 'text.auxiliary' }} />
|
<Icon type='icon-ziti' sx={{ color: 'text.tertiary' }} />
|
||||||
<Skeleton variant='text' width={80} height={24} />
|
<Skeleton variant='text' width={80} height={24} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const HeadingIcon = [
|
||||||
|
|
||||||
const HeadingSx = [
|
const HeadingSx = [
|
||||||
{ fontSize: 14, fontWeight: 700, color: 'text.secondary' },
|
{ fontSize: 14, fontWeight: 700, color: 'text.secondary' },
|
||||||
{ fontSize: 14, fontWeight: 400, color: 'text.auxiliary' },
|
{ fontSize: 14, fontWeight: 400, color: 'text.tertiary' },
|
||||||
{ fontSize: 14, fontWeight: 400, color: 'text.disabled' },
|
{ fontSize: 14, fontWeight: 400, color: 'text.disabled' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ const Toc = ({
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
mb: 1,
|
mb: 1,
|
||||||
p: 1,
|
p: 1,
|
||||||
pb: 0,
|
pb: 0,
|
||||||
|
|
|
||||||
|
|
@ -267,10 +267,10 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
gap={0.5}
|
gap={0.5}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
cursor: 'text',
|
cursor: 'text',
|
||||||
':hover': {
|
':hover': {
|
||||||
color: 'text.auxiliary',
|
color: 'text.tertiary',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -286,7 +286,7 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
gap={0.5}
|
gap={0.5}
|
||||||
sx={{ fontSize: 12, color: 'text.auxiliary' }}
|
sx={{ fontSize: 12, color: 'text.tertiary' }}
|
||||||
>
|
>
|
||||||
<IconZiti />
|
<IconZiti />
|
||||||
{characterCount} 字
|
{characterCount} 字
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue