Compare commits

...

22 Commits

Author SHA1 Message Date
jiangwel 12b51f2b2b fix: 修复前端样式问题 2025-11-12 11:06:19 +08:00
jiangwel 1ff013ac47 chore: 更新模型设置模式及相关配置,移除冗余迁移功能 2025-11-11 16:48:15 +08:00
jiangwel c88bd58b6b chore: 添加模型设置模式及相关配置 2025-11-10 21:07:18 +08:00
jiangwel bddd515df2 feat: 添加关闭弹窗时未应用更改的确认提示 2025-11-10 21:07:18 +08:00
jiangwel b1c564ff2a feat(model): 添加模型设置模式切换功能
实现模型设置模式(手动/自动)切换功能,包括:
1. 新增SettingRepository及相关数据库操作
2. 添加模型模式相关常量定义
3. 实现模式切换API接口及业务逻辑
4. 添加数据库迁移脚本初始化模型设置
5. 更新swagger文档

feat(模型): 添加百智云自动模式配置功能

实现百智云模型自动模式的API Key和对话模型配置功能
重构相关代码,将BaiZhiCloud重命名为AutoMode
更新迁移脚本和模型设置接口

feat: 添加模型模式设置和自动模式支持

feat(系统设置): 重构系统设置模块,迁移至新表结构

- 新增system_settings表及相关迁移文件
- 将原setting表功能迁移至system_setting表
- 更新模型设置相关逻辑,支持自动模式配置
- 优化Makefile构建目标,分离pro和pro_consumer
- 更新API文档,修正模型设置相关接口路径

feat: 前端支持自动配置模型

feat: 在教程添加模型配置

feat: 在教程中添加配置模型

chore

feat: 修改前端样式

feat: 修改前端样式

feat: 前端流程跑通

feat: 跑通前端流程

feat: 优化接口
2025-11-10 21:07:18 +08:00
xiaomakuaiz e4dbfcb9fb
Merge pull request #1492 from KuaiYu95/fe/contribute
feat: 支持批量重新学习失败的文档
2025-11-10 19:33:56 +08:00
xiaomakuaiz 40c395400d
Merge pull request #1483 from coltea/feat-restudy
feat restudy
2025-11-10 19:33:43 +08:00
coltea b7cdec0d4a feat restudy 2025-11-10 14:13:09 +08:00
xiaomakuaiz 44126e0a11
Merge pull request #1493 from guanweiwang/hotfix/bug
fix: 拖拽组件输入文字 失焦问题
2025-11-10 12:14:36 +08:00
Gavan 3375fcb643 fix: 拖拽组件输入文字 失焦问题 2025-11-10 11:29:42 +08:00
yu.kuai ef3bae6336 feat: 支持批量重新学习失败的文档 2025-11-10 11:15:36 +08:00
xiaomakuaiz d9d3bc4911
Merge pull request #1491 from KuaiYu95/fe/editor-ui
feat: 编辑器部分组件样式更新
2025-11-10 10:37:29 +08:00
xiaomakuaiz 546062470b
Merge pull request #1478 from xiaomakuaiz/feature/summary-optimization
Feature/summary optimization
2025-11-10 10:37:10 +08:00
yu.kuai 5a3b23ac75 feat: 编辑器部分组件样式更新 2025-11-10 10:12:54 +08:00
xiaomakuaiz 35cd94e342
Merge pull request #1484 from guanweiwang/pref/landing_drag
pref: 优化和bug修复
2025-11-07 17:06:15 +08:00
monkeycode-ai f7c0fe273b Improve summary optimization with simplified aggregation
优化摘要生成逻辑:
1. 将chunk token限制从16KB提升到30KB,更合理地利用模型上下文
2. 简化摘要聚合逻辑,移除复杂的分批聚合,直接合并所有summaries
3. 保留fallback机制,当最终摘要生成失败时返回已聚合的摘要

这些改进确保了:
- 长文档能够更充分地被摘要(30KB vs 16KB)
- 代码更简洁,避免不必要的迭代聚合和额外LLM调用
- 即使最终摘要失败也能返回有用的结果

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-07 14:21:36 +08:00
Gavan 0175624c84 pref: 删除无用代码,提取公共拖拽函数,修复 footer 2025-11-07 12:00:48 +08:00
monkeycode-ai a6f4688b88 Run goimports on llm summary
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 20:07:30 +08:00
monkeycode-ai 575f51f0ea Simplify final summary aggregation
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 19:52:03 +08:00
monkeycode-ai 83f6853716 Iteratively reduce summary chunks
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 19:21:26 +08:00
monkeycode-ai 3dae8e8d01 Raise summary chunk limit to 16k
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 19:17:27 +08:00
monkeycode-ai 2e1e1848c4 Adjust summary chunking and concurrency
Co-authored-by: monkeycode-ai <monkeycode-ai@chaitin.com>
2025-11-06 19:09:15 +08:00
105 changed files with 4920 additions and 4720 deletions

View File

@ -56,3 +56,11 @@ type NodePermissionEditReq 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 {
}

View File

@ -96,7 +96,9 @@ func createApp() (*App, error) {
return nil, err
}
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)
geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
ipdbIPDB, err := ipdb.NewIPDB(configConfig, logger)
@ -105,7 +107,6 @@ func createApp() (*App, error) {
}
ipAddressRepo := ipdb2.NewIPAddressRepo(ipdbIPDB, logger)
conversationUsecase := usecase.NewConversationUsecase(conversationRepository, nodeRepository, geoRepo, logger, ipAddressRepo, authRepo)
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository)
blockWordRepo := pg2.NewBlockWordRepo(db, logger)
chatUsecase, err := usecase.NewChatUsecase(llmUsecase, knowledgeBaseRepository, conversationUsecase, modelUsecase, appRepository, blockWordRepo, authRepo, logger)
if err != nil {

View File

@ -8,12 +8,12 @@ package main
import (
"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/mq"
cache2 "github.com/chaitin/panda-wiki/repo/cache"
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"
"github.com/chaitin/panda-wiki/store/cache"
"github.com/chaitin/panda-wiki/store/ipdb"
@ -49,11 +49,18 @@ func createApp() (*App, error) {
modelRepository := pg2.NewModelRepository(db, logger)
promptRepo := pg2.NewPromptRepo(db, 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 {
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 {
return nil, err
}
@ -71,22 +78,17 @@ func createApp() (*App, error) {
geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
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)
minioClient, err := s3.NewMinioClient(configConfig)
if err != nil {
return nil, err
}
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo)
cronHandler, err := mq2.NewStatCronHandler(logger, statRepository, statUseCase, nodeUsecase)
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo, modelUsecase)
cronHandler, err := mq3.NewStatCronHandler(logger, statRepository, statUseCase, nodeUsecase)
if err != nil {
return nil, err
}
mqHandlers := &mq2.MQHandlers{
mqHandlers := &mq3.MQHandlers{
RAGMQHandler: ragmqHandler,
RagDocUpdateHandler: ragDocUpdateHandler,
StatCronHandler: cronHandler,
@ -105,6 +107,6 @@ func createApp() (*App, error) {
type App struct {
MQConsumer mq.MQConsumer
Config *config.Config
MQHandlers *mq2.MQHandlers
StatCronHandler *mq2.CronHandler
MQHandlers *mq3.MQHandlers
StatCronHandler *mq3.CronHandler
}

View File

@ -60,7 +60,9 @@ func createApp() (*App, error) {
return nil, err
}
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)
knowledgeBaseUsecase, err := usecase.NewKnowledgeBaseUsecase(knowledgeBaseRepository, nodeRepository, ragRepository, userRepository, ragService, kbRepo, logger, configConfig)
if err != nil {

39
backend/consts/model.go Normal file
View File

@ -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"
)

View File

@ -0,0 +1,7 @@
package consts
type SystemSettingKey string
const (
SystemSettingModelMode SystemSettingKey = "model_setting_mode"
)

View File

@ -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": {
"post": {
"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": {
"post": {
"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": {
"post": {
"security": [
@ -3952,6 +4085,17 @@ const docTemplate = `{
"LicenseEditionEnterprise"
]
},
"consts.ModelSettingMode": {
"type": "string",
"enum": [
"manual",
"auto"
],
"x-enum-varnames": [
"ModelSettingModeManual",
"ModelSettingModeAuto"
]
},
"consts.NodeAccessPerm": {
"type": "string",
"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": {
"type": "string",
"enum": [
@ -7045,6 +7214,37 @@ const docTemplate = `{
"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": {
"type": "object",
"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": {
"type": "object",
"required": [

View File

@ -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": {
"post": {
"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": {
"post": {
"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": {
"post": {
"security": [
@ -3945,6 +4078,17 @@
"LicenseEditionEnterprise"
]
},
"consts.ModelSettingMode": {
"type": "string",
"enum": [
"manual",
"auto"
],
"x-enum-varnames": [
"ModelSettingModeManual",
"ModelSettingModeAuto"
]
},
"consts.NodeAccessPerm": {
"type": "string",
"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": {
"type": "string",
"enum": [
@ -7038,6 +7207,37 @@
"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": {
"type": "object",
"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": {
"type": "object",
"required": [

View File

@ -131,6 +131,14 @@ definitions:
- LicenseEditionFree
- LicenseEditionContributor
- LicenseEditionEnterprise
consts.ModelSettingMode:
enum:
- manual
- auto
type: string
x-enum-varnames:
- ModelSettingModeManual
- ModelSettingModeAuto
consts.NodeAccessPerm:
enum:
- open
@ -1628,6 +1636,22 @@ definitions:
type:
type: string
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:
enum:
- chat
@ -2178,6 +2202,27 @@ definitions:
- StatPageSceneNodeDetail
- StatPageSceneChat
- 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:
properties:
title:
@ -2986,6 +3031,21 @@ definitions:
$ref: '#/definitions/domain.NodeGroupDetail'
type: array
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:
properties:
id:
@ -4050,6 +4110,27 @@ paths:
summary: get model list
tags:
- 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:
post:
consumes:
@ -4077,6 +4158,33 @@ paths:
summary: get provider supported model list
tags:
- 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:
post:
consumes:
@ -4374,6 +4482,36 @@ paths:
summary: Recommend Nodes
tags:
- 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:
post:
consumes:

View File

@ -165,3 +165,13 @@ type ProviderModelListItem struct {
type ActivateModelReq struct {
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"`
}

View File

@ -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"` // 手动模式下嵌入模型是否更新
}

View File

@ -27,6 +27,7 @@ var ProviderSet = wire.NewSet(
usecase.NewLLMUsecase,
usecase.NewStatUseCase,
usecase.NewNodeUsecase,
usecase.NewModelUsecase,
NewRAGMQHandler,
NewRagDocUpdateHandler,

View File

@ -15,24 +15,24 @@ import (
)
type RAGMQHandler struct {
consumer mq.MQConsumer
logger *log.Logger
rag rag.RAGService
nodeRepo *pg.NodeRepository
kbRepo *pg.KnowledgeBaseRepository
modelRepo *pg.ModelRepository
llmUsecase *usecase.LLMUsecase
consumer mq.MQConsumer
logger *log.Logger
rag rag.RAGService
nodeRepo *pg.NodeRepository
kbRepo *pg.KnowledgeBaseRepository
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{
consumer: consumer,
logger: logger.WithModule("mq.rag"),
rag: rag,
nodeRepo: nodeRepo,
kbRepo: kbRepo,
llmUsecase: llmUsecase,
modelRepo: modelRepo,
consumer: consumer,
logger: logger.WithModule("mq.rag"),
rag: rag,
nodeRepo: nodeRepo,
kbRepo: kbRepo,
llmUsecase: llmUsecase,
modelUsecase: modelUsecase,
}
if err := consumer.RegisterHandler(domain.VectorTaskTopic, h.HandleNodeContentVectorRequest); err != nil {
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))
return nil
}
model, err := h.modelRepo.GetChatModel(ctx)
model, err := h.modelUsecase.GetChatModel(ctx)
if err != nil {
h.logger.Error("get chat model failed", log.Error(err))
return nil
}
summary, err := h.llmUsecase.SummaryNode(ctx, model, node.Name, node.Content)
if err != nil {
h.logger.Error("summary node content failed", log.Error(err))

View File

@ -40,6 +40,8 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
group.POST("/check", handler.CheckModel)
group.POST("/provider/supported", handler.GetProviderSupportedModelList)
group.PUT("", handler.UpdateModel)
group.POST("/switch-mode", handler.SwitchMode)
group.GET("/mode-setting", handler.GetModelModeSetting)
return handler
}
@ -211,3 +213,58 @@ func (h *ModelHandler) GetProviderSupportedModelList(c echo.Context) error {
}
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)
}

View File

@ -47,6 +47,7 @@ func NewNodeHandler(
group.POST("/batch_move", h.BatchMoveNode)
group.GET("/recommend_nodes", h.RecommendNodes)
group.POST("/restudy", h.NodeRestudy)
// node permission
group.GET("/permission", h.NodePermission)
@ -384,3 +385,32 @@ func (h *NodeHandler) NodePermissionEdit(c echo.Context) error {
}
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)
}

View File

@ -23,4 +23,5 @@ var ProviderSet = wire.NewSet(
NewAuthRepo,
NewWechatRepository,
NewAPITokenRepo,
NewSystemSettingRepo,
)

View File

@ -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
}

View File

@ -0,0 +1,4 @@
-- Drop settings table
DROP TABLE IF EXISTS system_settings;
-- drop index
DROP INDEX IF EXISTS idx_system_settings_key;

View File

@ -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'
);

View File

@ -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) {
// For FIM (Fill in Middle) style completion, we need to handle prefix and suffix
if req.Prefix != "" || req.Suffix != "" {
model, err := u.model.GetModelByType(ctx, domain.ModelTypeChat)
model, err := u.model.GetChatModel(ctx)
if err != nil {
u.logger.Error("get chat model failed", log.Error(err))
return "", domain.ErrModelNotConfigured

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"math"
"slices"
"strings"
"time"
@ -16,8 +15,6 @@ import (
"github.com/cloudwego/eino/schema"
"github.com/pkoukk/tiktoken-go"
"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/domain"
@ -39,6 +36,11 @@ type LLMUsecase struct {
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 {
tiktoken.SetBpeLoader(&utils.Localloader{})
modelkit := modelkit.NewModelKit(logger.Logger)
@ -197,52 +199,59 @@ func (u *LLMUsecase) SummaryNode(ctx context.Context, model *domain.Model, name,
return "", err
}
chunks, err := u.SplitByTokenLimit(content, int(math.Floor(1024*32*0.95)))
chunks, err := u.SplitByTokenLimit(content, summaryChunkTokenLimit)
if err != nil {
return "", err
}
sem := semaphore.NewWeighted(int64(10))
summaries := parallel.Map(chunks, func(chunk string, _ int) string {
if err := sem.Acquire(ctx, 1); err != nil {
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))
if len(chunks) > summaryMaxChunks {
u.logger.Debug("trim summary chunks for large document", log.String("node", name), log.Int("original_chunks", len(chunks)), log.Int("used_chunks", summaryMaxChunks))
chunks = chunks[:summaryMaxChunks]
}
contents, err := u.SplitByTokenLimit(strings.Join(summaries, "\n\n"), int(math.Floor(1024*32*0.95)))
if err != nil {
return "", err
summaries := make([]string, 0, len(chunks))
for idx, chunk := range chunks {
summary, err := u.requestSummary(ctx, chatModel, name, chunk)
if err != nil {
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{
{
Role: "system",
@ -250,20 +259,13 @@ func (u *LLMUsecase) SummaryNode(ctx context.Context, model *domain.Model, name,
},
{
Role: "user",
Content: fmt.Sprintf("文档名称:%s\n文档内容%s", name, contents[0]),
Content: fmt.Sprintf("文档名称:%s\n文档内容%s", name, content),
},
})
if err != nil {
return "", err
}
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, nil
return strings.TrimSpace(u.trimThinking(summary)), nil
}
func (u *LLMUsecase) SplitByTokenLimit(text string, maxTokens int) ([]string, error) {

View File

@ -2,14 +2,15 @@ package usecase
import (
"context"
"encoding/json"
"fmt"
"time"
"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/consts"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/repo/mq"
@ -18,118 +19,47 @@ import (
)
type ModelUsecase struct {
modelRepo *pg.ModelRepository
logger *log.Logger
config *config.Config
nodeRepo *pg.NodeRepository
ragRepo *mq.RAGRepository
ragStore rag.RAGService
kbRepo *pg.KnowledgeBaseRepository
modelRepo *pg.ModelRepository
logger *log.Logger
config *config.Config
nodeRepo *pg.NodeRepository
ragRepo *mq.RAGRepository
ragStore rag.RAGService
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{
modelRepo: modelRepo,
logger: logger.WithModule("usecase.model"),
config: config,
nodeRepo: nodeRepo,
ragRepo: ragRepo,
ragStore: ragStore,
kbRepo: kbRepo,
}
if err := u.initEmbeddingAndRerankModel(context.Background()); err != nil {
logger.Error("init embedding & rerank & analysis model failed", log.Any("error", err))
modelRepo: modelRepo,
logger: logger.WithModule("usecase.model"),
config: config,
nodeRepo: nodeRepo,
ragRepo: ragRepo,
ragStore: ragStore,
kbRepo: kbRepo,
systemSettingRepo: settingRepo,
modelkit: modelkit,
}
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 {
var updatedEmbeddingModel bool
if model.Type == domain.ModelTypeEmbedding {
updatedEmbeddingModel = true
}
if err := u.modelRepo.Create(ctx, model); err != nil {
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
} else {
model.ID = id
}
}
if model.Type == domain.ModelTypeEmbedding {
return u.TriggerUpsertRecords(ctx)
}
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 {
var updatedEmbeddingModel bool
if req.Type == domain.ModelTypeEmbedding {
updatedEmbeddingModel = true
}
if err := u.modelRepo.Update(ctx, req); err != nil {
return err
}
ragModelTypes := []domain.ModelType{
domain.ModelTypeEmbedding,
domain.ModelTypeRerank,
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 {
// 模型更新成功后,如果更新嵌入模型,则触发记录更新
if updatedEmbeddingModel {
if _, err := u.updateModeSettingConfig(ctx, "", "", "", true); err != nil {
return err
}
}
// update all records when embedding model is updated
if req.Type == domain.ModelTypeEmbedding {
return u.TriggerUpsertRecords(ctx)
}
return nil
}
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) {
@ -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 {
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
}

View File

@ -27,17 +27,18 @@ import (
)
type NodeUsecase struct {
nodeRepo *pg.NodeRepository
appRepo *pg.AppRepository
ragRepo *mq.RAGRepository
kbRepo *pg.KnowledgeBaseRepository
modelRepo *pg.ModelRepository
userRepo *pg.UserRepository
authRepo *pg.AuthRepo
llmUsecase *LLMUsecase
logger *log.Logger
s3Client *s3.MinioClient
rAGService rag.RAGService
nodeRepo *pg.NodeRepository
appRepo *pg.AppRepository
ragRepo *mq.RAGRepository
kbRepo *pg.KnowledgeBaseRepository
modelRepo *pg.ModelRepository
userRepo *pg.UserRepository
authRepo *pg.AuthRepo
llmUsecase *LLMUsecase
logger *log.Logger
s3Client *s3.MinioClient
rAGService rag.RAGService
modelUsecase *ModelUsecase
}
func NewNodeUsecase(
@ -52,19 +53,21 @@ func NewNodeUsecase(
s3Client *s3.MinioClient,
modelRepo *pg.ModelRepository,
authRepo *pg.AuthRepo,
modelUsecase *ModelUsecase,
) *NodeUsecase {
return &NodeUsecase{
nodeRepo: nodeRepo,
rAGService: ragService,
appRepo: appRepo,
ragRepo: ragRepo,
kbRepo: kbRepo,
authRepo: authRepo,
userRepo: userRepo,
llmUsecase: llmUsecase,
modelRepo: modelRepo,
logger: logger.WithModule("usecase.node"),
s3Client: s3Client,
nodeRepo: nodeRepo,
rAGService: ragService,
appRepo: appRepo,
ragRepo: ragRepo,
kbRepo: kbRepo,
authRepo: authRepo,
userRepo: userRepo,
llmUsecase: llmUsecase,
modelRepo: modelRepo,
logger: logger.WithModule("usecase.node"),
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) {
model, err := u.modelRepo.GetChatModel(ctx)
model, err := u.modelUsecase.GetChatModel(ctx)
if err != nil {
if err == gorm.ErrRecordNotFound {
return "", domain.ErrModelNotConfigured
@ -573,3 +576,30 @@ func (u *NodeUsecase) SyncRagNodeStatus(ctx context.Context) error {
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
}

View File

@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react';
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 {
setKbC,
@ -10,18 +10,20 @@ import {
import { useAppSelector, useAppDispatch } from '@/store';
import { postApiV1KnowledgeBaseRelease } from '@/request/KnowledgeBase';
import {
Step1Config,
Step2Import,
Step3Publish,
Step4Test,
Step5Decorate,
Step6Complete,
Step1Model,
Step2Config,
Step3Import,
Step4Publish,
Step5Test,
Step6Decorate,
Step7Complete,
} from './steps';
import dayjs from 'dayjs';
// Remove interface as we're using Redux state
const steps = [
'模型配置',
'配置监听',
'录入文档',
'发布内容',
@ -31,20 +33,19 @@ const steps = [
];
const CreateWikiModal = () => {
const { kb_c, kb_id, kbList, modelStatus } = useAppSelector(
state => state.config,
);
const { kb_c, kb_id, kbList } = useAppSelector(state => state.config);
const dispatch = useAppDispatch();
const location = useLocation();
const [open, setOpen] = useState(false);
const [activeStep, setActiveStep] = useState(0);
const [nodeIds, setNodeIds] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const step1ConfigRef = useRef<{ onSubmit: () => Promise<void> }>(null);
const step2ImportRef = useRef<{
const Step1ModelRef = useRef<{ onSubmit: () => Promise<void> }>(null);
const step2ConfigRef = useRef<{ onSubmit: () => Promise<void> }>(null);
const step3ImportRef = useRef<{
onSubmit: () => Promise<Record<'id', string>[]>;
}>(null);
const step5DecorateRef = useRef<{ onSubmit: () => Promise<void> }>(null);
const step6DecorateRef = useRef<{ onSubmit: () => Promise<void> }>(null);
const onCancel = () => {
dispatch(setKbC(false));
@ -66,7 +67,20 @@ const CreateWikiModal = () => {
const handleNext = () => {
if (activeStep === 0) {
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?.()
.then(() => {
setActiveStep(prev => prev + 1);
@ -74,9 +88,9 @@ const CreateWikiModal = () => {
.finally(() => {
setLoading(false);
});
} else if (activeStep === 1) {
} else if (activeStep === 2) {
setLoading(true);
step2ImportRef.current
step3ImportRef.current
?.onSubmit?.()
.then(res => {
setNodeIds(res.map(item => item.id));
@ -85,17 +99,17 @@ const CreateWikiModal = () => {
.finally(() => {
setLoading(false);
});
} else if (activeStep === 2) {
} else if (activeStep === 3) {
setLoading(true);
onPublish().finally(() => {
setActiveStep(prev => prev + 1);
setLoading(false);
});
} else if (activeStep === 3) {
setActiveStep(prev => prev + 1);
} else if (activeStep === 4) {
setActiveStep(prev => prev + 1);
} else if (activeStep === 5) {
setLoading(true);
step5DecorateRef.current
step6DecorateRef.current
?.onSubmit?.()
.then(() => {
setActiveStep(prev => prev + 1);
@ -103,7 +117,7 @@ const CreateWikiModal = () => {
.finally(() => {
setLoading(false);
});
} else if (activeStep === 5) {
} else if (activeStep === 6) {
onCancel();
}
};
@ -117,17 +131,19 @@ const CreateWikiModal = () => {
const renderStepContent = () => {
switch (activeStep) {
case 0:
return <Step1Config ref={step1ConfigRef} />;
return <Step1Model ref={Step1ModelRef} />;
case 1:
return <Step2Import ref={step2ImportRef} />;
return <Step2Config ref={step2ConfigRef} />;
case 2:
return <Step3Publish />;
return <Step3Import ref={step3ImportRef} />;
case 3:
return <Step4Test />;
return <Step4Publish />;
case 4:
return <Step5Decorate ref={step5DecorateRef} nodeIds={nodeIds} />;
return <Step5Test />;
case 5:
return <Step6Complete />;
return <Step6Decorate ref={step6DecorateRef} nodeIds={nodeIds} />;
case 6:
return <Step7Complete />;
default:
return null;
}
@ -148,8 +164,8 @@ const CreateWikiModal = () => {
}, [kb_c]);
useEffect(() => {
if (kbList?.length === 0 && modelStatus) setOpen(true);
}, [kbList, modelStatus]);
if (kbList?.length === 0) setOpen(true);
}, [kbList]);
return (
<Modal

View File

@ -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;

View File

@ -73,11 +73,11 @@ const VALIDATION_RULES = {
},
};
interface Step1ConfigProps {
interface Step2ConfigProps {
ref: Ref<{ onSubmit: () => Promise<unknown> }>;
}
const Step1Config: React.FC<Step1ConfigProps> = ({ ref }) => {
const Step2Config: React.FC<Step2ConfigProps> = ({ ref }) => {
const {
control,
formState: { errors },
@ -358,4 +358,4 @@ const Step1Config: React.FC<Step1ConfigProps> = ({ ref }) => {
);
};
export default Step1Config;
export default Step2Config;

View File

@ -5,11 +5,11 @@ import { postApiV1Node } from '@/request/Node';
import { INIT_DOC_DATA } from './initData';
import { useAppSelector } from '@/store';
interface Step2ImportProps {
interface Step3ImportProps {
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 onSubmit = () => {
return Promise.all(
@ -42,4 +42,4 @@ const Step2Import: React.FC<Step2ImportProps> = ({ ref }) => {
);
};
export default Step2Import;
export default Step3Import;

View File

@ -1,7 +1,7 @@
import { Box, Stack, FormControlLabel, Checkbox } from '@mui/material';
import publish from '@/assets/images/init/publish.png';
const Step3Publish = () => {
const Step4Publish = () => {
return (
<Stack gap={2} sx={{ textAlign: 'center', py: 4 }}>
<Box component='img' src={publish} sx={{ width: '100%' }}></Box>
@ -18,4 +18,4 @@ const Step3Publish = () => {
);
};
export default Step3Publish;
export default Step4Publish;

View File

@ -1,7 +1,7 @@
import { Box, Stack } from '@mui/material';
import test from '@/assets/images/init/test.png';
const Step4Test = () => {
const Step5Test = () => {
return (
<Stack gap={2} sx={{ textAlign: 'center', py: 4 }}>
<Box component='img' src={test} sx={{ width: '100%' }}></Box>
@ -9,4 +9,4 @@ const Step4Test = () => {
);
};
export default Step4Test;
export default Step5Test;

View File

@ -5,12 +5,12 @@ import { INIT_LADING_DATA } from './initData';
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
import { useAppSelector } from '@/store';
interface Step5DecorateProps {
interface Step6DecorateProps {
ref: Ref<{ onSubmit: () => void }>;
nodeIds: string[];
}
const Step5Decorate: React.FC<Step5DecorateProps> = ({ ref, nodeIds }) => {
const Step6Decorate: React.FC<Step6DecorateProps> = ({ ref, nodeIds }) => {
const { kb_id } = useAppSelector(state => state.config);
const onSubmit = () => {
return getApiV1AppDetail({
@ -60,4 +60,4 @@ const Step5Decorate: React.FC<Step5DecorateProps> = ({ ref, nodeIds }) => {
);
};
export default Step5Decorate;
export default Step6Decorate;

View File

@ -3,7 +3,7 @@ import { Box, Stack, Button } from '@mui/material';
import complete from '@/assets/images/init/complete.png';
import { useAppSelector } from '@/store';
const Step6Complete = () => {
const Step7Complete = () => {
const { kbDetail } = useAppSelector(state => state.config);
const wikiUrl = useMemo(() => {
@ -56,4 +56,4 @@ const Step6Complete = () => {
);
};
export default Step6Complete;
export default Step7Complete;

View File

@ -1,6 +1,7 @@
export { default as Step1Config } from './Step1Config';
export { default as Step2Import } from './Step2Import';
export { default as Step3Publish } from './Step3Publish';
export { default as Step4Test } from './Step4Test';
export { default as Step5Decorate } from './Step5Decorate';
export { default as Step6Complete } from './Step6Complete';
export { default as Step1Model } from './Step1Model';
export { default as Step2Config } from './Step2Config';
export { default as Step3Import } from './Step3Import';
export { default as Step4Publish } from './Step4Publish';
export { default as Step5Test } from './Step5Test';
export { default as Step6Decorate } from './Step6Decorate';
export { default as Step7Complete } from './Step7Complete';

View File

@ -1,5 +1,5 @@
import { FooterSetting } from '@/api/type';
import { useAppDispatch, useAppSelector } from '@/store';
import { Icon } from '@ctzhian/ui';
import {
closestCenter,
DndContext,
@ -18,22 +18,15 @@ import {
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Box, Button, IconButton, Stack, TextField } from '@mui/material';
import { Icon } from '@ctzhian/ui';
import { Box, IconButton, Stack, TextField } from '@mui/material';
import {
CSSProperties,
forwardRef,
HTMLAttributes,
useCallback,
useEffect,
useState,
} from 'react';
import {
Control,
Controller,
FieldErrors,
useFieldArray,
} from 'react-hook-form';
import { Control, Controller, FieldErrors } from 'react-hook-form';
import { BrandGroup } from '.';
export type ItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> & {
@ -122,7 +115,7 @@ const LinkItem = forwardRef<HTMLDivElement, LinkItemProps>(
size='small'
onClick={onRemove}
sx={{
color: 'text.auxiliary',
color: 'text.tertiary',
':hover': { color: 'error.main' },
flexShrink: 0,
width: '28px',
@ -395,7 +388,7 @@ const Item = forwardRef<HTMLDivElement, ItemProps>(
size='small'
onClick={handleRemove}
sx={{
color: 'text.auxiliary',
color: 'text.tertiary',
':hover': { color: 'error.main' },
flexShrink: 0,
width: '28px',

View File

@ -1,8 +1,8 @@
import UploadFile from '@/components/UploadFile';
import { DomainSocialMediaAccount } from '@/request/types';
import { Icon } from '@ctzhian/ui';
import {
Box,
FormControl,
IconButton,
MenuItem,
Select,
@ -11,7 +11,6 @@ import {
ToggleButton,
ToggleButtonGroup,
} from '@mui/material';
import { Icon } from '@ctzhian/ui';
import {
CSSProperties,
Dispatch,
@ -113,7 +112,7 @@ const Item = forwardRef<HTMLDivElement, SocialInfoProps>(
setIsEdit(true);
}}
sx={{
color: 'text.auxiliary',
color: 'text.tertiary',
':hover': { color: 'error.main' },
flexShrink: 0,
width: '28px',

View File

@ -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;

View File

@ -1,11 +1,22 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { FC } from 'react';
import FaqItem, { ItemTypeProps } from './Item';
import { ComponentType } from 'react';
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 {
isDragging,
attributes,
@ -13,7 +24,7 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
setNodeRef,
transform,
transition,
} = useSortable({ id: item.id });
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
@ -21,7 +32,7 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
};
return (
<FaqItem
<ItemComponent
ref={setNodeRef}
style={style}
withOpacity={isDragging}
@ -33,6 +44,6 @@ const SortableItem: FC<SortableItemProps> = ({ item, ...rest }) => {
{...rest}
/>
);
};
}
export default SortableItem;

View File

@ -0,0 +1,4 @@
export { default as DragList } from './DragList';
export type { DragListProps } from './DragList';
export type { SortableItemProps } from './SortableItem';

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,18 +1,21 @@
import React, { useState, useEffect } from 'react';
import { TextField, Chip, Autocomplete, Box } from '@mui/material';
import React, { useEffect, useRef, useMemo } from 'react';
import { TextField } from '@mui/material';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import type { ConfigProps } from '../type';
import { useForm, Controller } from 'react-hook-form';
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 { DEFAULT_DATA } from '../../../constants';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
import { handleLandingConfigs, findConfigById } from '../../../utils';
import { Empty } from '@ctzhian/ui';
const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
const { appPreviewData } = useAppSelector(state => state.config);
const [inputValue, setInputValue] = useState('');
const { control, watch, setValue, subscribe } = useForm<
typeof DEFAULT_DATA.banner
>({
@ -24,6 +27,49 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
const debouncedDispatch = useDebounceAppPreviewData();
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 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(() => {
const callback = subscribe({
formState: {
@ -59,6 +143,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
return () => {
callback();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscribe]);
return (
@ -132,60 +217,18 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
render={({ field }) => <TextField {...field} placeholder='请输入' />}
/>
</CommonItem>
<CommonItem title='热门搜索'>
<Controller
control={control}
name='hot_search'
render={({ field }) => (
<Autocomplete
{...field}
value={field.value || []}
multiple
freeSolo
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 title='热门搜索' onAdd={handleAddHotSearch}>
{hotSearchList.length === 0 ? (
<Empty />
) : (
<DragList
data={hotSearchList}
onChange={handleHotSearchChange}
setIsEdit={setIsEdit}
SortableItemComponent={HotSearchSortableItem}
ItemComponent={HotSearchItem}
/>
)}
</CommonItem>
<CommonItem title='主按钮' onAdd={handleAddButton}>
<DragList
@ -195,6 +238,8 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
setIsEdit(true);
}}
setIsEdit={setIsEdit}
SortableItemComponent={ButtonSortableItem}
ItemComponent={Item}
/>
</CommonItem>
</StyledCommonWrapper>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { Empty } from '@ctzhian/ui';
import { useAppSelector } from '@/store';
@ -40,6 +42,12 @@ const BasicDocConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -116,13 +124,15 @@ const BasicDocConfig = ({ setIsEdit, id }: ConfigProps) => {
{nodes.length === 0 ? (
<Empty />
) : (
<BasicDocDragList
<DragList
data={nodes}
onChange={value => {
setIsEdit(true);
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -36,6 +38,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -91,6 +99,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -37,6 +39,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -121,6 +129,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -34,6 +36,12 @@ const CaseConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -90,6 +98,8 @@ const CaseConfig = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -37,6 +39,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -93,6 +101,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
@ -40,6 +42,12 @@ const DirDocConfig = ({ setIsEdit, id }: ConfigProps) => {
nodeRec(newList);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -117,13 +125,15 @@ const DirDocConfig = ({ setIsEdit, id }: ConfigProps) => {
{nodes.length === 0 ? (
<Empty />
) : (
<BasicDocDragList
<DragList
data={nodes}
onChange={value => {
setIsEdit(true);
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,14 +1,15 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
import { Empty } from '@ctzhian/ui';
import { DEFAULT_DATA } from '../../../constants';
import ColorPickerField from '../../components/ColorPickerField';
import { findConfigById, handleLandingConfigs } from '../../../utils';
const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
@ -35,6 +36,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const FaqSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={FaqItem} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -111,10 +118,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
{list.length === 0 ? (
<Empty />
) : (
<FaqDragList
<DragList
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={FaqSortableComponent}
ItemComponent={FaqItem}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -34,6 +36,12 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -89,6 +97,8 @@ const Config = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -34,6 +36,12 @@ const MetricsConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -90,6 +98,8 @@ const MetricsConfig = ({ setIsEdit, id }: ConfigProps) => {
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 { useAppSelector } from '@/store';
import useDebounceAppPreviewData from '@/hooks/useDebounceAppPreviewData';
@ -36,6 +38,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -88,10 +96,12 @@ const FaqConfig = ({ setIsEdit, id }: ConfigProps) => {
{list.length === 0 ? (
<Empty />
) : (
<FaqDragList
<DragList
data={list}
onChange={handleListChange}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -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;

View File

@ -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;

View File

@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { CommonItem, StyledCommonWrapper } from '../../components/StyledCommon';
import { TextField } from '@mui/material';
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 type { ConfigProps } from '../type';
import { useAppSelector } from '@/store';
@ -40,6 +42,12 @@ const SimpleDocConfigConfig = ({ setIsEdit, id }: ConfigProps) => {
setIsEdit(true);
};
// 稳定的 SortableItemComponent 引用
const ItemSortableComponent = useMemo(
() => (props: any) => <SortableItem {...props} ItemComponent={Item} />,
[],
);
useEffect(() => {
reset(
findConfigById(
@ -124,6 +132,8 @@ const SimpleDocConfigConfig = ({ setIsEdit, id }: ConfigProps) => {
setValue('nodes', value);
}}
setIsEdit={setIsEdit}
SortableItemComponent={ItemSortableComponent}
ItemComponent={Item}
/>
)}
</CommonItem>

View File

@ -50,7 +50,7 @@ const dark = {
text: {
primary: '#fff',
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)',
slave: 'rgba(255,255,255,0.05)',
inverseAuxiliary: 'rgba(0,0,0,0.5)',

View File

@ -55,7 +55,7 @@ const light = {
text: {
primary: '#21222D',
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)',
disabled: 'rgba(33,34,35,0.2)',
inverse: '#FFFFFF',

View File

@ -7,7 +7,7 @@ import {
import RAG_SOURCES from '@/constant/rag';
import { treeSx } from '@/constant/styles';
import { postApiV1Node, putApiV1NodeDetail } from '@/request/Node';
import { ConstsNodeAccessPerm, ConstsNodeRagInfoStatus } from '@/request/types';
import { ConstsNodeAccessPerm } from '@/request/types';
import { useAppSelector } from '@/store';
import { AppContext, updateTree } from '@/utils/drag';
import { handleMultiSelect, updateAllParentStatus } from '@/utils/tree';
@ -522,20 +522,15 @@ const TreeItem = React.forwardRef<
gap={1}
sx={{ flexShrink: 0, fontSize: 12 }}
>
{item.type === 2 &&
item.rag_status &&
![
ConstsNodeRagInfoStatus.NodeRagStatusBasicSucceeded,
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceSucceeded,
].includes(item.rag_status) && (
<Tooltip title={item.rag_message}>
<StyledTag
color={RAG_SOURCES[item.rag_status].color as any}
>
{RAG_SOURCES[item.rag_status].name}
</StyledTag>
</Tooltip>
)}
{item.type === 2 && item.rag_status && (
<Tooltip title={item.rag_message}>
<StyledTag
color={RAG_SOURCES[item.rag_status].color as any}
>
{RAG_SOURCES[item.rag_status].name}
</StyledTag>
</Tooltip>
)}
{item.status === 1 && (
<StyledTag color='error'></StyledTag>
)}

View File

@ -3,7 +3,6 @@ import { useURLSearchParams } from '@/hooks';
import { ConstsUserRole } from '@/request/types';
import { useAppDispatch, useAppSelector } from '@/store';
import { setKbC, setKbId } from '@/store/slices/config';
import custom from '@/themes/custom';
import { Ellipsis, Icon, message } from '@ctzhian/ui';
import {
Box,
@ -117,7 +116,7 @@ const KBSelect = () => {
borderRadius: '5px',
bgcolor: 'background.paper3',
'&:hover': {
bgcolor: custom.selectedMenuItemBgColor,
bgcolor: 'rgba(50,72,242,0.1)',
},
}}
fullWidth

View File

@ -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

View File

@ -83,7 +83,7 @@ export default function ContributePreviewModal(
? '新增'
: '修改'}
</Box>
<Box sx={{ fontSize: 14, color: 'text.auxiliary', fontWeight: 400 }}>
<Box sx={{ fontSize: 14, color: 'text.tertiary', fontWeight: 400 }}>
{dayjs(row?.created_at).fromNow()}
</Box>
</Stack>

View File

@ -61,7 +61,7 @@ const MarkdownPreviewModal = ({
? '新增'
: '修改'}
</Box>
<Box sx={{ fontSize: 14, color: 'text.auxiliary', fontWeight: 400 }}>
<Box sx={{ fontSize: 14, color: 'text.tertiary', fontWeight: 400 }}>
{dayjs(row?.created_at).fromNow()}
</Box>
</Stack>

View File

@ -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;

View File

@ -8,7 +8,11 @@ import {
} from '@/components/Drag/DragTree/TreeMenu';
import { useURLSearchParams } from '@/hooks';
import { getApiV1NodeList } from '@/request/Node';
import { ConstsCrawlerSource, DomainNodeListItemResp } from '@/request/types';
import {
ConstsCrawlerSource,
ConstsNodeRagInfoStatus,
DomainNodeListItemResp,
} from '@/request/types';
import { useAppDispatch, useAppSelector } from '@/store';
import { setIsRefreshDocList } from '@/store/slices/config';
import { addOpacityToColor } from '@/utils';
@ -32,6 +36,7 @@ import DocSearch from './component/DocSearch';
import DocStatus from './component/DocStatus';
import DocSummary from './component/DocSummary';
import MoveDocs from './component/MoveDocs';
import RagErrorReStart from './component/RagErrorReStart';
import Summary from './component/Summary';
const Content = () => {
@ -44,10 +49,15 @@ const Content = () => {
const search = searchParams.get('search') || '';
const [supportSelect, setBatchOpen] = useState(false);
const [ragErrorCount, setRagErrorCount] = useState(0);
const [ragErrorIds, setRagErrorIds] = useState<string[]>([]);
const [ragErrorOpen, setRagErrorOpen] = useState(false);
const [publish, setPublish] = useState({
// published: 0,
unpublished: 0,
});
const [publishIds, setPublishIds] = useState<string[]>([]);
const [publishOpen, setPublishOpen] = useState(false);
const [list, setList] = useState<DomainNodeListItemResp[]>([]);
const [selected, setSelected] = useState<string[]>([]);
const [data, setData] = useState<ITreeItem[]>([]);
@ -58,8 +68,6 @@ const Content = () => {
const [moreSummaryOpen, setMoreSummaryOpen] = useState(false);
const [moveOpen, setMoveOpen] = 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 [propertiesOpen, setPropertiesOpen] = useState(false);
const [isBatch, setIsBatch] = useState(false);
@ -119,6 +127,11 @@ const Content = () => {
setPublishIds([item.id]);
};
const handleRestudy = (item: ITreeItem) => {
setRagErrorOpen(true);
setRagErrorIds([item.id]);
};
const handleProperties = (item: ITreeItem) => {
setPropertiesOpen(true);
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
? [{ label: '重命名', key: 'rename', onClick: renameItem }]
: []),
@ -308,8 +334,17 @@ const Content = () => {
setList(res || []);
setPublish({
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 next = openIds.size
? reopenFolders(collapsedAll, openIds)
@ -386,6 +421,29 @@ const Content = () => {
</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 direction={'row'} alignItems={'center'} gap={2}>
<DocSearch />
@ -670,6 +728,15 @@ const Content = () => {
}}
refresh={getData}
/>
<RagErrorReStart
open={ragErrorOpen}
defaultSelected={ragErrorIds}
onClose={() => {
setRagErrorOpen(false);
setRagErrorIds([]);
}}
refresh={getData}
/>
<MoveDocs
open={moveOpen}
data={list}

View File

@ -15,8 +15,11 @@ import {
DomainCreateModelReq,
DomainGetProviderModelListReq,
DomainGetProviderModelListResp,
DomainModelModeSetting,
DomainPWResponse,
DomainResponse,
DomainSwitchModeReq,
DomainSwitchModeResp,
DomainUpdateModelReq,
GithubComChaitinPandaWikiDomainCheckModelReq,
GithubComChaitinPandaWikiDomainCheckModelResp,
@ -124,6 +127,32 @@ export const getApiV1ModelList = (params: RequestParams = {}) =>
...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
*
@ -153,3 +182,33 @@ export const postApiV1ModelProviderSupported = (
format: "json",
...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,
});

View File

@ -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,
});

View File

@ -10,6 +10,7 @@ export * from './Message'
export * from './Model'
export * from './Node'
export * from './NodePermission'
export * from './NodeRestudy'
export * from './Stat'
export * from './User'
export * from './types'

View File

@ -922,6 +922,15 @@ export interface DomainMetricsConfig {
type?: string;
}
export interface DomainModelModeSetting {
/** 百智云 API Key */
auto_mode_api_key?: string;
/** 自定义对话模型名称 */
chat_model?: string;
/** 模式: manual 或 auto */
mode?: string;
}
export interface DomainMoveNodeReq {
id: string;
kb_id: string;
@ -1195,6 +1204,18 @@ export interface DomainStatPageReq {
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 {
title?: string;
type?: string;
@ -1623,12 +1644,39 @@ export interface V1NodePermissionResp {
visitable_groups?: DomainNodeGroupDetail[];
}
export interface V1NodeRestudyReq {
kb_id?: string;
node_ids?: string[];
}
export type V1NodeRestudyResp = Record<string, any>;
export interface V1ResetPasswordReq {
id: string;
/** @minLength 8 */
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 {
conversation_count?: number;
ip_count?: number;

View File

@ -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;

View File

@ -1,4 +0,0 @@
export default {
selectPopupBoxShadow: '0px 10px 20px 0px rgba(54,59,76,0.2)',
selectedMenuItemBgColor: 'rgba(50,72,242,0.1)',
};

View File

@ -51,7 +51,7 @@ const dark = {
text: {
primary: '#fff',
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)',
slave: 'rgba(255,255,255,0.05)',
inverseAuxiliary: 'rgba(0,0,0,0.5)',

View File

@ -56,7 +56,7 @@ const light = {
text: {
primary: '#21222D',
secondary: 'rgba(33,34,35,0.7)',
auxiliary: 'rgba(33,34,35,0.5)',
tertiary: '#646a73',
slave: 'rgba(33,34,35,0.3)',
disabled: 'rgba(33,34,35,0.2)',
inverse: '#FFFFFF',

View File

@ -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;

View File

@ -25,7 +25,6 @@
"highlight.js": "^11.11.1",
"html-react-parser": "^5.2.5",
"html-to-image": "^1.11.13",
"import-in-the-middle": "^1.14.2",
"js-cookie": "^3.0.5",
"katex": "^0.16.22",
"markdown-it": "13.0.1",
@ -42,7 +41,6 @@
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"require-in-the-middle": "^7.5.2",
"uuid": "^11.1.0"
},
"devDependencies": {

View File

@ -1,7 +1,7 @@
import SSEClient from '@/utils/fetch';
import { Box, Divider, Stack } from '@mui/material';
import { Editor, useTiptap, UseTiptapReturn } from '@ctzhian/tiptap';
import { Modal } from '@ctzhian/ui';
import { Box, Divider, Stack } from '@mui/material';
import { useCallback, useEffect, useRef, useState } from 'react';
interface AIGenerateProps {
@ -126,7 +126,7 @@ const AIGenerate = ({
ml: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'text.auxiliary',
color: 'text.tertiary',
}}
>
@ -150,7 +150,7 @@ const AIGenerate = ({
ml: 1,
fontSize: 14,
fontWeight: 'bold',
color: 'text.auxiliary',
color: 'text.tertiary',
}}
>

View File

@ -1,10 +1,10 @@
'use client';
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 { Box, Button, IconButton, Skeleton, Stack } from '@mui/material';
import { Ellipsis, Icon, message } from '@ctzhian/ui';
import dayjs from 'dayjs';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useWrapContext } from '..';
interface HeaderProps {
@ -46,7 +46,7 @@ const Header = ({ edit, detail, handleSave }: HeaderProps) => {
onClick={() => setCatalogOpen(true)}
sx={{
cursor: 'pointer',
color: 'text.auxiliary',
color: 'text.tertiary',
':hover': {
color: 'text.primary',
},
@ -78,7 +78,7 @@ const Header = ({ edit, detail, handleSave }: HeaderProps) => {
direction={'row'}
alignItems={'center'}
gap={0.5}
sx={{ fontSize: 12, color: 'text.auxiliary' }}
sx={{ fontSize: 12, color: 'text.tertiary' }}
>
<IconBaocun sx={{ fontSize: 12 }} />
{nodeDetail?.updated_at ? (

View File

@ -1,7 +1,7 @@
'use client';
import { Box, Skeleton, Stack } from '@mui/material';
import { useTiptap } from '@ctzhian/tiptap';
import { Icon } from '@ctzhian/ui';
import { Box, Skeleton, Stack } from '@mui/material';
import { useState } from 'react';
import Header from './Header';
import Toolbar from './Toolbar';
@ -61,11 +61,11 @@ const LoadingEditorWrap = () => {
</Stack>
<Stack direction={'row'} alignItems={'center'} gap={2} sx={{ mb: 4 }}>
<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} />
</Stack>
<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} />
</Stack>
</Stack>

View File

@ -30,7 +30,7 @@ const HeadingIcon = [
const HeadingSx = [
{ 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' },
];
@ -117,7 +117,7 @@ const Toc = ({
sx={{
fontSize: 14,
fontWeight: 'bold',
color: 'text.auxiliary',
color: 'text.tertiary',
mb: 1,
p: 1,
pb: 0,

View File

@ -267,10 +267,10 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
gap={0.5}
sx={{
fontSize: 12,
color: 'text.auxiliary',
color: 'text.tertiary',
cursor: 'text',
':hover': {
color: 'text.auxiliary',
color: 'text.tertiary',
},
}}
>
@ -286,7 +286,7 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
direction={'row'}
alignItems={'center'}
gap={0.5}
sx={{ fontSize: 12, color: 'text.auxiliary' }}
sx={{ fontSize: 12, color: 'text.tertiary' }}
>
<IconZiti />
{characterCount}

Some files were not shown because too many files have changed in this diff Show More