mirror of https://github.com/chaitin/PandaWiki.git
Compare commits
No commits in common. "e9d30eb3d4e2d76fbbcdd5aa9286c652ca01b71b" and "63a4a964b14fcb3f4856be3c2b84150c5620d581" have entirely different histories.
e9d30eb3d4
...
63a4a964b1
|
|
@ -24,5 +24,4 @@ type ShareNodeDetailResp struct {
|
||||||
CreatorAccount string `json:"creator_account"`
|
CreatorAccount string `json:"creator_account"`
|
||||||
EditorAccount string `json:"editor_account"`
|
EditorAccount string `json:"editor_account"`
|
||||||
PublisherAccount string `json:"publisher_account"`
|
PublisherAccount string `json:"publisher_account"`
|
||||||
List []*domain.ShareNodeListItemResp `json:"list" gorm:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//go:build wireinject
|
//go:build wireinject
|
||||||
|
// +build wireinject
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//go:build wireinject
|
//go:build wireinject
|
||||||
|
// +build wireinject
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//go:build wireinject
|
//go:build wireinject
|
||||||
|
// +build wireinject
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3478,7 +3478,7 @@ const docTemplate = `{
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Widget"
|
"share_chat"
|
||||||
],
|
],
|
||||||
"summary": "ChatWidget",
|
"summary": "ChatWidget",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -3509,52 +3509,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/share/v1/chat/widget/search": {
|
|
||||||
"post": {
|
|
||||||
"description": "WidgetSearch",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Widget"
|
|
||||||
],
|
|
||||||
"summary": "WidgetSearch",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Comment",
|
|
||||||
"name": "request",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ChatSearchReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"$ref": "#/definitions/domain.ChatSearchResp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/share/v1/comment": {
|
"/share/v1/comment": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "CreateComment",
|
"description": "CreateComment",
|
||||||
|
|
@ -6361,9 +6315,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.MessageContent": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"domain.MessageFrom": {
|
"domain.MessageFrom": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -6765,9 +6716,6 @@ const docTemplate = `{
|
||||||
"stream": {
|
"stream": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"stream_options": {
|
|
||||||
"$ref": "#/definitions/domain.OpenAIStreamOptions"
|
|
||||||
},
|
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
|
@ -6890,7 +6838,7 @@ const docTemplate = `{
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
"$ref": "#/definitions/domain.MessageContent"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -6920,14 +6868,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.OpenAIStreamOptions": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"include_usage": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.OpenAITool": {
|
"domain.OpenAITool": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -7194,35 +7134,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.ShareNodeListItemResp": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"emoji": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"parent_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"$ref": "#/definitions/domain.NodePermissions"
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"$ref": "#/definitions/domain.NodeType"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.SimpleAuth": {
|
"domain.SimpleAuth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -7740,33 +7651,15 @@ const docTemplate = `{
|
||||||
"domain.WidgetBotSettings": {
|
"domain.WidgetBotSettings": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"btn_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"btn_logo": {
|
"btn_logo": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"btn_position": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"btn_style": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"btn_text": {
|
"btn_text": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"disclaimer": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"is_open": {
|
"is_open": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"modal_position": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"placeholder": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"recommend_node_ids": {
|
"recommend_node_ids": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
@ -7779,9 +7672,6 @@ const docTemplate = `{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_mode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"theme_mode": {
|
"theme_mode": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
@ -8650,12 +8540,6 @@ const docTemplate = `{
|
||||||
"kb_id": {
|
"kb_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"list": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.ShareNodeListItemResp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta": {
|
"meta": {
|
||||||
"$ref": "#/definitions/domain.NodeMeta"
|
"$ref": "#/definitions/domain.NodeMeta"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3471,7 +3471,7 @@
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Widget"
|
"share_chat"
|
||||||
],
|
],
|
||||||
"summary": "ChatWidget",
|
"summary": "ChatWidget",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -3502,52 +3502,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/share/v1/chat/widget/search": {
|
|
||||||
"post": {
|
|
||||||
"description": "WidgetSearch",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Widget"
|
|
||||||
],
|
|
||||||
"summary": "WidgetSearch",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Comment",
|
|
||||||
"name": "request",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ChatSearchReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"$ref": "#/definitions/domain.ChatSearchResp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/share/v1/comment": {
|
"/share/v1/comment": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "CreateComment",
|
"description": "CreateComment",
|
||||||
|
|
@ -6354,9 +6308,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.MessageContent": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"domain.MessageFrom": {
|
"domain.MessageFrom": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -6758,9 +6709,6 @@
|
||||||
"stream": {
|
"stream": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"stream_options": {
|
|
||||||
"$ref": "#/definitions/domain.OpenAIStreamOptions"
|
|
||||||
},
|
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
|
@ -6883,7 +6831,7 @@
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
"$ref": "#/definitions/domain.MessageContent"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -6913,14 +6861,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.OpenAIStreamOptions": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"include_usage": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.OpenAITool": {
|
"domain.OpenAITool": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -7187,35 +7127,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.ShareNodeListItemResp": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"emoji": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"parent_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"permissions": {
|
|
||||||
"$ref": "#/definitions/domain.NodePermissions"
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"$ref": "#/definitions/domain.NodeType"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.SimpleAuth": {
|
"domain.SimpleAuth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -7733,33 +7644,15 @@
|
||||||
"domain.WidgetBotSettings": {
|
"domain.WidgetBotSettings": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"btn_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"btn_logo": {
|
"btn_logo": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"btn_position": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"btn_style": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"btn_text": {
|
"btn_text": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"disclaimer": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"is_open": {
|
"is_open": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"modal_position": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"placeholder": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"recommend_node_ids": {
|
"recommend_node_ids": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
@ -7772,9 +7665,6 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"search_mode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"theme_mode": {
|
"theme_mode": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
@ -8643,12 +8533,6 @@
|
||||||
"kb_id": {
|
"kb_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"list": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/domain.ShareNodeListItemResp"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta": {
|
"meta": {
|
||||||
"$ref": "#/definitions/domain.NodeMeta"
|
"$ref": "#/definitions/domain.NodeMeta"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1614,8 +1614,6 @@ definitions:
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
domain.MessageContent:
|
|
||||||
type: object
|
|
||||||
domain.MessageFrom:
|
domain.MessageFrom:
|
||||||
enum:
|
enum:
|
||||||
- 1
|
- 1
|
||||||
|
|
@ -1877,8 +1875,6 @@ definitions:
|
||||||
type: array
|
type: array
|
||||||
stream:
|
stream:
|
||||||
type: boolean
|
type: boolean
|
||||||
stream_options:
|
|
||||||
$ref: '#/definitions/domain.OpenAIStreamOptions'
|
|
||||||
temperature:
|
temperature:
|
||||||
type: number
|
type: number
|
||||||
tool_choice:
|
tool_choice:
|
||||||
|
|
@ -1960,7 +1956,7 @@ definitions:
|
||||||
domain.OpenAIMessage:
|
domain.OpenAIMessage:
|
||||||
properties:
|
properties:
|
||||||
content:
|
content:
|
||||||
$ref: '#/definitions/domain.MessageContent'
|
type: string
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
role:
|
role:
|
||||||
|
|
@ -1981,11 +1977,6 @@ definitions:
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
domain.OpenAIStreamOptions:
|
|
||||||
properties:
|
|
||||||
include_usage:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
domain.OpenAITool:
|
domain.OpenAITool:
|
||||||
properties:
|
properties:
|
||||||
function:
|
function:
|
||||||
|
|
@ -2159,25 +2150,6 @@ definitions:
|
||||||
role:
|
role:
|
||||||
$ref: '#/definitions/schema.RoleType'
|
$ref: '#/definitions/schema.RoleType'
|
||||||
type: object
|
type: object
|
||||||
domain.ShareNodeListItemResp:
|
|
||||||
properties:
|
|
||||||
emoji:
|
|
||||||
type: string
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
parent_id:
|
|
||||||
type: string
|
|
||||||
permissions:
|
|
||||||
$ref: '#/definitions/domain.NodePermissions'
|
|
||||||
position:
|
|
||||||
type: number
|
|
||||||
type:
|
|
||||||
$ref: '#/definitions/domain.NodeType'
|
|
||||||
updated_at:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
domain.SimpleAuth:
|
domain.SimpleAuth:
|
||||||
properties:
|
properties:
|
||||||
enabled:
|
enabled:
|
||||||
|
|
@ -2520,24 +2492,12 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
domain.WidgetBotSettings:
|
domain.WidgetBotSettings:
|
||||||
properties:
|
properties:
|
||||||
btn_id:
|
|
||||||
type: string
|
|
||||||
btn_logo:
|
btn_logo:
|
||||||
type: string
|
type: string
|
||||||
btn_position:
|
|
||||||
type: string
|
|
||||||
btn_style:
|
|
||||||
type: string
|
|
||||||
btn_text:
|
btn_text:
|
||||||
type: string
|
type: string
|
||||||
disclaimer:
|
|
||||||
type: string
|
|
||||||
is_open:
|
is_open:
|
||||||
type: boolean
|
type: boolean
|
||||||
modal_position:
|
|
||||||
type: string
|
|
||||||
placeholder:
|
|
||||||
type: string
|
|
||||||
recommend_node_ids:
|
recommend_node_ids:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -2546,8 +2506,6 @@ definitions:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
search_mode:
|
|
||||||
type: string
|
|
||||||
theme_mode:
|
theme_mode:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -3121,10 +3079,6 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
kb_id:
|
kb_id:
|
||||||
type: string
|
type: string
|
||||||
list:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/domain.ShareNodeListItemResp'
|
|
||||||
type: array
|
|
||||||
meta:
|
meta:
|
||||||
$ref: '#/definitions/domain.NodeMeta'
|
$ref: '#/definitions/domain.NodeMeta'
|
||||||
name:
|
name:
|
||||||
|
|
@ -5346,34 +5300,7 @@ paths:
|
||||||
$ref: '#/definitions/domain.Response'
|
$ref: '#/definitions/domain.Response'
|
||||||
summary: ChatWidget
|
summary: ChatWidget
|
||||||
tags:
|
tags:
|
||||||
- Widget
|
- share_chat
|
||||||
/share/v1/chat/widget/search:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: WidgetSearch
|
|
||||||
parameters:
|
|
||||||
- description: Comment
|
|
||||||
in: body
|
|
||||||
name: request
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ChatSearchReq'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/domain.Response'
|
|
||||||
- properties:
|
|
||||||
data:
|
|
||||||
$ref: '#/definitions/domain.ChatSearchResp'
|
|
||||||
type: object
|
|
||||||
summary: WidgetSearch
|
|
||||||
tags:
|
|
||||||
- Widget
|
|
||||||
/share/v1/comment:
|
/share/v1/comment:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -405,13 +405,6 @@ type WidgetBotSettings struct {
|
||||||
BtnLogo string `json:"btn_logo,omitempty"`
|
BtnLogo string `json:"btn_logo,omitempty"`
|
||||||
RecommendQuestions []string `json:"recommend_questions,omitempty"`
|
RecommendQuestions []string `json:"recommend_questions,omitempty"`
|
||||||
RecommendNodeIDs []string `json:"recommend_node_ids,omitempty"`
|
RecommendNodeIDs []string `json:"recommend_node_ids,omitempty"`
|
||||||
BtnStyle string `json:"btn_style,omitempty"`
|
|
||||||
BtnID string `json:"btn_id,omitempty"`
|
|
||||||
BtnPosition string `json:"btn_position,omitempty"`
|
|
||||||
ModalPosition string `json:"modal_position,omitempty"`
|
|
||||||
SearchMode string `json:"search_mode,omitempty"`
|
|
||||||
Placeholder string `json:"placeholder,omitempty"`
|
|
||||||
Disclaimer string `json:"disclaimer,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BrandGroup struct {
|
type BrandGroup struct {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,10 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OpenAI API 请求结构体
|
// OpenAI API 请求结构体
|
||||||
type OpenAICompletionsRequest struct {
|
type OpenAICompletionsRequest struct {
|
||||||
Model string `json:"model" validate:"required"`
|
Model string `json:"model" validate:"required"`
|
||||||
Messages []OpenAIMessage `json:"messages" validate:"required"`
|
Messages []OpenAIMessage `json:"messages" validate:"required"`
|
||||||
Stream bool `json:"stream,omitempty"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
StreamOptions *OpenAIStreamOptions `json:"stream_options,omitempty"`
|
|
||||||
Temperature *float64 `json:"temperature,omitempty"`
|
Temperature *float64 `json:"temperature,omitempty"`
|
||||||
MaxTokens *int `json:"max_tokens,omitempty"`
|
MaxTokens *int `json:"max_tokens,omitempty"`
|
||||||
TopP *float64 `json:"top_p,omitempty"`
|
TopP *float64 `json:"top_p,omitempty"`
|
||||||
|
|
@ -24,95 +17,9 @@ type OpenAICompletionsRequest struct {
|
||||||
ResponseFormat *OpenAIResponseFormat `json:"response_format,omitempty"`
|
ResponseFormat *OpenAIResponseFormat `json:"response_format,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIStreamOptions struct {
|
|
||||||
IncludeUsage bool `json:"include_usage,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageContent 支持字符串或内容数组
|
|
||||||
type MessageContent struct {
|
|
||||||
isString bool
|
|
||||||
strValue string
|
|
||||||
arrValue []OpenAIContentPart
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenAIContentPart 表示内容数组中的单个元素
|
|
||||||
type OpenAIContentPart struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Text string `json:"text,omitempty"`
|
|
||||||
ImageURL *OpenAIContentPartURL `json:"image_url,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenAIContentPartURL represents the image_url field in content parts
|
|
||||||
type OpenAIContentPartURL struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON 自定义解析,支持 string 或 array 格式
|
|
||||||
func (mc *MessageContent) UnmarshalJSON(data []byte) error {
|
|
||||||
// 尝试解析为字符串
|
|
||||||
var str string
|
|
||||||
if err := json.Unmarshal(data, &str); err == nil {
|
|
||||||
mc.isString = true
|
|
||||||
mc.strValue = str
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试解析为数组
|
|
||||||
var arr []OpenAIContentPart
|
|
||||||
if err := json.Unmarshal(data, &arr); err == nil {
|
|
||||||
mc.isString = false
|
|
||||||
mc.arrValue = arr
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("content must be string or array")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON 自定义序列化
|
|
||||||
func (mc *MessageContent) MarshalJSON() ([]byte, error) {
|
|
||||||
if mc.isString {
|
|
||||||
return json.Marshal(mc.strValue)
|
|
||||||
}
|
|
||||||
return json.Marshal(mc.arrValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStringContent 创建字符串类型的 MessageContent
|
|
||||||
func NewStringContent(s string) *MessageContent {
|
|
||||||
return &MessageContent{
|
|
||||||
isString: true,
|
|
||||||
strValue: s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewArrayContent 创建数组类型的 MessageContent
|
|
||||||
func NewArrayContent(parts []OpenAIContentPart) *MessageContent {
|
|
||||||
return &MessageContent{
|
|
||||||
isString: false,
|
|
||||||
arrValue: parts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String 获取文本内容
|
|
||||||
func (mc *MessageContent) String() string {
|
|
||||||
if mc.isString {
|
|
||||||
return mc.strValue
|
|
||||||
}
|
|
||||||
// 从数组中提取文本
|
|
||||||
var builder strings.Builder
|
|
||||||
for _, part := range mc.arrValue {
|
|
||||||
if part.Type == "text" {
|
|
||||||
if builder.Len() > 0 && part.Text != "" {
|
|
||||||
builder.WriteString(" ")
|
|
||||||
}
|
|
||||||
builder.WriteString(part.Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpenAIMessage struct {
|
type OpenAIMessage struct {
|
||||||
Role string `json:"role" validate:"required"`
|
Role string `json:"role" validate:"required"`
|
||||||
Content *MessageContent `json:"content,omitempty"`
|
Content string `json:"content,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
|
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
|
||||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
|
|
@ -183,7 +90,6 @@ type OpenAIStreamResponse struct {
|
||||||
Created int64 `json:"created"`
|
Created int64 `json:"created"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Choices []OpenAIStreamChoice `json:"choices"`
|
Choices []OpenAIStreamChoice `json:"choices"`
|
||||||
Usage *OpenAIUsage `json:"usage,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIStreamChoice struct {
|
type OpenAIStreamChoice struct {
|
||||||
|
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMessageContent_UnmarshalJSON_String(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
json string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{"simple string", `"hello"`, "hello"},
|
|
||||||
{"with quotes", `"say \"hello\""`, `say "hello"`},
|
|
||||||
{"with newline", `"line1\nline2"`, "line1\nline2"},
|
|
||||||
{"empty string", `""`, ""},
|
|
||||||
{"unicode", `"你好 🌍"`, "你好 🌍"},
|
|
||||||
{"special chars", `"Hello \"World\"\nNew Line\tTab"`, "Hello \"World\"\nNew Line\tTab"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
var mc MessageContent
|
|
||||||
err := json.Unmarshal([]byte(tt.json), &mc)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.expected, mc.String())
|
|
||||||
assert.True(t, mc.isString)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_UnmarshalJSON_Array(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
json string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"single text part",
|
|
||||||
`[{"type":"text","text":"Hello"}]`,
|
|
||||||
"Hello",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"multiple text parts",
|
|
||||||
`[{"type":"text","text":"Hello"},{"type":"text","text":"World"}]`,
|
|
||||||
"Hello World",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mixed types with image",
|
|
||||||
`[{"type":"text","text":"Look at this"},{"type":"image_url","image_url":{"url":"https://example.com/img.png"}},{"type":"text","text":"image"}]`,
|
|
||||||
"Look at this image",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"empty array",
|
|
||||||
`[]`,
|
|
||||||
"",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
var mc MessageContent
|
|
||||||
err := json.Unmarshal([]byte(tt.json), &mc)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.expected, mc.String())
|
|
||||||
assert.False(t, mc.isString)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_UnmarshalJSON_Invalid(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
json string
|
|
||||||
}{
|
|
||||||
{"number", `123`},
|
|
||||||
{"boolean", `true`},
|
|
||||||
{"object", `{"key":"value"}`},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
var mc MessageContent
|
|
||||||
err := json.Unmarshal([]byte(tt.json), &mc)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "content must be string or array")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_UnmarshalJSON_Null(t *testing.T) {
|
|
||||||
var mc *MessageContent
|
|
||||||
err := json.Unmarshal([]byte(`null`), &mc)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Nil(t, mc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_MarshalJSON_String(t *testing.T) {
|
|
||||||
mc := NewStringContent("Hello World")
|
|
||||||
data, err := json.Marshal(mc)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, `"Hello World"`, string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_MarshalJSON_Array(t *testing.T) {
|
|
||||||
mc := NewArrayContent([]OpenAIContentPart{
|
|
||||||
{Type: "text", Text: "Hello"},
|
|
||||||
{Type: "text", Text: "World"},
|
|
||||||
})
|
|
||||||
data, err := json.Marshal(mc)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.JSONEq(t, `[{"type":"text","text":"Hello"},{"type":"text","text":"World"}]`, string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_Roundtrip_String(t *testing.T) {
|
|
||||||
original := NewStringContent("Test message with \"quotes\" and \nnewlines")
|
|
||||||
|
|
||||||
// Marshal
|
|
||||||
data, err := json.Marshal(original)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Unmarshal
|
|
||||||
var decoded MessageContent
|
|
||||||
err = json.Unmarshal(data, &decoded)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
assert.Equal(t, original.String(), decoded.String())
|
|
||||||
assert.Equal(t, original.isString, decoded.isString)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_Roundtrip_Array(t *testing.T) {
|
|
||||||
parts := []OpenAIContentPart{
|
|
||||||
{Type: "text", Text: "Part 1"},
|
|
||||||
{Type: "text", Text: "Part 2"},
|
|
||||||
}
|
|
||||||
original := NewArrayContent(parts)
|
|
||||||
|
|
||||||
// Marshal
|
|
||||||
data, err := json.Marshal(original)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Unmarshal
|
|
||||||
var decoded MessageContent
|
|
||||||
err = json.Unmarshal(data, &decoded)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
assert.Equal(t, original.String(), decoded.String())
|
|
||||||
assert.Equal(t, original.isString, decoded.isString)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewStringContent(t *testing.T) {
|
|
||||||
mc := NewStringContent("test")
|
|
||||||
assert.NotNil(t, mc)
|
|
||||||
assert.True(t, mc.isString)
|
|
||||||
assert.Equal(t, "test", mc.strValue)
|
|
||||||
assert.Equal(t, "test", mc.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewArrayContent(t *testing.T) {
|
|
||||||
parts := []OpenAIContentPart{
|
|
||||||
{Type: "text", Text: "Hello"},
|
|
||||||
}
|
|
||||||
mc := NewArrayContent(parts)
|
|
||||||
assert.NotNil(t, mc)
|
|
||||||
assert.False(t, mc.isString)
|
|
||||||
assert.Equal(t, parts, mc.arrValue)
|
|
||||||
assert.Equal(t, "Hello", mc.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_String_EmptyArray(t *testing.T) {
|
|
||||||
mc := NewArrayContent([]OpenAIContentPart{})
|
|
||||||
assert.Equal(t, "", mc.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMessageContent_String_NoTextParts(t *testing.T) {
|
|
||||||
mc := NewArrayContent([]OpenAIContentPart{
|
|
||||||
{Type: "image_url", Text: ""},
|
|
||||||
})
|
|
||||||
assert.Equal(t, "", mc.String())
|
|
||||||
}
|
|
||||||
|
|
@ -49,7 +49,6 @@ require (
|
||||||
github.com/sbzhu/weworkapi_golang v0.0.0-20210525081115-1799804a7c8d
|
github.com/sbzhu/weworkapi_golang v0.0.0-20210525081115-1799804a7c8d
|
||||||
github.com/silenceper/wechat/v2 v2.1.9
|
github.com/silenceper/wechat/v2 v2.1.9
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/stretchr/testify v1.10.0
|
|
||||||
github.com/swaggo/echo-swagger v1.4.1
|
github.com/swaggo/echo-swagger v1.4.1
|
||||||
github.com/swaggo/swag v1.16.5
|
github.com/swaggo/swag v1.16.5
|
||||||
github.com/tidwall/gjson v1.14.1
|
github.com/tidwall/gjson v1.14.1
|
||||||
|
|
@ -99,7 +98,6 @@ require (
|
||||||
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250710065240-482d48888f25 // indirect
|
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250710065240-482d48888f25 // indirect
|
||||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250626133421-3c142631c961 // indirect
|
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250626133421-3c142631c961 // indirect
|
||||||
github.com/cohesion-org/deepseek-go v1.2.8 // indirect
|
github.com/cohesion-org/deepseek-go v1.2.8 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
|
@ -167,7 +165,6 @@ require (
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ func NewShareChatHandler(
|
||||||
share.POST("/search", h.ChatSearch, h.ShareAuthMiddleware.Authorize)
|
share.POST("/search", h.ChatSearch, h.ShareAuthMiddleware.Authorize)
|
||||||
share.POST("/completions", h.ChatCompletions)
|
share.POST("/completions", h.ChatCompletions)
|
||||||
share.POST("/widget", h.ChatWidget)
|
share.POST("/widget", h.ChatWidget)
|
||||||
share.POST("/widget/search", h.WidgetSearch)
|
|
||||||
share.POST("/feedback", h.FeedBack)
|
share.POST("/feedback", h.FeedBack)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +131,7 @@ func (h *ShareChatHandler) ChatMessage(c echo.Context) error {
|
||||||
//
|
//
|
||||||
// @Summary ChatWidget
|
// @Summary ChatWidget
|
||||||
// @Description ChatWidget
|
// @Description ChatWidget
|
||||||
// @Tags Widget
|
// @Tags share_chat
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param app_type query string true "app type"
|
// @Param app_type query string true "app type"
|
||||||
|
|
@ -269,9 +268,7 @@ func (h *ShareChatHandler) ChatCompletions(c echo.Context) error {
|
||||||
var lastUserMessage string
|
var lastUserMessage string
|
||||||
for i := len(req.Messages) - 1; i >= 0; i-- {
|
for i := len(req.Messages) - 1; i >= 0; i-- {
|
||||||
if req.Messages[i].Role == "user" {
|
if req.Messages[i].Role == "user" {
|
||||||
if req.Messages[i].Content != nil {
|
lastUserMessage = req.Messages[i].Content
|
||||||
lastUserMessage = req.Messages[i].Content.String()
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -348,12 +345,11 @@ func (h *ShareChatHandler) handleOpenAIStreamResponse(c echo.Context, eventCh <-
|
||||||
Index: 0,
|
Index: 0,
|
||||||
Delta: domain.OpenAIMessage{
|
Delta: domain.OpenAIMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: domain.NewStringContent(event.Content),
|
Content: event.Content,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.writeOpenAIStreamEvent(c, streamResp); err != nil {
|
if err := h.writeOpenAIStreamEvent(c, streamResp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -401,7 +397,7 @@ func (h *ShareChatHandler) handleOpenAINonStreamResponse(c echo.Context, eventCh
|
||||||
Index: 0,
|
Index: 0,
|
||||||
Message: domain.OpenAIMessage{
|
Message: domain.OpenAIMessage{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: domain.NewStringContent(content),
|
Content: content,
|
||||||
},
|
},
|
||||||
FinishReason: "stop",
|
FinishReason: "stop",
|
||||||
},
|
},
|
||||||
|
|
@ -445,7 +441,7 @@ func stringPtr(s string) *string {
|
||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatSearch searches chat messages in shared knowledge base
|
// ChatMessage chat search
|
||||||
//
|
//
|
||||||
// @Summary ChatSearch
|
// @Summary ChatSearch
|
||||||
// @Description ChatSearch
|
// @Description ChatSearch
|
||||||
|
|
@ -488,43 +484,3 @@ func (h *ShareChatHandler) ChatSearch(c echo.Context) error {
|
||||||
}
|
}
|
||||||
return h.NewResponseWithData(c, resp)
|
return h.NewResponseWithData(c, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WidgetSearch
|
|
||||||
//
|
|
||||||
// @Summary WidgetSearch
|
|
||||||
// @Description WidgetSearch
|
|
||||||
// @Tags Widget
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param request body domain.ChatSearchReq true "Comment"
|
|
||||||
// @Success 200 {object} domain.Response{data=domain.ChatSearchResp}
|
|
||||||
// @Router /share/v1/chat/widget/search [post]
|
|
||||||
func (h *ShareChatHandler) WidgetSearch(c echo.Context) error {
|
|
||||||
var req domain.ChatSearchReq
|
|
||||||
if err := c.Bind(&req); err != nil {
|
|
||||||
return h.NewResponseWithError(c, "parse request failed", err)
|
|
||||||
}
|
|
||||||
req.KBID = c.Request().Header.Get("X-KB-ID")
|
|
||||||
if err := c.Validate(&req); err != nil {
|
|
||||||
return h.NewResponseWithError(c, "validate request failed", err)
|
|
||||||
}
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
|
|
||||||
// validate widget info
|
|
||||||
widgetAppInfo, err := h.appUsecase.GetWidgetAppInfo(c.Request().Context(), req.KBID)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error("get widget app info failed", log.Error(err))
|
|
||||||
return h.sendErrMsg(c, "get app info error")
|
|
||||||
}
|
|
||||||
if !widgetAppInfo.Settings.WidgetBotSettings.IsOpen {
|
|
||||||
return h.sendErrMsg(c, "widget is not open")
|
|
||||||
}
|
|
||||||
|
|
||||||
req.RemoteIP = c.RealIP()
|
|
||||||
|
|
||||||
resp, err := h.chatUsecase.Search(ctx, &req)
|
|
||||||
if err != nil {
|
|
||||||
return h.NewResponseWithError(c, "failed to search docs", err)
|
|
||||||
}
|
|
||||||
return h.NewResponseWithData(c, resp)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -91,15 +91,5 @@ func (h *ShareNodeHandler) GetNodeDetail(c echo.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewResponseWithError(c, "failed to get node detail", err)
|
return h.NewResponseWithError(c, "failed to get node detail", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the node is a folder, return the list of child nodes
|
|
||||||
if node.Type == domain.NodeTypeFolder {
|
|
||||||
childNodes, err := h.usecase.GetNodeReleaseListByParentID(c.Request().Context(), kbID, id, domain.GetAuthID(c))
|
|
||||||
if err != nil {
|
|
||||||
return h.NewResponseWithError(c, "failed to get child nodes", err)
|
|
||||||
}
|
|
||||||
node.List = childNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.NewResponseWithData(c, node)
|
return h.NewResponseWithData(c, node)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit bb1b17dd5c7d72d40f6a1198b1604f4d3c44116e
|
Subproject commit bcf7e0f0bedb18f43cf36463ddb45ace6c1dbab9
|
||||||
|
|
@ -350,56 +350,6 @@ func (u *NodeUsecase) GetNodeReleaseListByKBID(ctx context.Context, kbID string,
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *NodeUsecase) GetNodeReleaseListByParentID(ctx context.Context, kbID, parentID string, authId uint) ([]*domain.ShareNodeListItemResp, error) {
|
|
||||||
// 一次性查询所有节点
|
|
||||||
allNodes, err := u.nodeRepo.GetNodeReleaseListByKBID(ctx, kbID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeGroupIds, err := u.GetNodeIdsByAuthId(ctx, authId, consts.NodePermNameVisible)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先过滤权限
|
|
||||||
visibleNodes := make([]*domain.ShareNodeListItemResp, 0)
|
|
||||||
for i, node := range allNodes {
|
|
||||||
switch node.Permissions.Visible {
|
|
||||||
case consts.NodeAccessPermOpen:
|
|
||||||
visibleNodes = append(visibleNodes, allNodes[i])
|
|
||||||
case consts.NodeAccessPermPartial:
|
|
||||||
if slices.Contains(nodeGroupIds, node.ID) {
|
|
||||||
visibleNodes = append(visibleNodes, allNodes[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建父子关系映射
|
|
||||||
childrenMap := make(map[string][]*domain.ShareNodeListItemResp)
|
|
||||||
for _, node := range visibleNodes {
|
|
||||||
childrenMap[node.ParentID] = append(childrenMap[node.ParentID], node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 递归收集所有后代节点
|
|
||||||
result := make([]*domain.ShareNodeListItemResp, 0)
|
|
||||||
u.collectDescendants(parentID, childrenMap, &result)
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectDescendants 递归收集所有后代节点
|
|
||||||
func (u *NodeUsecase) collectDescendants(parentID string, childrenMap map[string][]*domain.ShareNodeListItemResp, result *[]*domain.ShareNodeListItemResp) {
|
|
||||||
children := childrenMap[parentID]
|
|
||||||
for _, child := range children {
|
|
||||||
*result = append(*result, child)
|
|
||||||
// 如果是文件夹,递归收集其子节点
|
|
||||||
if child.Type == domain.NodeTypeFolder {
|
|
||||||
u.collectDescendants(child.ID, childrenMap, result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *NodeUsecase) GetNodeIdsByAuthId(ctx context.Context, authId uint, PermName consts.NodePermName) ([]string, error) {
|
func (u *NodeUsecase) GetNodeIdsByAuthId(ctx context.Context, authId uint, PermName consts.NodePermName) ([]string, error) {
|
||||||
authGroups, err := u.authRepo.GetAuthGroupWithParentsByAuthId(ctx, authId)
|
authGroups, err := u.authRepo.GetAuthGroupWithParentsByAuthId(ctx, authId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -588,7 +588,6 @@ export type ChatConversationItem = {
|
||||||
export type ChatConversationPair = {
|
export type ChatConversationPair = {
|
||||||
user: string;
|
user: string;
|
||||||
assistant: string;
|
assistant: string;
|
||||||
thinking_content: string;
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
info: {
|
info: {
|
||||||
feedback_content: string;
|
feedback_content: string;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -262,7 +262,7 @@ const MemberAdd = ({
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
sx: {
|
sx: {
|
||||||
'.Mui-disabled': {
|
'.Mui-disabled': {
|
||||||
opacity: '1 !important',
|
opacity: 1,
|
||||||
color: 'text.disabled',
|
color: 'text.disabled',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import { ChatConversationPair } from '@/api';
|
||||||
import { getApiV1ConversationDetail } from '@/request/Conversation';
|
import { getApiV1ConversationDetail } from '@/request/Conversation';
|
||||||
import { DomainConversationDetailResp } from '@/request/types';
|
import { DomainConversationDetailResp } from '@/request/types';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import MarkDown from '@/components/MarkDown';
|
import MarkDown from '@/components/MarkDown';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
|
|
@ -13,169 +13,10 @@ import {
|
||||||
Box,
|
Box,
|
||||||
Stack,
|
Stack,
|
||||||
useTheme,
|
useTheme,
|
||||||
styled,
|
|
||||||
alpha,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const handleThinkingContent = (content: string) => {
|
|
||||||
const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|$)/g;
|
|
||||||
const thinkMatches = [];
|
|
||||||
let match;
|
|
||||||
while ((match = thinkRegex.exec(content)) !== null) {
|
|
||||||
thinkMatches.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let answerContent = content.replace(/<think>[\s\S]*?<\/think>/g, '');
|
|
||||||
answerContent = answerContent.replace(/<think>[\s\S]*$/, '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
thinkingContent: thinkMatches.join(''),
|
|
||||||
answerContent: answerContent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StyledConversationItem = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 聊天气泡相关组件
|
|
||||||
export const StyledUserBubble = styled(Box)(({ theme }) => ({
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
maxWidth: '75%',
|
|
||||||
padding: theme.spacing(1, 2),
|
|
||||||
borderRadius: '10px 10px 0px 10px',
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
color: theme.palette.primary.contrastText,
|
|
||||||
fontSize: 14,
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
width: '100%',
|
|
||||||
gap: theme.spacing(3),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledAiBubbleContent = styled(Box)(() => ({
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 对话相关组件
|
|
||||||
export const StyledAccordion = styled(Accordion)(() => ({
|
|
||||||
padding: 0,
|
|
||||||
border: 'none',
|
|
||||||
'&:before': {
|
|
||||||
content: '""',
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
background: 'transparent',
|
|
||||||
backgroundImage: 'none',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
paddingRight: theme.spacing(2),
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
paddingBottom: theme.spacing(1),
|
|
||||||
userSelect: 'text',
|
|
||||||
borderRadius: '10px',
|
|
||||||
backgroundColor: theme.palette.background.paper3,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
borderTop: 'none',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledQuestionText = styled(Box)(() => ({
|
|
||||||
fontWeight: '700',
|
|
||||||
fontSize: 16,
|
|
||||||
lineHeight: '24px',
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 搜索结果相关组件
|
|
||||||
export const StyledChunkAccordion = styled(Accordion)(({ theme }) => ({
|
|
||||||
backgroundImage: 'none',
|
|
||||||
background: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
padding: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledChunkAccordionSummary = styled(AccordionSummary)(
|
|
||||||
({ theme }) => ({
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
'.MuiAccordionSummary-content': {
|
|
||||||
flexGrow: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledChunkAccordionDetails = styled(AccordionDetails)(
|
|
||||||
({ theme }) => ({
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
borderTop: 'none',
|
|
||||||
borderLeft: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledChunkItem = styled(Box)(({ theme }) => ({
|
|
||||||
cursor: 'pointer',
|
|
||||||
'&:hover': {
|
|
||||||
'.hover-primary': {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 思考过程相关组件
|
|
||||||
export const StyledThinkingAccordion = styled(Accordion)(({ theme }) => ({
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
padding: 0,
|
|
||||||
paddingBottom: theme.spacing(2),
|
|
||||||
'&:before': {
|
|
||||||
content: '""',
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledThinkingAccordionSummary = styled(AccordionSummary)(
|
|
||||||
({ theme }) => ({
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
'.MuiAccordionSummary-content': {
|
|
||||||
flexGrow: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledThinkingAccordionDetails = styled(AccordionDetails)(
|
|
||||||
({ theme }) => ({
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
borderTop: 'none',
|
|
||||||
borderLeft: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
'.markdown-body': {
|
|
||||||
opacity: 0.75,
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const Detail = ({
|
const Detail = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
|
|
@ -214,11 +55,7 @@ const Detail = ({
|
||||||
};
|
};
|
||||||
} else if (message.role === 'assistant') {
|
} else if (message.role === 'assistant') {
|
||||||
if (currentPair.user) {
|
if (currentPair.user) {
|
||||||
const { thinkingContent, answerContent } = handleThinkingContent(
|
currentPair.assistant = message.content;
|
||||||
message.content || '',
|
|
||||||
);
|
|
||||||
currentPair.assistant = answerContent;
|
|
||||||
currentPair.thinking_content = thinkingContent;
|
|
||||||
currentPair.created_at = message.created_at;
|
currentPair.created_at = message.created_at;
|
||||||
// @ts-expect-error 类型不兼容
|
// @ts-expect-error 类型不兼容
|
||||||
currentPair.info = message.info;
|
currentPair.info = message.info;
|
||||||
|
|
@ -330,43 +167,26 @@ const Detail = ({
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
{conversations &&
|
{conversations &&
|
||||||
conversations.map((item, index) => (
|
conversations.map((item, index) => (
|
||||||
<StyledConversationItem key={index}>
|
<Box key={index}>
|
||||||
{/* 用户问题气泡 - 右对齐 */}
|
<Accordion defaultExpanded={true}>
|
||||||
<StyledUserBubble>{item.user}</StyledUserBubble>
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
||||||
{/* AI回答气泡 - 左对齐 */}
|
sx={{
|
||||||
<StyledAiBubble>
|
userSelect: 'text',
|
||||||
{/* 思考过程 */}
|
backgroundColor: 'background.paper3',
|
||||||
{!!item.thinking_content && (
|
fontSize: '18px',
|
||||||
<StyledThinkingAccordion defaultExpanded>
|
fontWeight: 'bold',
|
||||||
<StyledThinkingAccordionSummary
|
}}
|
||||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
|
||||||
>
|
>
|
||||||
<Stack direction='row' alignItems='center' gap={1}>
|
{item.user}
|
||||||
<Typography
|
</AccordionSummary>
|
||||||
variant='body2'
|
<AccordionDetails>
|
||||||
sx={theme => ({
|
<MarkDown
|
||||||
fontSize: 12,
|
content={item.assistant || '未查询到回答内容'}
|
||||||
color: alpha(theme.palette.text.primary, 0.5),
|
/>
|
||||||
})}
|
</AccordionDetails>
|
||||||
>
|
</Accordion>
|
||||||
已思考
|
</Box>
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</StyledThinkingAccordionSummary>
|
|
||||||
|
|
||||||
<StyledThinkingAccordionDetails>
|
|
||||||
<MarkDown content={item.thinking_content || ''} />
|
|
||||||
</StyledThinkingAccordionDetails>
|
|
||||||
</StyledThinkingAccordion>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* AI回答内容 */}
|
|
||||||
<StyledAiBubbleContent>
|
|
||||||
<MarkDown content={item.assistant} />
|
|
||||||
</StyledAiBubbleContent>
|
|
||||||
</StyledAiBubble>
|
|
||||||
</StyledConversationItem>
|
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -446,19 +446,15 @@ const Content = () => {
|
||||||
>
|
>
|
||||||
{ragReStartCount} 个文档未学习,
|
{ragReStartCount} 个文档未学习,
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonBase
|
<Button
|
||||||
disableRipple
|
size='small'
|
||||||
sx={{
|
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 400,
|
|
||||||
color: 'primary.main',
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRagOpen(true);
|
setRagOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
去学习
|
去学习
|
||||||
</ButtonBase>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,14 @@ import { getApiV1ConversationMessageDetail } from '@/request';
|
||||||
import MarkDown from '@/components/MarkDown';
|
import MarkDown from '@/components/MarkDown';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import { Box, Stack, Typography, alpha } from '@mui/material';
|
|
||||||
import { Ellipsis, Modal } from '@ctzhian/ui';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import {
|
import {
|
||||||
StyledConversationItem,
|
Accordion,
|
||||||
StyledUserBubble,
|
AccordionDetails,
|
||||||
StyledAiBubble,
|
AccordionSummary,
|
||||||
StyledThinkingAccordion,
|
Box,
|
||||||
StyledThinkingAccordionSummary,
|
} from '@mui/material';
|
||||||
StyledThinkingAccordionDetails,
|
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
||||||
StyledAiBubbleContent,
|
import { useEffect, useState } from 'react';
|
||||||
} from '../conversation/Detail';
|
|
||||||
|
|
||||||
const Detail = ({
|
const Detail = ({
|
||||||
id,
|
id,
|
||||||
|
|
@ -40,7 +36,6 @@ const Detail = ({
|
||||||
user: data.question,
|
user: data.question,
|
||||||
assistant: res.content!,
|
assistant: res.content!,
|
||||||
created_at: res.created_at!,
|
created_at: res.created_at!,
|
||||||
thinking_content: '',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -67,43 +62,24 @@ const Detail = ({
|
||||||
>
|
>
|
||||||
<Box sx={{ fontSize: 14 }}>
|
<Box sx={{ fontSize: 14 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<StyledConversationItem>
|
<Accordion defaultExpanded={true}>
|
||||||
{/* 用户问题气泡 - 右对齐 */}
|
<AccordionSummary
|
||||||
<StyledUserBubble>{conversations?.user}</StyledUserBubble>
|
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
||||||
|
sx={{
|
||||||
{/* AI回答气泡 - 左对齐 */}
|
userSelect: 'text',
|
||||||
<StyledAiBubble>
|
backgroundColor: 'background.paper3',
|
||||||
{/* 思考过程 */}
|
fontSize: '18px',
|
||||||
{!!conversations?.thinking_content && (
|
fontWeight: 'bold',
|
||||||
<StyledThinkingAccordion defaultExpanded>
|
}}
|
||||||
<StyledThinkingAccordionSummary
|
|
||||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
|
||||||
>
|
>
|
||||||
<Stack direction='row' alignItems='center' gap={1}>
|
{conversations?.user}
|
||||||
<Typography
|
</AccordionSummary>
|
||||||
variant='body2'
|
<AccordionDetails>
|
||||||
sx={theme => ({
|
<MarkDown
|
||||||
fontSize: 12,
|
content={conversations?.assistant || '未查询到回答内容'}
|
||||||
color: alpha(theme.palette.text.primary, 0.5),
|
/>
|
||||||
})}
|
</AccordionDetails>
|
||||||
>
|
</Accordion>
|
||||||
已思考
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</StyledThinkingAccordionSummary>
|
|
||||||
|
|
||||||
<StyledThinkingAccordionDetails>
|
|
||||||
<MarkDown content={conversations?.thinking_content || ''} />
|
|
||||||
</StyledThinkingAccordionDetails>
|
|
||||||
</StyledThinkingAccordion>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* AI回答内容 */}
|
|
||||||
<StyledAiBubbleContent>
|
|
||||||
<MarkDown content={conversations?.assistant || ''} />
|
|
||||||
</StyledAiBubbleContent>
|
|
||||||
</StyledAiBubble>
|
|
||||||
</StyledConversationItem>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,7 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
sx: {
|
sx: {
|
||||||
'.Mui-disabled': {
|
'.Mui-disabled': {
|
||||||
opacity: '1 !important',
|
opacity: 1,
|
||||||
color: 'text.disabled',
|
color: 'text.disabled',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -873,7 +873,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
sx: {
|
sx: {
|
||||||
'.Mui-disabled': {
|
'.Mui-disabled': {
|
||||||
opacity: '1 !important',
|
opacity: 1,
|
||||||
color: 'text.disabled',
|
color: 'text.disabled',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,8 @@ import {
|
||||||
} from '@/request/types';
|
} from '@/request/types';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import { Icon, message } from '@ctzhian/ui';
|
import { Icon, message } from '@ctzhian/ui';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
Collapse,
|
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Link,
|
Link,
|
||||||
Radio,
|
Radio,
|
||||||
|
|
@ -34,8 +31,6 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
const [isEnabled, setIsEnabled] = useState(false);
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
||||||
const [widgetConfigOpen, setWidgetConfigOpen] = useState(false);
|
|
||||||
const [modalConfigOpen, setModalConfigOpen] = useState(false);
|
|
||||||
const { kb_id } = useAppSelector(state => state.config);
|
const { kb_id } = useAppSelector(state => state.config);
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
|
|
@ -48,15 +43,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
is_open: 0,
|
is_open: 0,
|
||||||
theme_mode: 'light',
|
theme_mode: 'light',
|
||||||
btn_style: 'hover_ball',
|
|
||||||
btn_id: '',
|
|
||||||
btn_position: 'bottom_right',
|
|
||||||
disclaimer: '',
|
|
||||||
btn_text: '',
|
btn_text: '',
|
||||||
btn_logo: '',
|
btn_logo: '',
|
||||||
modal_position: 'follow',
|
|
||||||
search_mode: 'all',
|
|
||||||
placeholder: '',
|
|
||||||
recommend_questions: [] as string[],
|
recommend_questions: [] as string[],
|
||||||
recommend_node_ids: [] as string[],
|
recommend_node_ids: [] as string[],
|
||||||
},
|
},
|
||||||
|
|
@ -66,8 +54,6 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
|
|
||||||
const recommend_questions = watch('recommend_questions') || [];
|
const recommend_questions = watch('recommend_questions') || [];
|
||||||
const recommend_node_ids = watch('recommend_node_ids') || [];
|
const recommend_node_ids = watch('recommend_node_ids') || [];
|
||||||
const btn_style = watch('btn_style') || 'hover_ball';
|
|
||||||
const isCustomButton = btn_style === 'btn_trigger';
|
|
||||||
|
|
||||||
const recommendQuestionsField = useCommitPendingInput<string>({
|
const recommendQuestionsField = useCommitPendingInput<string>({
|
||||||
value: recommend_questions,
|
value: recommend_questions,
|
||||||
|
|
@ -101,17 +87,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
reset({
|
reset({
|
||||||
is_open: res.settings?.widget_bot_settings?.is_open ? 1 : 0,
|
is_open: res.settings?.widget_bot_settings?.is_open ? 1 : 0,
|
||||||
theme_mode: res.settings?.widget_bot_settings?.theme_mode || 'light',
|
theme_mode: res.settings?.widget_bot_settings?.theme_mode || 'light',
|
||||||
btn_style: res.settings?.widget_bot_settings?.btn_style || 'hover_ball',
|
|
||||||
btn_id: res.settings?.widget_bot_settings?.btn_id || '',
|
|
||||||
btn_position:
|
|
||||||
res.settings?.widget_bot_settings?.btn_position || 'bottom_right',
|
|
||||||
btn_text: res.settings?.widget_bot_settings?.btn_text || '在线客服',
|
btn_text: res.settings?.widget_bot_settings?.btn_text || '在线客服',
|
||||||
btn_logo: res.settings?.widget_bot_settings?.btn_logo || '',
|
btn_logo: res.settings?.widget_bot_settings?.btn_logo,
|
||||||
modal_position:
|
|
||||||
res.settings?.widget_bot_settings?.modal_position || 'follow',
|
|
||||||
search_mode: res.settings?.widget_bot_settings?.search_mode || 'all',
|
|
||||||
placeholder: res.settings?.widget_bot_settings?.placeholder || '',
|
|
||||||
disclaimer: res.settings?.widget_bot_settings?.disclaimer || '',
|
|
||||||
recommend_questions:
|
recommend_questions:
|
||||||
res.settings?.widget_bot_settings?.recommend_questions || [],
|
res.settings?.widget_bot_settings?.recommend_questions || [],
|
||||||
recommend_node_ids:
|
recommend_node_ids:
|
||||||
|
|
@ -131,15 +108,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
widget_bot_settings: {
|
widget_bot_settings: {
|
||||||
is_open: data.is_open === 1 ? true : false,
|
is_open: data.is_open === 1 ? true : false,
|
||||||
theme_mode: data.theme_mode as 'light' | 'dark',
|
theme_mode: data.theme_mode as 'light' | 'dark',
|
||||||
btn_style: data.btn_style,
|
|
||||||
btn_id: data.btn_id,
|
|
||||||
btn_position: data.btn_position,
|
|
||||||
btn_text: data.btn_text,
|
btn_text: data.btn_text,
|
||||||
btn_logo: data.btn_logo,
|
btn_logo: data.btn_logo,
|
||||||
modal_position: data.modal_position,
|
|
||||||
search_mode: data.search_mode,
|
|
||||||
placeholder: data.placeholder,
|
|
||||||
disclaimer: data.disclaimer,
|
|
||||||
recommend_questions: data.recommend_questions || [],
|
recommend_questions: data.recommend_questions || [],
|
||||||
recommend_node_ids: data.recommend_node_ids || [],
|
recommend_node_ids: data.recommend_node_ids || [],
|
||||||
},
|
},
|
||||||
|
|
@ -181,7 +151,6 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack spacing={3}>
|
|
||||||
<FormItem label='网页挂件机器人'>
|
<FormItem label='网页挂件机器人'>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -212,139 +181,10 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
{isEnabled && (
|
{isEnabled && (
|
||||||
<>
|
<>
|
||||||
<FormItem
|
<FormItem label='配色方案'>
|
||||||
label='嵌入代码'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
{url ? (
|
|
||||||
<ShowText
|
|
||||||
noEllipsis
|
|
||||||
text={[
|
|
||||||
`<!--// Head 标签引入样式 -->`,
|
|
||||||
`<link rel="stylesheet" href="${url}/widget-bot.css">`,
|
|
||||||
`<!--// Body 标签引入挂件 -->`,
|
|
||||||
`<script src="${url}/widget-bot.js"></script>`,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Stack
|
|
||||||
direction='row'
|
|
||||||
alignItems={'center'}
|
|
||||||
gap={0.5}
|
|
||||||
sx={{
|
|
||||||
color: 'warning.main',
|
|
||||||
fontSize: 14,
|
|
||||||
p: 1.5,
|
|
||||||
borderRadius: 1,
|
|
||||||
bgcolor: 'warning.light',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon type='icon-jinggao' />
|
|
||||||
未配置域名,可在右侧
|
|
||||||
<Box component={'span'} sx={{ fontWeight: 500 }}>
|
|
||||||
服务监听方式
|
|
||||||
</Box>{' '}
|
|
||||||
中配置
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label='挂件配置'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
{!widgetConfigOpen && (
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
variant='outlined'
|
|
||||||
onClick={() => setWidgetConfigOpen(true)}
|
|
||||||
endIcon={<ExpandMoreIcon />}
|
|
||||||
>
|
|
||||||
展开
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Collapse in={widgetConfigOpen}>
|
|
||||||
<Stack spacing={2.5}>
|
|
||||||
<FormItem
|
|
||||||
label='按钮样式'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='btn_style'
|
name='theme_mode'
|
||||||
render={({ field }) => (
|
|
||||||
<RadioGroup
|
|
||||||
row
|
|
||||||
{...field}
|
|
||||||
onChange={e => {
|
|
||||||
const value = e.target.value;
|
|
||||||
field.onChange(value);
|
|
||||||
if (value === 'btn_trigger') {
|
|
||||||
setValue('modal_position', 'fixed');
|
|
||||||
}
|
|
||||||
setIsEdit(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControlLabel
|
|
||||||
value='hover_ball'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>悬浮球</Box>}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='side_sticky'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>侧边吸附</Box>}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='btn_trigger'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>自定义按钮</Box>}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
{isCustomButton ? (
|
|
||||||
<FormItem
|
|
||||||
label='自定义按钮 ID'
|
|
||||||
required
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='btn_id'
|
|
||||||
rules={{
|
|
||||||
required: '自定义按钮 ID 不能为空',
|
|
||||||
}}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
fullWidth
|
|
||||||
placeholder='嵌入网站中自定义按钮的 #id 点击触发,如: pandawiki-widget-bot-btn'
|
|
||||||
error={!!errors.btn_id}
|
|
||||||
helperText={errors.btn_id?.message}
|
|
||||||
onChange={event => {
|
|
||||||
setIsEdit(true);
|
|
||||||
field.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FormItem
|
|
||||||
label='按钮位置'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='btn_position'
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
row
|
row
|
||||||
|
|
@ -355,43 +195,28 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value='top_left'
|
value={'light'}
|
||||||
control={<Radio size='small' />}
|
control={<Radio size='small' />}
|
||||||
label={<Box sx={{ width: 100 }}>左上</Box>}
|
label={<Box sx={{ width: 100 }}>浅色模式</Box>}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value='top_right'
|
value={'dark'}
|
||||||
control={<Radio size='small' />}
|
control={<Radio size='small' />}
|
||||||
label={<Box sx={{ width: 100 }}>右上</Box>}
|
label={<Box sx={{ width: 100 }}>深色模式</Box>}
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='bottom_left'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>左下</Box>}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='bottom_right'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>右下</Box>}
|
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
{btn_style !== 'hover_ball' && (
|
<FormItem label='侧边按钮文字'>
|
||||||
<FormItem
|
|
||||||
label='按钮文字'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='btn_text'
|
name='btn_text'
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder='输入按钮文字'
|
{...field}
|
||||||
|
placeholder='输入侧边按钮文字'
|
||||||
error={!!errors.btn_text}
|
error={!!errors.btn_text}
|
||||||
helperText={errors.btn_text?.message}
|
helperText={errors.btn_text?.message}
|
||||||
onChange={event => {
|
onChange={event => {
|
||||||
|
|
@ -402,12 +227,7 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
<FormItem label='侧边按钮 Logo'>
|
||||||
<FormItem
|
|
||||||
label='按钮图标'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='btn_logo'
|
name='btn_logo'
|
||||||
|
|
@ -426,186 +246,13 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</>
|
<FormItem label='推荐问题'>
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Collapse>
|
|
||||||
</Box>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label='弹框配置'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
{!modalConfigOpen && (
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
variant='outlined'
|
|
||||||
onClick={() => setModalConfigOpen(true)}
|
|
||||||
endIcon={<ExpandMoreIcon />}
|
|
||||||
>
|
|
||||||
展开
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Collapse in={modalConfigOpen}>
|
|
||||||
<Stack spacing={2.5}>
|
|
||||||
{/* <FormItem label='配色方案' sx={{ alignItems: 'flex-start' }} labelSx={{ mt: 1 }}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='theme_mode'
|
|
||||||
render={({ field }) => (
|
|
||||||
<RadioGroup
|
|
||||||
row
|
|
||||||
{...field}
|
|
||||||
onChange={e => {
|
|
||||||
field.onChange(e.target.value);
|
|
||||||
setIsEdit(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControlLabel
|
|
||||||
value='light'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>浅色模式</Box>}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='dark'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>深色模式</Box>}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='system'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>跟随系统</Box>}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='wiki'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>跟随 WIKI 网站</Box>}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormItem> */}
|
|
||||||
<FormItem
|
|
||||||
label='弹窗位置'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='modal_position'
|
|
||||||
render={({ field }) => {
|
|
||||||
const isDisabled = btn_style === 'btn_trigger';
|
|
||||||
return (
|
|
||||||
<RadioGroup
|
|
||||||
row
|
|
||||||
{...field}
|
|
||||||
value={isDisabled ? 'fixed' : field.value}
|
|
||||||
onChange={e => {
|
|
||||||
if (!isDisabled) {
|
|
||||||
field.onChange(e.target.value);
|
|
||||||
setIsEdit(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControlLabel
|
|
||||||
value='follow'
|
|
||||||
control={
|
|
||||||
<Radio size='small' disabled={isDisabled} />
|
|
||||||
}
|
|
||||||
label={<Box sx={{ width: 100 }}>跟随按钮</Box>}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='fixed'
|
|
||||||
control={
|
|
||||||
<Radio size='small' disabled={isDisabled} />
|
|
||||||
}
|
|
||||||
label={<Box sx={{ width: 100 }}>居中展示</Box>}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label='搜索模式'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='search_mode'
|
|
||||||
render={({ field }) => (
|
|
||||||
<RadioGroup
|
|
||||||
row
|
|
||||||
{...field}
|
|
||||||
onChange={e => {
|
|
||||||
field.onChange(e.target.value);
|
|
||||||
setIsEdit(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControlLabel
|
|
||||||
value='all'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>双模式切换</Box>}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='qa'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={
|
|
||||||
<Box sx={{ width: 100 }}>智能问答模式</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value='doc'
|
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={
|
|
||||||
<Box sx={{ width: 100 }}>搜索文档模式</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label='搜索提示语'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='placeholder'
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
{...field}
|
|
||||||
placeholder='输入搜索提示语'
|
|
||||||
error={!!errors.placeholder}
|
|
||||||
helperText={errors.placeholder?.message}
|
|
||||||
onChange={event => {
|
|
||||||
setIsEdit(true);
|
|
||||||
field.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem
|
|
||||||
label='推荐问题'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<FreeSoloAutocomplete
|
<FreeSoloAutocomplete
|
||||||
{...recommendQuestionsField}
|
{...recommendQuestionsField}
|
||||||
placeholder='回车确认,填写下一个推荐问题'
|
placeholder='回车确认,填写下一个推荐问题'
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem label='推荐文档'>
|
||||||
label='推荐文档'
|
|
||||||
sx={{ alignItems: 'flex-start' }}
|
|
||||||
labelSx={{ mt: 1 }}
|
|
||||||
>
|
|
||||||
<RecommendDocDragList
|
<RecommendDocDragList
|
||||||
ids={recommend_node_ids}
|
ids={recommend_node_ids}
|
||||||
onChange={(value: string[]) => {
|
onChange={(value: string[]) => {
|
||||||
|
|
@ -614,36 +261,36 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
<FormItem label='嵌入代码'>
|
||||||
label='免责声明'
|
{url ? (
|
||||||
sx={{ alignItems: 'flex-start' }}
|
<ShowText
|
||||||
labelSx={{ mt: 1 }}
|
noEllipsis
|
||||||
|
text={[
|
||||||
|
`<!--// Head 标签引入样式 -->`,
|
||||||
|
`<link rel="stylesheet" href="${url}/widget-bot.css">`,
|
||||||
|
'',
|
||||||
|
`<!--// Body 标签引入挂件 -->`,
|
||||||
|
`<script src="${url}/widget-bot.js"></script>`,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Stack
|
||||||
|
direction='row'
|
||||||
|
alignItems={'center'}
|
||||||
|
gap={0.5}
|
||||||
|
sx={{ color: 'warning.main', fontSize: 14 }}
|
||||||
>
|
>
|
||||||
<Controller
|
<Icon type='icon-jinggao' />
|
||||||
control={control}
|
未配置域名,可在右侧
|
||||||
name='disclaimer'
|
<Box component={'span'} sx={{ fontWeight: 500 }}>
|
||||||
render={({ field }) => (
|
服务监听方式
|
||||||
<TextField
|
</Box>{' '}
|
||||||
fullWidth
|
中配置
|
||||||
{...field}
|
|
||||||
placeholder='输入免责声明'
|
|
||||||
error={!!errors.disclaimer}
|
|
||||||
helperText={errors.disclaimer?.message}
|
|
||||||
onChange={event => {
|
|
||||||
setIsEdit(true);
|
|
||||||
field.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
)}
|
||||||
</Box>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
|
||||||
</SettingCardItem>
|
</SettingCardItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import {
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
||||||
|
|
@ -207,32 +206,6 @@ export const postShareProV1AuthLdap = (
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 用户登出
|
|
||||||
*
|
|
||||||
* @tags ShareAuth
|
|
||||||
* @name PostShareProV1AuthLogout
|
|
||||||
* @summary 用户登出
|
|
||||||
* @request POST:/share/pro/v1/auth/logout
|
|
||||||
* @response `200` `(DomainPWResponse & {
|
|
||||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
|
||||||
|
|
||||||
})` OK
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postShareProV1AuthLogout = (params: RequestParams = {}) =>
|
|
||||||
httpRequest<
|
|
||||||
DomainPWResponse & {
|
|
||||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp;
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
path: `/share/pro/v1/auth/logout`,
|
|
||||||
method: "POST",
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description OAuth登录
|
* @description OAuth登录
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -457,11 +457,6 @@ export type GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp = Record<
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp = Record<
|
|
||||||
string,
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
redirect_url?: string;
|
redirect_url?: string;
|
||||||
|
|
|
||||||
|
|
@ -171,11 +171,6 @@ export enum ConstsNodeAccessPerm {
|
||||||
NodeAccessPermClosed = "closed",
|
NodeAccessPermClosed = "closed",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConstsModelSettingMode {
|
|
||||||
ModelSettingModeManual = "manual",
|
|
||||||
ModelSettingModeAuto = "auto",
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @format int32 */
|
/** @format int32 */
|
||||||
export enum ConstsLicenseEdition {
|
export enum ConstsLicenseEdition {
|
||||||
/** 开源版 */
|
/** 开源版 */
|
||||||
|
|
@ -934,10 +929,8 @@ export interface DomainModelModeSetting {
|
||||||
auto_mode_api_key?: string;
|
auto_mode_api_key?: string;
|
||||||
/** 自定义对话模型名称 */
|
/** 自定义对话模型名称 */
|
||||||
chat_model?: string;
|
chat_model?: string;
|
||||||
/** 手动模式下嵌入模型是否更新 */
|
|
||||||
is_manual_embedding_updated?: boolean;
|
|
||||||
/** 模式: manual 或 auto */
|
/** 模式: manual 或 auto */
|
||||||
mode?: ConstsModelSettingMode;
|
mode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainMoveNodeReq {
|
export interface DomainMoveNodeReq {
|
||||||
|
|
@ -1189,17 +1182,6 @@ export interface DomainShareConversationMessage {
|
||||||
role?: SchemaRoleType;
|
role?: SchemaRoleType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainShareNodeListItemResp {
|
|
||||||
emoji?: string;
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
parent_id?: string;
|
|
||||||
permissions?: DomainNodePermissions;
|
|
||||||
position?: number;
|
|
||||||
type?: DomainNodeType;
|
|
||||||
updated_at?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DomainSimpleAuth {
|
export interface DomainSimpleAuth {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
@ -1377,18 +1359,11 @@ export interface DomainWecomAIBotSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainWidgetBotSettings {
|
export interface DomainWidgetBotSettings {
|
||||||
btn_id?: string;
|
|
||||||
btn_logo?: string;
|
btn_logo?: string;
|
||||||
btn_position?: string;
|
|
||||||
btn_style?: string;
|
|
||||||
btn_text?: string;
|
btn_text?: string;
|
||||||
disclaimer?: string;
|
|
||||||
is_open?: boolean;
|
is_open?: boolean;
|
||||||
modal_position?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
recommend_node_ids?: string[];
|
recommend_node_ids?: string[];
|
||||||
recommend_questions?: string[];
|
recommend_questions?: string[];
|
||||||
search_mode?: string;
|
|
||||||
theme_mode?: string;
|
theme_mode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1694,7 +1669,6 @@ export interface V1ShareNodeDetailResp {
|
||||||
editor_id?: string;
|
editor_id?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
list?: DomainShareNodeListItemResp[];
|
|
||||||
meta?: DomainNodeMeta;
|
meta?: DomainNodeMeta;
|
||||||
name?: string;
|
name?: string;
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:22-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export default isDevelopment
|
||||||
|
|
||||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||||
// This can increase your server load as well as your hosting bill.
|
// This can increase your server load as well as your hosting bill.
|
||||||
// Note: Check that the configured route will not match with your Next.js proxy, otherwise reporting of client-
|
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||||
// side errors will fail.
|
// side errors will fail.
|
||||||
tunnelRoute: '/monitoring',
|
tunnelRoute: '/monitoring',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3010",
|
"dev": "next dev --turbopack -p 3010",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@emotion/cache": "^11.14.0",
|
"@emotion/cache": "^11.14.0",
|
||||||
"@mui/material-nextjs": "^7.3.5",
|
"@mui/material-nextjs": "^7.1.0",
|
||||||
"@sentry/nextjs": "^10.8.0",
|
"@sentry/nextjs": "^10.8.0",
|
||||||
"@types/markdown-it": "13.0.1",
|
"@types/markdown-it": "13.0.1",
|
||||||
"@vscode/markdown-it-katex": "^1.1.2",
|
"@vscode/markdown-it-katex": "^1.1.2",
|
||||||
|
|
@ -25,13 +25,12 @@
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"html-react-parser": "^5.2.5",
|
"html-react-parser": "^5.2.5",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
"import-in-the-middle": "^1.4.0",
|
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"katex": "^0.16.22",
|
"katex": "^0.16.22",
|
||||||
"markdown-it": "13.0.1",
|
"markdown-it": "13.0.1",
|
||||||
"markdown-it-highlightjs": "^4.2.0",
|
"markdown-it-highlightjs": "^4.2.0",
|
||||||
"mermaid": "^11.9.0",
|
"mermaid": "^11.9.0",
|
||||||
"next": "^16.0.0",
|
"next": "15.4.6",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-photo-view": "^1.2.7",
|
"react-photo-view": "^1.2.7",
|
||||||
|
|
@ -42,23 +41,17 @@
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ctzhian/cx-swagger-api": "^1.0.0",
|
"@ctzhian/cx-swagger-api": "^1.0.0",
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@next/eslint-plugin-next": "^16.0.0",
|
"@next/eslint-plugin-next": "^15.4.5",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/rangy": "^1.3.0",
|
"@types/rangy": "^1.3.0",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"eslint-config-next": "16.0.0",
|
"eslint-config-next": "15.3.2",
|
||||||
"eslint-config-prettier": "^9.1.2"
|
"eslint-config-prettier": "^9.1.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac",
|
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||||
"pnpm": {
|
|
||||||
"overrides": {
|
|
||||||
"require-in-the-middle": "^7.5.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,35 @@
|
||||||
/* 挂件按钮基础样式 */
|
/* 挂件按钮样式 - 基于MUI主题 */
|
||||||
.widget-bot-button {
|
.widget-bot-button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 190px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
border-radius: 18px 0 0 18px;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
|
box-shadow: 0px 6px 10px 0px rgba(54, 59, 76, 0.17);
|
||||||
|
padding: 11px;
|
||||||
|
min-height: 120px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
|
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
|
||||||
border: none;
|
border: none;
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
/* 优化拖拽性能 */
|
|
||||||
will-change: transform;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-button:hover:not(.dragging) {
|
.widget-bot-button:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
box-shadow: 0 4px 8px rgba(50, 72, 242, 0.2);
|
||||||
|
|
||||||
.widget-bot-hover-ball:hover:not(.dragging) {
|
|
||||||
transform: scale(1.1) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-button.dragging {
|
.widget-bot-button.dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
transition: none !important;
|
transform: rotate(2deg);
|
||||||
/* 拖拽时禁用过渡,提升性能 */
|
box-shadow: 0 6px 12px rgba(50, 72, 242, 0.25);
|
||||||
/* transform 由 JS 控制,包含 rotate 和 translate */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-button-content {
|
.widget-bot-button-content {
|
||||||
|
|
@ -43,13 +39,14 @@
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 图标样式 */
|
.widget-bot-logo {
|
||||||
.widget-bot-icon {
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 文字样式 */
|
|
||||||
.widget-bot-text {
|
.widget-bot-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
@ -63,47 +60,6 @@
|
||||||
margin: 1px 0;
|
margin: 1px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 侧边吸附按钮样式 */
|
|
||||||
.widget-bot-side-sticky {
|
|
||||||
width: 48px;
|
|
||||||
padding: 6px 6px 12px 6px;
|
|
||||||
background: #FFFFFF;
|
|
||||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
|
||||||
border-radius: 24px;
|
|
||||||
border: 1px solid #ECEEF1;
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-side-sticky .widget-bot-icon {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-side-sticky .widget-bot-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #646a73;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 悬浮球按钮样式 */
|
|
||||||
.widget-bot-hover-ball {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 24px;
|
|
||||||
background: #FFFFFF;
|
|
||||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
|
||||||
border: 1px solid #ECEEF1;
|
|
||||||
padding: 0;
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-hover-ball .widget-bot-hover-ball-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 模态框样式 - 基于MUI主题 */
|
/* 模态框样式 - 基于MUI主题 */
|
||||||
.widget-bot-modal {
|
.widget-bot-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -119,11 +75,6 @@
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-modal-fixed {
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-modal-content {
|
.widget-bot-modal-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
|
@ -137,14 +88,6 @@
|
||||||
animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-modal-content-fixed {
|
|
||||||
position: relative;
|
|
||||||
width: 800px;
|
|
||||||
height: auto;
|
|
||||||
max-height: 90vh;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInUp {
|
@keyframes slideInUp {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -157,30 +100,34 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 关闭按钮样式 - 透明框 */
|
/* 关闭按钮样式 - 基于MUI IconButton */
|
||||||
.widget-bot-close-btn {
|
.widget-bot-close-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 22.5px;
|
top: 12px;
|
||||||
right: 16px;
|
right: 12px;
|
||||||
background: transparent;
|
background: none;
|
||||||
width: 36.26px;
|
width: 36px;
|
||||||
height: 25px;
|
height: 36px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 50%;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 0;
|
font-size: 18px;
|
||||||
opacity: 1;
|
opacity: 0.5;
|
||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
transition: none;
|
transition: all 0.1s ease-in-out;
|
||||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||||
padding: 0;
|
}
|
||||||
margin: 0;
|
|
||||||
pointer-events: none;
|
.widget-bot-close-btn:hover {
|
||||||
/* 允许鼠标穿透到下方 */
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-bot-close-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iframe样式 */
|
/* iframe样式 */
|
||||||
|
|
@ -193,11 +140,6 @@
|
||||||
background: #F8F9FA;
|
background: #F8F9FA;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-modal-content-fixed .widget-bot-iframe {
|
|
||||||
min-height: 600px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 防止页面滚动 */
|
/* 防止页面滚动 */
|
||||||
body.widget-bot-modal-open {
|
body.widget-bot-modal-open {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -205,34 +147,19 @@ body.widget-bot-modal-open {
|
||||||
|
|
||||||
/* 暗色主题支持 - 基于data-theme属性 */
|
/* 暗色主题支持 - 基于data-theme属性 */
|
||||||
.widget-bot-button[data-theme="dark"] {
|
.widget-bot-button[data-theme="dark"] {
|
||||||
|
background: #6E73FE;
|
||||||
box-shadow: 0 2px 4px rgba(110, 115, 254, 0.15);
|
box-shadow: 0 2px 4px rgba(110, 115, 254, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-side-sticky[data-theme="dark"] {
|
.widget-bot-button[data-theme="dark"]:hover {
|
||||||
background: #6E73FE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-side-sticky[data-theme="dark"]:hover {
|
|
||||||
background: #5d68fd;
|
background: #5d68fd;
|
||||||
box-shadow: 0 4px 8px rgba(110, 115, 254, 0.2);
|
box-shadow: 0 4px 8px rgba(110, 115, 254, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-side-sticky[data-theme="dark"].dragging {
|
.widget-bot-button[data-theme="dark"].dragging {
|
||||||
box-shadow: 0 6px 12px rgba(110, 115, 254, 0.25);
|
box-shadow: 0 6px 12px rgba(110, 115, 254, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-hover-ball[data-theme="dark"] {
|
|
||||||
background: #6E73FE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-hover-ball[data-theme="dark"]:hover {
|
|
||||||
transform: scale(1.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-hover-ball[data-theme="dark"].dragging {
|
|
||||||
box-shadow: 0 8px 20px rgba(110, 115, 254, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-modal[data-theme="dark"] {
|
.widget-bot-modal[data-theme="dark"] {
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
@ -242,63 +169,61 @@ body.widget-bot-modal-open {
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端适配 - 统一处理 */
|
/* 移动端适配 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.widget-bot-side-sticky {
|
.widget-bot-button {
|
||||||
width: 48px;
|
bottom: 16px;
|
||||||
padding: 6px 6px 12px 6px;
|
padding: 8px;
|
||||||
border-radius: 24px;
|
border-radius: 10px 0 0 10px;
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-hover-ball {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 24px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-hover-ball .widget-bot-hover-ball-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-text {
|
.widget-bot-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-icon {
|
.widget-bot-logo {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端弹框统一居中显示,宽度100%-32px,高度90vh */
|
|
||||||
.widget-bot-modal-content {
|
.widget-bot-modal-content {
|
||||||
position: relative !important;
|
width: calc(100% - 60.5px);
|
||||||
width: calc(100% - 32px) !important;
|
height: 90%;
|
||||||
height: 90vh !important;
|
max-width: none;
|
||||||
max-width: none !important;
|
max-height: none;
|
||||||
max-height: 90vh !important;
|
|
||||||
top: auto !important;
|
|
||||||
left: auto !important;
|
|
||||||
right: auto !important;
|
|
||||||
bottom: auto !important;
|
|
||||||
margin: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-modal-content-fixed {
|
|
||||||
width: calc(100% - 32px) !important;
|
|
||||||
height: 90vh !important;
|
|
||||||
max-height: 90vh !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-close-btn {
|
.widget-bot-close-btn {
|
||||||
top: 22.5px;
|
top: 8px;
|
||||||
right: 16px;
|
right: 8px;
|
||||||
width: 36.26px;
|
width: 32px;
|
||||||
height: 25px;
|
height: 32px;
|
||||||
font-size: 0;
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏幕适配 */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.widget-bot-button {
|
||||||
|
bottom: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-bot-text {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-bot-modal-content {
|
||||||
|
width: calc(100% - 55.5px);
|
||||||
|
height: 90%;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-bot-close-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,32 +274,19 @@ body.widget-bot-modal-open {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 浅色主题样式 - 显式定义 */
|
/* 浅色主题样式 - 显式定义 */
|
||||||
.widget-bot-side-sticky[data-theme="light"] {
|
.widget-bot-button[data-theme="light"] {
|
||||||
background: #FFFFFF;
|
background: #3248F2;
|
||||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
color: #FFFFFF;
|
||||||
border: 1px solid #ECEEF1;
|
box-shadow: 0px 6px 10px 0px rgba(54, 59, 76, 0.17);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-side-sticky[data-theme="light"]:hover {
|
.widget-bot-button[data-theme="light"]:hover {
|
||||||
background: #FFFFFF;
|
background: #2a3cdb;
|
||||||
|
box-shadow: 0 4px 8px rgba(50, 72, 242, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-side-sticky[data-theme="light"].dragging {
|
.widget-bot-button[data-theme="light"].dragging {
|
||||||
background: #FFFFFF;
|
box-shadow: 0 6px 12px rgba(50, 72, 242, 0.25);
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-hover-ball[data-theme="light"] {
|
|
||||||
background: #FFFFFF;
|
|
||||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
|
||||||
border: 1px solid #ECEEF1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-hover-ball[data-theme="light"]:hover {
|
|
||||||
transform: scale(1.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.widget-bot-hover-ball[data-theme="light"].dragging {
|
|
||||||
box-shadow: 0 8px 20px rgba(50, 72, 242, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-modal[data-theme="light"] {
|
.widget-bot-modal[data-theme="light"] {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const defaultModalPosition = 'follow';
|
|
||||||
const defaultBtnPosition = 'bottom_left';
|
|
||||||
const defaultBtnStyle = 'side_sticky';
|
|
||||||
|
|
||||||
// 获取当前脚本的域名
|
// 获取当前脚本的域名
|
||||||
const currentScript = document.currentScript || document.querySelector('script[src*="widget-bot.js"]');
|
const currentScript = document.currentScript || document.querySelector('script[src*="widget-bot.js"]');
|
||||||
const widgetDomain = currentScript ? new URL(currentScript.src).origin : window.location.origin;
|
const widgetDomain = currentScript ? new URL(currentScript.src).origin : window.location.origin;
|
||||||
|
|
@ -15,13 +11,6 @@
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let dragOffset = { x: 0, y: 0 };
|
let dragOffset = { x: 0, y: 0 };
|
||||||
let currentTheme = 'light'; // 默认浅色主题
|
let currentTheme = 'light'; // 默认浅色主题
|
||||||
let customTriggerElement = null; // 自定义触发元素
|
|
||||||
let customTriggerHandler = null; // 自定义触发元素的事件处理函数
|
|
||||||
let dragAnimationFrame = null; // 拖拽动画帧ID
|
|
||||||
let buttonSize = { width: 0, height: 0 }; // 缓存按钮尺寸
|
|
||||||
let initialPosition = { left: 0, top: 0 }; // 拖拽开始时的初始位置
|
|
||||||
let hasDragged = false; // 标记是否发生了拖拽
|
|
||||||
let dragStartPos = { x: 0, y: 0 }; // 拖拽开始时的鼠标位置
|
|
||||||
|
|
||||||
// 应用主题
|
// 应用主题
|
||||||
function applyTheme(theme_mode) {
|
function applyTheme(theme_mode) {
|
||||||
|
|
@ -71,22 +60,13 @@
|
||||||
applyTheme(widgetInfo.theme_mode);
|
applyTheme(widgetInfo.theme_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据 btn_style 创建不同的挂件
|
|
||||||
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
|
|
||||||
if (btnStyle === 'btn_trigger') {
|
|
||||||
createCustomTrigger();
|
|
||||||
} else {
|
|
||||||
createWidget();
|
createWidget();
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取挂件信息失败:', error);
|
console.error('获取挂件信息失败:', error);
|
||||||
// 使用默认值
|
// 使用默认值
|
||||||
widgetInfo = {
|
widgetInfo = {
|
||||||
btn_text: '在线客服',
|
btn_text: '在线客服',
|
||||||
btn_logo: `''`,
|
btn_logo: '',
|
||||||
btn_style: defaultBtnStyle,
|
|
||||||
btn_position: defaultBtnPosition,
|
|
||||||
modal_position: defaultModalPosition,
|
|
||||||
theme_mode: 'light'
|
theme_mode: 'light'
|
||||||
};
|
};
|
||||||
applyTheme(widgetInfo.theme_mode);
|
applyTheme(widgetInfo.theme_mode);
|
||||||
|
|
@ -98,148 +78,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建两行文字(每行两个字)
|
// 创建垂直文字
|
||||||
function createTwoLineText(text) {
|
function createVerticalText(text) {
|
||||||
const chars = text.split('').filter(it => !!it.trim());
|
return text.split('').map((char, index) =>
|
||||||
const lines = [];
|
`<span>${char}</span>`
|
||||||
for (let i = 0; i < chars.length; i += 2) {
|
).join('');
|
||||||
lines.push(chars.slice(i, i + 2).join(''));
|
|
||||||
}
|
|
||||||
return lines.map(line => `<span>${line}</span>`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用按钮位置
|
|
||||||
function applyButtonPosition(button, position) {
|
|
||||||
const pos = position || defaultBtnPosition;
|
|
||||||
button.style.top = 'auto';
|
|
||||||
button.style.right = 'auto';
|
|
||||||
button.style.bottom = 'auto';
|
|
||||||
button.style.left = 'auto';
|
|
||||||
|
|
||||||
// 两种模式使用相同的默认位置:距离边缘16px,垂直方向190px
|
|
||||||
switch (pos) {
|
|
||||||
case 'top_left':
|
|
||||||
button.style.top = '190px';
|
|
||||||
button.style.left = '16px';
|
|
||||||
break;
|
|
||||||
case 'top_right':
|
|
||||||
button.style.top = '190px';
|
|
||||||
button.style.right = '16px';
|
|
||||||
break;
|
|
||||||
case 'bottom_left':
|
|
||||||
button.style.bottom = '190px';
|
|
||||||
button.style.left = '16px';
|
|
||||||
break;
|
|
||||||
case 'bottom_right':
|
|
||||||
default:
|
|
||||||
button.style.bottom = '190px';
|
|
||||||
button.style.right = '16px';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建侧边吸附按钮
|
|
||||||
function createSideStickyButton() {
|
|
||||||
widgetButton = document.createElement('div');
|
|
||||||
widgetButton.className = 'widget-bot-button widget-bot-side-sticky';
|
|
||||||
widgetButton.setAttribute('role', 'button');
|
|
||||||
widgetButton.setAttribute('tabindex', '0');
|
|
||||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
|
|
||||||
widgetButton.setAttribute('data-theme', currentTheme);
|
|
||||||
|
|
||||||
const buttonContent = document.createElement('div');
|
|
||||||
buttonContent.className = 'widget-bot-button-content';
|
|
||||||
|
|
||||||
// 侧边吸附显示图标和文字(btn_logo 以及 btn_text)
|
|
||||||
const icon = document.createElement('img');
|
|
||||||
const defaultIconSrc = widgetDomain + '/favicon.png';
|
|
||||||
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
|
|
||||||
icon.alt = 'icon';
|
|
||||||
icon.className = 'widget-bot-icon';
|
|
||||||
icon.onerror = () => {
|
|
||||||
// 如果当前不是 favicon.png,尝试使用 favicon.png 作为备用
|
|
||||||
if (icon.src !== defaultIconSrc) {
|
|
||||||
icon.src = defaultIconSrc;
|
|
||||||
} else {
|
|
||||||
// 如果 favicon.png 也加载失败,隐藏图标
|
|
||||||
icon.style.display = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
buttonContent.appendChild(icon);
|
|
||||||
|
|
||||||
// 添加文字
|
|
||||||
const textDiv = document.createElement('div');
|
|
||||||
textDiv.className = 'widget-bot-text';
|
|
||||||
textDiv.innerHTML = createTwoLineText(widgetInfo.btn_text || '在线客服');
|
|
||||||
buttonContent.appendChild(textDiv);
|
|
||||||
|
|
||||||
widgetButton.appendChild(buttonContent);
|
|
||||||
|
|
||||||
// 应用位置 - 距离边缘16px,垂直方向190px
|
|
||||||
const position = widgetInfo.btn_position || defaultBtnPosition;
|
|
||||||
applyButtonPosition(widgetButton, position);
|
|
||||||
|
|
||||||
// 设置 border-radius 为 24px(统一圆角)
|
|
||||||
widgetButton.style.borderRadius = '24px';
|
|
||||||
|
|
||||||
// 添加事件监听器
|
|
||||||
widgetButton.addEventListener('click', handleButtonClick);
|
|
||||||
widgetButton.addEventListener('mousedown', startDrag);
|
|
||||||
widgetButton.addEventListener('keydown', handleKeyDown);
|
|
||||||
|
|
||||||
// 添加触摸事件支持
|
|
||||||
widgetButton.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
||||||
widgetButton.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
||||||
widgetButton.addEventListener('touchend', handleTouchEnd);
|
|
||||||
|
|
||||||
document.body.appendChild(widgetButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建悬浮球按钮
|
|
||||||
function createHoverBallButton() {
|
|
||||||
widgetButton = document.createElement('div');
|
|
||||||
widgetButton.className = 'widget-bot-button widget-bot-hover-ball';
|
|
||||||
widgetButton.setAttribute('role', 'button');
|
|
||||||
widgetButton.setAttribute('tabindex', '0');
|
|
||||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
|
|
||||||
widgetButton.setAttribute('data-theme', currentTheme);
|
|
||||||
|
|
||||||
const buttonContent = document.createElement('div');
|
|
||||||
buttonContent.className = 'widget-bot-button-content';
|
|
||||||
|
|
||||||
// 悬浮球只显示图标(btn_logo)
|
|
||||||
const icon = document.createElement('img');
|
|
||||||
const defaultIconSrc = widgetDomain + '/favicon.png';
|
|
||||||
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
|
|
||||||
icon.alt = 'icon';
|
|
||||||
icon.className = 'widget-bot-icon widget-bot-hover-ball-icon';
|
|
||||||
icon.onerror = () => {
|
|
||||||
// 如果当前不是 favicon.png,尝试使用 favicon.png 作为备用
|
|
||||||
if (icon.src !== defaultIconSrc) {
|
|
||||||
icon.src = defaultIconSrc;
|
|
||||||
} else {
|
|
||||||
// 如果 favicon.png 也加载失败,隐藏图标
|
|
||||||
icon.style.display = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
buttonContent.appendChild(icon);
|
|
||||||
|
|
||||||
widgetButton.appendChild(buttonContent);
|
|
||||||
|
|
||||||
// 应用位置 - 距离边缘16px,垂直方向190px
|
|
||||||
applyButtonPosition(widgetButton, widgetInfo.btn_position || defaultBtnPosition);
|
|
||||||
|
|
||||||
// 添加事件监听器
|
|
||||||
widgetButton.addEventListener('click', handleButtonClick);
|
|
||||||
widgetButton.addEventListener('mousedown', startDrag);
|
|
||||||
widgetButton.addEventListener('keydown', handleKeyDown);
|
|
||||||
|
|
||||||
// 添加触摸事件支持
|
|
||||||
widgetButton.addEventListener('touchstart', handleTouchStart, { passive: false });
|
|
||||||
widgetButton.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
||||||
widgetButton.addEventListener('touchend', handleTouchEnd);
|
|
||||||
|
|
||||||
document.body.appendChild(widgetButton);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建挂件按钮
|
// 创建挂件按钮
|
||||||
|
|
@ -249,14 +92,49 @@
|
||||||
widgetButton.remove();
|
widgetButton.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
|
// 创建按钮容器
|
||||||
|
widgetButton = document.createElement('div');
|
||||||
|
widgetButton.className = 'widget-bot-button';
|
||||||
|
widgetButton.setAttribute('role', 'button');
|
||||||
|
widgetButton.setAttribute('tabindex', '0');
|
||||||
|
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text}窗口`);
|
||||||
|
widgetButton.setAttribute('data-theme', currentTheme);
|
||||||
|
|
||||||
if (btnStyle === 'hover_ball') {
|
const buttonContent = document.createElement('div');
|
||||||
createHoverBallButton();
|
buttonContent.className = 'widget-bot-button-content';
|
||||||
} else {
|
|
||||||
createSideStickyButton();
|
// 添加logo(如果有)
|
||||||
|
if (widgetInfo.btn_logo) {
|
||||||
|
const logo = document.createElement('img');
|
||||||
|
logo.src = widgetDomain + widgetInfo.btn_logo;
|
||||||
|
logo.alt = 'logo';
|
||||||
|
logo.className = 'widget-bot-logo';
|
||||||
|
logo.onerror = () => {
|
||||||
|
logo.style.display = 'none';
|
||||||
|
};
|
||||||
|
buttonContent.appendChild(logo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加文字
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
textDiv.className = 'widget-bot-text';
|
||||||
|
textDiv.innerHTML = createVerticalText(widgetInfo.btn_text || '在线客服');
|
||||||
|
buttonContent.appendChild(textDiv);
|
||||||
|
|
||||||
|
widgetButton.appendChild(buttonContent);
|
||||||
|
|
||||||
|
// 添加事件监听器
|
||||||
|
widgetButton.addEventListener('click', showModal);
|
||||||
|
widgetButton.addEventListener('mousedown', startDrag);
|
||||||
|
widgetButton.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
// 添加触摸事件支持
|
||||||
|
widgetButton.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||||
|
widgetButton.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||||
|
widgetButton.addEventListener('touchend', handleTouchEnd);
|
||||||
|
|
||||||
|
document.body.appendChild(widgetButton);
|
||||||
|
|
||||||
// 创建模态框
|
// 创建模态框
|
||||||
createModal();
|
createModal();
|
||||||
|
|
||||||
|
|
@ -267,109 +145,6 @@
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建自定义触发按钮
|
|
||||||
function createCustomTrigger() {
|
|
||||||
const btnId = widgetInfo.btn_id;
|
|
||||||
if (!btnId) {
|
|
||||||
console.error('btn_trigger 模式需要提供 btn_id');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let retryCount = 0;
|
|
||||||
const maxRetries = 50; // 最多重试 50 次(5秒)
|
|
||||||
|
|
||||||
// 绑定事件到元素
|
|
||||||
function attachTrigger(element) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
// 避免重复绑定
|
|
||||||
if (element.hasAttribute('data-widget-trigger-attached')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.setAttribute('data-widget-trigger-attached', 'true');
|
|
||||||
customTriggerElement = element;
|
|
||||||
|
|
||||||
// 创建事件处理函数并保存引用
|
|
||||||
customTriggerHandler = function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 绑定点击事件
|
|
||||||
element.addEventListener('click', customTriggerHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试查找并绑定元素
|
|
||||||
function tryAttachTrigger() {
|
|
||||||
const element = document.getElementById(btnId);
|
|
||||||
if (element) {
|
|
||||||
attachTrigger(element);
|
|
||||||
createModal();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 立即尝试一次
|
|
||||||
if (tryAttachTrigger()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果元素还没加载,使用多种方式监听
|
|
||||||
function retryAttach() {
|
|
||||||
if (tryAttachTrigger()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
retryCount++;
|
|
||||||
if (retryCount < maxRetries) {
|
|
||||||
setTimeout(retryAttach, 100);
|
|
||||||
} else {
|
|
||||||
console.warn('自定义触发按钮未找到,已停止重试:', btnId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 MutationObserver 监听 DOM 变化
|
|
||||||
const observer = new MutationObserver(function (mutations) {
|
|
||||||
if (tryAttachTrigger()) {
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 开始观察 DOM 变化
|
|
||||||
observer.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果 DOM 已加载完成,立即开始重试
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
setTimeout(retryAttach, 100);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setTimeout(retryAttach, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟断开观察器(避免无限观察)
|
|
||||||
setTimeout(function () {
|
|
||||||
observer.disconnect();
|
|
||||||
}, 10000); // 10秒后断开
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理按钮点击事件(区分点击和拖拽)
|
|
||||||
function handleButtonClick(e) {
|
|
||||||
// 如果发生了拖拽,不打开弹框
|
|
||||||
if (hasDragged) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 键盘事件处理
|
// 键盘事件处理
|
||||||
function handleKeyDown(e) {
|
function handleKeyDown(e) {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
|
@ -401,8 +176,7 @@
|
||||||
Math.pow(touch.clientY - touchStartPos.y, 2)
|
Math.pow(touch.clientY - touchStartPos.y, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 只有在没有拖拽且移动距离很小的情况下才认为是点击
|
if (distance < 10) {
|
||||||
if (!hasDragged && distance < 10) {
|
|
||||||
// 判断为点击事件
|
// 判断为点击事件
|
||||||
setTimeout(() => showModal(), 100);
|
setTimeout(() => showModal(), 100);
|
||||||
}
|
}
|
||||||
|
|
@ -424,41 +198,22 @@
|
||||||
widgetModal.setAttribute('aria-labelledby', 'widget-modal-title');
|
widgetModal.setAttribute('aria-labelledby', 'widget-modal-title');
|
||||||
widgetModal.setAttribute('data-theme', currentTheme);
|
widgetModal.setAttribute('data-theme', currentTheme);
|
||||||
|
|
||||||
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
|
|
||||||
if (modalPosition === 'fixed') {
|
|
||||||
widgetModal.classList.add('widget-bot-modal-fixed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const modalContent = document.createElement('div');
|
const modalContent = document.createElement('div');
|
||||||
modalContent.className = 'widget-bot-modal-content';
|
modalContent.className = 'widget-bot-modal-content';
|
||||||
if (modalPosition === 'fixed') {
|
|
||||||
modalContent.classList.add('widget-bot-modal-content-fixed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建关闭按钮(透明框)
|
// 创建关闭按钮
|
||||||
const closeBtn = document.createElement('button');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.className = 'widget-bot-close-btn';
|
closeBtn.className = 'widget-bot-close-btn';
|
||||||
|
closeBtn.innerHTML = '<svg t="1752218667372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4632" id="mx_n_1752218667373" width="32" height="32"><path d="M512 939.19762963a427.19762963 427.19762963 0 1 1 0-854.39525926 427.19762963 427.19762963 0 0 1 0 854.39525926z m0-482.08605274L396.47540505 341.53519999a19.41807408 19.41807408 0 0 0-27.44421216 0l-27.44421097 27.44421217a19.41807408 19.41807408 0 0 0 0 27.44421095L457.00801422 512l-115.47281423 115.52459495a19.41807408 19.41807408 0 0 0 0 27.44421216l27.44421217 27.44421097a19.41807408 19.41807408 0 0 0 27.44421095 0L512 566.99198578l115.52459495 115.47281423a19.41807408 19.41807408 0 0 0 27.44421216 0l27.44421097-27.44421217a19.41807408 19.41807408 0 0 0 0-27.44421095l-115.47281424-115.47281423 115.47281424-115.57637689a19.41807408 19.41807408 0 0 0 0-27.44421095l-27.44421097-27.44421096a19.41807408 19.41807408 0 0 0-27.44421216 0L512 457.00801422z" p-id="4633" fill="#ffffff"></path></svg>'
|
||||||
closeBtn.setAttribute('aria-label', '关闭窗口');
|
closeBtn.setAttribute('aria-label', '关闭窗口');
|
||||||
closeBtn.setAttribute('type', 'button');
|
closeBtn.setAttribute('type', 'button');
|
||||||
|
closeBtn.addEventListener('click', hideModal);
|
||||||
// 创建一个内部元素来处理实际的点击事件(因为按钮设置了 pointer-events: none)
|
|
||||||
const closeBtnArea = document.createElement('div');
|
|
||||||
closeBtnArea.style.width = '100%';
|
|
||||||
closeBtnArea.style.height = '100%';
|
|
||||||
closeBtnArea.style.pointerEvents = 'auto'; // 内部元素可以接收事件
|
|
||||||
closeBtnArea.style.cursor = 'pointer';
|
|
||||||
closeBtnArea.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
hideModal();
|
|
||||||
});
|
|
||||||
closeBtn.appendChild(closeBtnArea);
|
|
||||||
|
|
||||||
// 创建iframe
|
// 创建iframe
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.className = 'widget-bot-iframe';
|
iframe.className = 'widget-bot-iframe';
|
||||||
iframe.src = `${widgetDomain}/widget`;
|
iframe.src = `${widgetDomain}/widget`;
|
||||||
iframe.setAttribute('title', `${widgetInfo.btn_text || '在线客服'}服务窗口`);
|
iframe.setAttribute('title', `${widgetInfo.btn_text}服务窗口`);
|
||||||
iframe.setAttribute('allow', 'camera; microphone; geolocation');
|
iframe.setAttribute('allow', 'camera; microphone; geolocation');
|
||||||
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-presentation');
|
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-presentation');
|
||||||
|
|
||||||
|
|
@ -469,156 +224,6 @@
|
||||||
document.body.appendChild(widgetModal);
|
document.body.appendChild(widgetModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测是否为移动端
|
|
||||||
function isMobile() {
|
|
||||||
return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 智能定位弹框(follow模式)
|
|
||||||
function positionModalFollow(modalContent) {
|
|
||||||
if (!widgetButton || !modalContent) return;
|
|
||||||
|
|
||||||
// 移动端强制居中显示
|
|
||||||
if (isMobile()) {
|
|
||||||
modalContent.style.position = 'relative';
|
|
||||||
modalContent.style.top = 'auto';
|
|
||||||
modalContent.style.left = 'auto';
|
|
||||||
modalContent.style.right = 'auto';
|
|
||||||
modalContent.style.bottom = 'auto';
|
|
||||||
modalContent.style.margin = 'auto';
|
|
||||||
modalContent.style.width = 'calc(100% - 32px)';
|
|
||||||
modalContent.style.height = 'auto';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const buttonRect = widgetButton.getBoundingClientRect();
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
const windowHeight = window.innerHeight;
|
|
||||||
const margin = 16; // 距离屏幕边缘的最小距离
|
|
||||||
const buttonGap = 16; // 弹框和按钮之间的最小距离
|
|
||||||
|
|
||||||
// 先设置一个临时位置来获取弹框尺寸
|
|
||||||
const originalPosition = modalContent.style.position;
|
|
||||||
const originalTop = modalContent.style.top;
|
|
||||||
const originalLeft = modalContent.style.left;
|
|
||||||
const originalVisibility = modalContent.style.visibility;
|
|
||||||
const originalDisplay = modalContent.style.display;
|
|
||||||
|
|
||||||
modalContent.style.position = 'absolute';
|
|
||||||
modalContent.style.top = '0';
|
|
||||||
modalContent.style.left = '0';
|
|
||||||
modalContent.style.visibility = 'hidden';
|
|
||||||
modalContent.style.display = 'block';
|
|
||||||
|
|
||||||
const modalRect = modalContent.getBoundingClientRect();
|
|
||||||
const modalWidth = modalRect.width;
|
|
||||||
const modalHeight = modalRect.height;
|
|
||||||
|
|
||||||
modalContent.style.visibility = originalVisibility || 'visible';
|
|
||||||
modalContent.style.display = originalDisplay || 'block';
|
|
||||||
|
|
||||||
// 计算按钮中心点
|
|
||||||
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
|
|
||||||
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
|
|
||||||
|
|
||||||
// 判断按钮在屏幕的哪一侧
|
|
||||||
const isLeftSide = buttonCenterX < windowWidth / 2;
|
|
||||||
const isTopSide = buttonCenterY < windowHeight / 2;
|
|
||||||
|
|
||||||
// 智能选择弹框位置,确保完整显示
|
|
||||||
let finalTop, finalBottom, finalLeft, finalRight;
|
|
||||||
|
|
||||||
if (isLeftSide) {
|
|
||||||
// 按钮在左侧,弹框优先显示在右侧(按钮右侧)
|
|
||||||
finalLeft = buttonRect.right + buttonGap;
|
|
||||||
finalRight = 'auto';
|
|
||||||
|
|
||||||
// 如果右侧空间不够,显示在左侧(按钮左侧)
|
|
||||||
if (finalLeft + modalWidth > windowWidth - margin) {
|
|
||||||
finalLeft = 'auto';
|
|
||||||
finalRight = windowWidth - buttonRect.left + buttonGap;
|
|
||||||
// 如果左侧空间也不够,则贴左边(但保持与按钮的距离)
|
|
||||||
if (buttonRect.left - buttonGap - modalWidth < margin) {
|
|
||||||
finalLeft = margin;
|
|
||||||
finalRight = 'auto';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 按钮在右侧,弹框优先显示在左侧(按钮左侧)
|
|
||||||
finalLeft = 'auto';
|
|
||||||
finalRight = windowWidth - buttonRect.left + buttonGap;
|
|
||||||
|
|
||||||
// 如果左侧空间不够,显示在右侧(按钮右侧)
|
|
||||||
if (buttonRect.left - buttonGap - modalWidth < margin) {
|
|
||||||
finalRight = 'auto';
|
|
||||||
finalLeft = buttonRect.right + buttonGap;
|
|
||||||
// 如果右侧空间也不够,则贴右边(但保持与按钮的距离)
|
|
||||||
if (finalLeft + modalWidth > windowWidth - margin) {
|
|
||||||
finalLeft = 'auto';
|
|
||||||
finalRight = margin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 垂直方向:优先与按钮顶部对齐
|
|
||||||
// 弹框顶部与按钮顶部对齐
|
|
||||||
finalTop = buttonRect.top;
|
|
||||||
finalBottom = 'auto';
|
|
||||||
|
|
||||||
// 如果弹框底部超出屏幕,则向上调整,确保弹框完整显示在屏幕内
|
|
||||||
if (finalTop + modalHeight > windowHeight - margin) {
|
|
||||||
// 计算向上调整后的位置
|
|
||||||
const adjustedTop = windowHeight - margin - modalHeight;
|
|
||||||
// 如果调整后的位置仍然在按钮上方,则使用调整后的位置
|
|
||||||
if (adjustedTop >= margin) {
|
|
||||||
finalTop = adjustedTop;
|
|
||||||
} else {
|
|
||||||
// 如果调整后仍然超出,则贴顶部
|
|
||||||
finalTop = margin;
|
|
||||||
}
|
|
||||||
} else if (finalTop < margin) {
|
|
||||||
// 如果弹框顶部超出屏幕,则贴顶部
|
|
||||||
finalTop = margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用最终位置
|
|
||||||
modalContent.style.top = finalTop !== undefined ? (typeof finalTop === 'string' ? finalTop : finalTop + 'px') : 'auto';
|
|
||||||
modalContent.style.bottom = finalBottom !== undefined ? (typeof finalBottom === 'string' ? finalBottom : finalBottom + 'px') : 'auto';
|
|
||||||
modalContent.style.left = finalLeft !== undefined ? (typeof finalLeft === 'string' ? finalLeft : finalLeft + 'px') : 'auto';
|
|
||||||
modalContent.style.right = finalRight !== undefined ? (typeof finalRight === 'string' ? finalRight : finalRight + 'px') : 'auto';
|
|
||||||
|
|
||||||
// 最终检查并修正,确保弹框完全在屏幕内
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const finalModalRect = modalContent.getBoundingClientRect();
|
|
||||||
|
|
||||||
// 修正左边界
|
|
||||||
if (finalModalRect.left < margin) {
|
|
||||||
modalContent.style.left = margin + 'px';
|
|
||||||
modalContent.style.right = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修正右边界
|
|
||||||
if (finalModalRect.right > windowWidth - margin) {
|
|
||||||
modalContent.style.right = margin + 'px';
|
|
||||||
modalContent.style.left = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修正上边界
|
|
||||||
if (finalModalRect.top < margin) {
|
|
||||||
modalContent.style.top = margin + 'px';
|
|
||||||
modalContent.style.bottom = 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修正下边界
|
|
||||||
if (finalModalRect.bottom > windowHeight - margin) {
|
|
||||||
modalContent.style.bottom = margin + 'px';
|
|
||||||
modalContent.style.top = 'auto';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示模态框
|
// 显示模态框
|
||||||
function showModal() {
|
function showModal() {
|
||||||
if (!widgetModal) return;
|
if (!widgetModal) return;
|
||||||
|
|
@ -626,31 +231,27 @@
|
||||||
widgetModal.style.display = 'flex';
|
widgetModal.style.display = 'flex';
|
||||||
document.body.classList.add('widget-bot-modal-open');
|
document.body.classList.add('widget-bot-modal-open');
|
||||||
|
|
||||||
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
|
// 计算模态框位置
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const buttonRect = widgetButton.getBoundingClientRect();
|
||||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||||
|
|
||||||
// 移动端强制居中显示
|
if (modalContent) {
|
||||||
if (isMobile()) {
|
// 设置模态框位置:距离按钮16px,距离底部24px
|
||||||
modalContent.style.position = 'relative';
|
const modalBottom = 24;
|
||||||
modalContent.style.top = 'auto';
|
const modalRight = Math.max(16, window.innerWidth - buttonRect.left + 16);
|
||||||
modalContent.style.left = 'auto';
|
|
||||||
modalContent.style.right = 'auto';
|
modalContent.style.bottom = modalBottom + 'px';
|
||||||
modalContent.style.bottom = 'auto';
|
modalContent.style.right = modalRight + 'px';
|
||||||
modalContent.style.margin = 'auto';
|
|
||||||
modalContent.style.width = 'calc(100% - 32px)';
|
// 确保模态框不会超出屏幕
|
||||||
modalContent.style.height = 'auto';
|
const modalRect = modalContent.getBoundingClientRect();
|
||||||
} else if (modalPosition === 'fixed') {
|
if (modalRect.left < 16) {
|
||||||
// 桌面端固定模式:居中展示
|
modalContent.style.right = '16px';
|
||||||
modalContent.style.position = 'relative';
|
modalContent.style.left = '16px';
|
||||||
modalContent.style.top = 'auto';
|
|
||||||
modalContent.style.left = 'auto';
|
|
||||||
modalContent.style.right = 'auto';
|
|
||||||
modalContent.style.bottom = 'auto';
|
|
||||||
modalContent.style.margin = 'auto';
|
|
||||||
} else {
|
|
||||||
// 桌面端跟随模式:跟随按钮位置 - 智能定位,确保弹框完整显示在屏幕内
|
|
||||||
positionModalFollow(modalContent);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 添加ESC键关闭功能
|
// 添加ESC键关闭功能
|
||||||
document.addEventListener('keydown', handleEscKey);
|
document.addEventListener('keydown', handleEscKey);
|
||||||
|
|
@ -686,98 +287,42 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
hasDragged = false; // 重置拖拽标记
|
|
||||||
|
|
||||||
const rect = widgetButton.getBoundingClientRect();
|
const rect = widgetButton.getBoundingClientRect();
|
||||||
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
||||||
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
||||||
|
|
||||||
// 记录拖拽开始位置
|
dragOffset.x = clientX - rect.left;
|
||||||
dragStartPos.x = clientX;
|
dragOffset.y = clientY - rect.top;
|
||||||
dragStartPos.y = clientY;
|
|
||||||
|
|
||||||
// 缓存按钮尺寸,避免拖拽过程中频繁读取
|
// 清除bottom定位,使用top定位
|
||||||
buttonSize.width = rect.width;
|
|
||||||
buttonSize.height = rect.height;
|
|
||||||
|
|
||||||
// 先清除 transform,确保获取真实的位置
|
|
||||||
widgetButton.style.transform = 'none';
|
|
||||||
|
|
||||||
// 重新获取位置(清除 transform 后的真实位置)
|
|
||||||
const realRect = widgetButton.getBoundingClientRect();
|
|
||||||
|
|
||||||
// 记录初始位置(基于清除 transform 后的真实位置)
|
|
||||||
initialPosition.left = realRect.left;
|
|
||||||
initialPosition.top = realRect.top;
|
|
||||||
|
|
||||||
dragOffset.x = clientX - realRect.left;
|
|
||||||
dragOffset.y = clientY - realRect.top;
|
|
||||||
|
|
||||||
// 确保使用 fixed 定位,使用真实位置
|
|
||||||
widgetButton.style.position = 'fixed';
|
|
||||||
widgetButton.style.top = realRect.top + 'px';
|
|
||||||
widgetButton.style.left = realRect.left + 'px';
|
|
||||||
widgetButton.style.right = 'auto';
|
|
||||||
widgetButton.style.bottom = 'auto';
|
widgetButton.style.bottom = 'auto';
|
||||||
|
widgetButton.style.top = rect.top + 'px';
|
||||||
|
widgetButton.style.position = 'fixed';
|
||||||
|
|
||||||
// 禁用过渡效果,提升拖拽性能
|
document.addEventListener('mousemove', drag);
|
||||||
widgetButton.style.transition = 'none';
|
|
||||||
|
|
||||||
// 提示浏览器优化(使用 left/top 定位)
|
|
||||||
widgetButton.style.willChange = 'left, top';
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', drag, { passive: false });
|
|
||||||
document.addEventListener('mouseup', stopDrag);
|
document.addEventListener('mouseup', stopDrag);
|
||||||
|
|
||||||
widgetButton.classList.add('dragging');
|
widgetButton.classList.add('dragging');
|
||||||
widgetButton.style.zIndex = '10001';
|
widgetButton.style.zIndex = '10001';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽中 - 直接更新位置,实现丝滑跟随
|
// 拖拽中
|
||||||
function drag(e) {
|
function drag(e) {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
if (e.preventDefault) {
|
if (e.preventDefault) {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
}
|
};
|
||||||
|
|
||||||
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
|
||||||
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
||||||
|
|
||||||
// 检测是否发生了实际移动(超过5px才认为是拖拽)
|
|
||||||
const moveDistance = Math.sqrt(
|
|
||||||
Math.pow(clientX - dragStartPos.x, 2) +
|
|
||||||
Math.pow(clientY - dragStartPos.y, 2)
|
|
||||||
);
|
|
||||||
if (moveDistance > 5) {
|
|
||||||
hasDragged = true;
|
|
||||||
}
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
const windowHeight = window.innerHeight;
|
|
||||||
const buttonWidth = buttonSize.width;
|
|
||||||
const buttonHeight = buttonSize.height;
|
|
||||||
|
|
||||||
// 直接基于鼠标位置计算新位置
|
|
||||||
// 鼠标位置减去拖拽偏移量,得到按钮左上角应该的位置
|
|
||||||
const newLeft = clientX - dragOffset.x;
|
|
||||||
const newTop = clientY - dragOffset.y;
|
const newTop = clientY - dragOffset.y;
|
||||||
|
const maxTop = window.innerHeight - widgetButton.offsetHeight;
|
||||||
|
|
||||||
// 垂直位置:限制在屏幕范围内,距离顶部和底部最小距离为 24px
|
// 限制在屏幕范围内
|
||||||
const minTop = 24;
|
const constrainedTop = Math.max(0, Math.min(newTop, maxTop));
|
||||||
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
|
|
||||||
const constrainedTop = Math.max(minTop, Math.min(newTop, maxTop));
|
|
||||||
|
|
||||||
// 水平位置:限制在屏幕范围内
|
|
||||||
const maxLeft = windowWidth - buttonWidth;
|
|
||||||
const constrainedLeft = Math.max(0, Math.min(newLeft, maxLeft));
|
|
||||||
|
|
||||||
// 直接使用 left/top 定位,实现无延迟的丝滑跟随
|
|
||||||
// 使用 transform: none 确保不会有任何 transform 干扰
|
|
||||||
widgetButton.style.left = constrainedLeft + 'px';
|
|
||||||
widgetButton.style.top = constrainedTop + 'px';
|
widgetButton.style.top = constrainedTop + 'px';
|
||||||
widgetButton.style.right = 'auto';
|
|
||||||
widgetButton.style.bottom = 'auto';
|
|
||||||
widgetButton.style.transform = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止拖拽
|
// 停止拖拽
|
||||||
|
|
@ -785,75 +330,26 @@
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
|
|
||||||
// 取消待执行的动画帧
|
|
||||||
if (dragAnimationFrame) {
|
|
||||||
cancelAnimationFrame(dragAnimationFrame);
|
|
||||||
dragAnimationFrame = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.removeEventListener('mousemove', drag);
|
document.removeEventListener('mousemove', drag);
|
||||||
document.removeEventListener('mouseup', stopDrag);
|
document.removeEventListener('mouseup', stopDrag);
|
||||||
|
|
||||||
widgetButton.classList.remove('dragging');
|
widgetButton.classList.remove('dragging');
|
||||||
widgetButton.style.zIndex = '9999';
|
widgetButton.style.zIndex = '9999';
|
||||||
|
|
||||||
// 恢复过渡效果
|
// 吸附到右侧,恢复bottom定位
|
||||||
widgetButton.style.transition = '';
|
|
||||||
widgetButton.style.willChange = '';
|
|
||||||
|
|
||||||
// 根据按钮类型和当前位置进行最终定位
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const buttonRect = widgetButton.getBoundingClientRect();
|
const currentTop = parseInt(widgetButton.style.top);
|
||||||
const currentLeft = buttonRect.left;
|
|
||||||
const currentTop = buttonRect.top;
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
const buttonWidth = buttonSize.width;
|
const buttonHeight = widgetButton.offsetHeight;
|
||||||
const buttonHeight = buttonSize.height;
|
|
||||||
|
|
||||||
// 两种模式使用相同的停止拖拽逻辑:只能左右侧边缘吸附
|
// 计算距离底部的位置
|
||||||
// 根据按钮实际位置判断左右,保持当前位置
|
const bottomPosition = windowHeight - currentTop - buttonHeight;
|
||||||
const screenCenterX = windowWidth / 2;
|
|
||||||
const buttonCenterX = currentLeft + buttonWidth / 2;
|
|
||||||
const isLeftSide = buttonCenterX < screenCenterX;
|
|
||||||
const sideDistance = 16; // 距离边缘的距离
|
|
||||||
|
|
||||||
// 垂直位置:保持在当前位置,限制在屏幕范围内,距离顶部和底部最小距离为 24px
|
// 恢复right和bottom定位,清除top
|
||||||
const minTop = 24;
|
widgetButton.style.right = '0';
|
||||||
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
|
widgetButton.style.bottom = Math.max(20, bottomPosition) + 'px';
|
||||||
const finalTop = Math.max(minTop, Math.min(currentTop, maxTop));
|
widgetButton.style.top = 'auto';
|
||||||
let finalLeft;
|
|
||||||
|
|
||||||
// 水平位置:距离左右边16px
|
|
||||||
if (isLeftSide) {
|
|
||||||
finalLeft = sideDistance;
|
|
||||||
widgetButton.style.left = sideDistance + 'px';
|
|
||||||
widgetButton.style.right = 'auto';
|
|
||||||
} else {
|
|
||||||
finalLeft = windowWidth - sideDistance - buttonWidth;
|
|
||||||
widgetButton.style.right = sideDistance + 'px';
|
|
||||||
widgetButton.style.left = 'auto';
|
widgetButton.style.left = 'auto';
|
||||||
}
|
|
||||||
|
|
||||||
widgetButton.style.top = finalTop + 'px';
|
|
||||||
widgetButton.style.bottom = 'auto';
|
|
||||||
|
|
||||||
// 清除 transform,使用 left/top 定位
|
|
||||||
widgetButton.style.transform = 'none';
|
|
||||||
|
|
||||||
// 更新 border-radius(现在都是24px圆角)
|
|
||||||
widgetButton.style.borderRadius = '24px';
|
|
||||||
|
|
||||||
// 更新初始位置,为下次拖拽做准备
|
|
||||||
if (finalLeft !== undefined && finalTop !== undefined) {
|
|
||||||
initialPosition.left = finalLeft;
|
|
||||||
initialPosition.top = finalTop;
|
|
||||||
} else {
|
|
||||||
// 如果未定义,使用当前实际位置
|
|
||||||
initialPosition.left = buttonRect.left;
|
|
||||||
initialPosition.top = buttonRect.top;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -894,30 +390,19 @@
|
||||||
// 窗口大小改变时重新定位
|
// 窗口大小改变时重新定位
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener('resize', function () {
|
||||||
if (widgetModal && widgetModal.style.display === 'flex') {
|
if (widgetModal && widgetModal.style.display === 'flex') {
|
||||||
|
// 重新计算模态框位置
|
||||||
|
setTimeout(() => {
|
||||||
|
const buttonRect = widgetButton.getBoundingClientRect();
|
||||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||||
if (!modalContent) return;
|
|
||||||
|
|
||||||
// 移动端强制居中显示
|
if (modalContent) {
|
||||||
if (isMobile()) {
|
const modalBottom = 24;
|
||||||
modalContent.style.position = 'relative';
|
const modalRight = Math.max(16, window.innerWidth - buttonRect.left + 16);
|
||||||
modalContent.style.top = 'auto';
|
|
||||||
modalContent.style.left = 'auto';
|
modalContent.style.bottom = modalBottom + 'px';
|
||||||
modalContent.style.right = 'auto';
|
modalContent.style.right = modalRight + 'px';
|
||||||
modalContent.style.bottom = 'auto';
|
|
||||||
modalContent.style.margin = 'auto';
|
|
||||||
modalContent.style.width = 'calc(100% - 32px)';
|
|
||||||
modalContent.style.height = 'auto';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}, 100);
|
||||||
const modalPosition = widgetInfo?.modal_position || defaultModalPosition;
|
|
||||||
if (modalPosition === 'fixed') {
|
|
||||||
// 固定居中模式不需要重新定位
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新计算模态框位置(使用智能定位)
|
|
||||||
positionModalFollow(modalContent);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -938,13 +423,8 @@
|
||||||
if (widgetModal) {
|
if (widgetModal) {
|
||||||
widgetModal.remove();
|
widgetModal.remove();
|
||||||
}
|
}
|
||||||
if (customTriggerElement && customTriggerHandler) {
|
|
||||||
customTriggerElement.removeEventListener('click', customTriggerHandler);
|
|
||||||
customTriggerElement.removeAttribute('data-widget-trigger-attached');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 启动
|
// 启动
|
||||||
init();
|
init();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { ThemeStoreProvider } from '@/provider/themeStore';
|
||||||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||||
import { getShareProV1AuthInfo } from '@/request/pro/ShareAuth';
|
import { getShareProV1AuthInfo } from '@/request/pro/ShareAuth';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v16-appRouter';
|
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
|
||||||
import type { Metadata, Viewport } from 'next';
|
import type { Metadata, Viewport } from 'next';
|
||||||
import localFont from 'next/font/local';
|
import localFont from 'next/font/local';
|
||||||
import { headers, cookies } from 'next/headers';
|
import { headers, cookies } from 'next/headers';
|
||||||
|
|
@ -92,7 +92,7 @@ const Layout = async ({
|
||||||
return (
|
return (
|
||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
<body
|
<body
|
||||||
className={`${gilory.variable} ${themeMode === 'dark' ? 'dark' : 'light'}`}
|
className={`${gilory.variable} ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||||
>
|
>
|
||||||
<AppRouterCacheProvider>
|
<AppRouterCacheProvider>
|
||||||
<ThemeStoreProvider themeMode={themeMode}>
|
<ThemeStoreProvider themeMode={themeMode}>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
--color-primary-main: #6e73fe;
|
--color-primary-main: #6e73fe;
|
||||||
|
|
||||||
/* 代码块颜色 */
|
/* 代码块颜色 */
|
||||||
--code-bg: rgba(0, 0, 0, 0.03);
|
--code-bg: #ffffff;
|
||||||
--code-color: #21222d;
|
--code-color: #21222d;
|
||||||
--inline-code-bg: #fff5f5;
|
--inline-code-bg: #fff5f5;
|
||||||
--inline-code-color: #ff502c;
|
--inline-code-color: #ff502c;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import StoreProvider from '@/provider';
|
import StoreProvider from '@/provider';
|
||||||
|
import { darkThemeWidget, lightThemeWidget } from '@/theme';
|
||||||
import { getShareV1AppWidgetInfo } from '@/request/ShareApp';
|
import { getShareV1AppWidgetInfo } from '@/request/ShareApp';
|
||||||
|
import { ThemeProvider } from '@ctzhian/ui';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Layout = async ({
|
const Layout = async ({
|
||||||
|
|
@ -9,7 +12,18 @@ const Layout = async ({
|
||||||
}>) => {
|
}>) => {
|
||||||
const widgetDetail: any = await getShareV1AppWidgetInfo();
|
const widgetDetail: any = await getShareV1AppWidgetInfo();
|
||||||
|
|
||||||
return <StoreProvider widget={widgetDetail}>{children}</StoreProvider>;
|
const themeMode =
|
||||||
|
widgetDetail?.settings?.widget_bot_settings?.theme_mode || 'light';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider
|
||||||
|
theme={themeMode === 'dark' ? darkThemeWidget : lightThemeWidget}
|
||||||
|
>
|
||||||
|
<StoreProvider widget={widgetDetail} themeMode={themeMode || 'light'}>
|
||||||
|
{children}
|
||||||
|
</StoreProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,17 @@
|
||||||
import Widget from '@/views/widget';
|
import Widget from '@/views/widget';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
export default Widget;
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Widget />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
|
||||||
|
|
@ -109,19 +109,10 @@ export type WidgetInfo = {
|
||||||
search_placeholder: string;
|
search_placeholder: string;
|
||||||
recommend_questions: string[];
|
recommend_questions: string[];
|
||||||
widget_bot_settings: {
|
widget_bot_settings: {
|
||||||
btn_logo?: string;
|
btn_logo: string;
|
||||||
btn_text?: string;
|
btn_text: string;
|
||||||
btn_style?: string;
|
is_open: boolean;
|
||||||
btn_id?: string;
|
theme_mode: 'light' | 'dark';
|
||||||
btn_position?: string;
|
|
||||||
modal_position?: string;
|
|
||||||
is_open?: boolean;
|
|
||||||
recommend_node_ids?: string[];
|
|
||||||
recommend_questions?: string[];
|
|
||||||
theme_mode?: string;
|
|
||||||
search_mode?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
disclaimer?: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Logo from '@/assets/images/logo.png';
|
import Logo from '@/assets/images/logo.png';
|
||||||
import { Stack, Box, IconButton, alpha, Tooltip } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { postShareProV1AuthLogout } from '@/request/pro/ShareAuth';
|
|
||||||
import { IconDengchu } from '@panda-wiki/icons';
|
|
||||||
import { useStore } from '@/provider';
|
import { useStore } from '@/provider';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import ErrorIcon from '@mui/icons-material/Error';
|
|
||||||
import { Modal } from '@ctzhian/ui';
|
|
||||||
import {
|
import {
|
||||||
Header as CustomHeader,
|
Header as CustomHeader,
|
||||||
WelcomeHeader as WelcomeHeaderComponent,
|
WelcomeHeader as WelcomeHeaderComponent,
|
||||||
|
|
@ -20,58 +16,8 @@ interface HeaderProps {
|
||||||
isWelcomePage?: boolean;
|
isWelcomePage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogoutButton = () => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const handleLogout = () => {
|
|
||||||
return postShareProV1AuthLogout().then(() => {
|
|
||||||
// 使用当前页面的协议(http 或 https)
|
|
||||||
const protocol = window.location.protocol;
|
|
||||||
const host = window.location.host;
|
|
||||||
window.location.href = `${protocol}//${host}/auth/login`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
title={
|
|
||||||
<Stack direction='row' alignItems='center' gap={1}>
|
|
||||||
<ErrorIcon sx={{ fontSize: 24, color: 'warning.main' }} />
|
|
||||||
<Box sx={{ mt: '2px' }}>提示</Box>
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
open={open}
|
|
||||||
okText='确定'
|
|
||||||
cancelText='取消'
|
|
||||||
onCancel={() => setOpen(false)}
|
|
||||||
onOk={handleLogout}
|
|
||||||
closable={false}
|
|
||||||
>
|
|
||||||
<Box sx={{ pl: 4 }}>确定要退出登录吗?</Box>
|
|
||||||
</Modal>
|
|
||||||
<Tooltip title='退出登录' arrow>
|
|
||||||
<IconButton size='small' onClick={() => setOpen(true)}>
|
|
||||||
<IconDengchu
|
|
||||||
sx={theme => ({
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: alpha(theme.palette.text.primary, 0.65),
|
|
||||||
fontSize: 24,
|
|
||||||
'&:hover': { color: theme.palette.primary.main },
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Header = ({ isDocPage = false, isWelcomePage = false }: HeaderProps) => {
|
const Header = ({ isDocPage = false, isWelcomePage = false }: HeaderProps) => {
|
||||||
const {
|
const { mobile = false, kbDetail, catalogWidth, setQaModalOpen } = useStore();
|
||||||
mobile = false,
|
|
||||||
kbDetail,
|
|
||||||
catalogWidth,
|
|
||||||
setQaModalOpen,
|
|
||||||
authInfo,
|
|
||||||
} = useStore();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const docWidth = useMemo(() => {
|
const docWidth = useMemo(() => {
|
||||||
if (isWelcomePage) return 'full';
|
if (isWelcomePage) return 'full';
|
||||||
|
|
@ -109,23 +55,16 @@ const Header = ({ isDocPage = false, isWelcomePage = false }: HeaderProps) => {
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onQaClick={() => setQaModalOpen?.(true)}
|
onQaClick={() => setQaModalOpen?.(true)}
|
||||||
>
|
>
|
||||||
<Stack sx={{ ml: 2 }} direction='row' alignItems='center' gap={1}>
|
<Box sx={{ ml: 2 }}>
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
{!!authInfo && <LogoutButton />}
|
</Box>
|
||||||
</Stack>
|
|
||||||
<QaModal />
|
<QaModal />
|
||||||
</CustomHeader>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WelcomeHeader = () => {
|
export const WelcomeHeader = () => {
|
||||||
const {
|
const { mobile = false, kbDetail, catalogWidth, setQaModalOpen } = useStore();
|
||||||
mobile = false,
|
|
||||||
kbDetail,
|
|
||||||
catalogWidth,
|
|
||||||
setQaModalOpen,
|
|
||||||
authInfo,
|
|
||||||
} = useStore();
|
|
||||||
const handleSearch = (value?: string, type: 'chat' | 'search' = 'chat') => {
|
const handleSearch = (value?: string, type: 'chat' | 'search' = 'chat') => {
|
||||||
if (value?.trim()) {
|
if (value?.trim()) {
|
||||||
if (type === 'chat') {
|
if (type === 'chat') {
|
||||||
|
|
@ -152,7 +91,6 @@ export const WelcomeHeader = () => {
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onQaClick={() => setQaModalOpen?.(true)}
|
onQaClick={() => setQaModalOpen?.(true)}
|
||||||
>
|
>
|
||||||
<Box sx={{ ml: 2 }}>{!!authInfo && <LogoutButton />}</Box>
|
|
||||||
<QaModal />
|
<QaModal />
|
||||||
</WelcomeHeaderComponent>
|
</WelcomeHeaderComponent>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import React, { useState, useEffect } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { styled, SvgIcon, SvgIconProps } from '@mui/material';
|
import { styled, SvgIcon, SvgIconProps } from '@mui/material';
|
||||||
|
|
||||||
// ==================== 图片数据缓存工具函数 ====================
|
// ==================== 图片数据缓存 ====================
|
||||||
|
// 全局图片 blob URL 缓存,避免重复请求 OSS
|
||||||
|
const imageBlobCache = new Map<string, string>();
|
||||||
|
|
||||||
// 下载图片并转换为 blob URL
|
// 下载图片并转换为 blob URL
|
||||||
const fetchImageAsBlob = async (
|
const fetchImageAsBlob = async (src: string): Promise<string> => {
|
||||||
src: string,
|
|
||||||
imageBlobCache: Map<string, string>,
|
|
||||||
): Promise<string> => {
|
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
if (imageBlobCache.has(src)) {
|
if (imageBlobCache.has(src)) {
|
||||||
return imageBlobCache.get(src)!;
|
return imageBlobCache.get(src)!;
|
||||||
|
|
@ -39,8 +39,12 @@ const fetchImageAsBlob = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清理图片 blob 缓存
|
// 导出获取图片 blob URL 的函数
|
||||||
export const clearImageBlobCache = (imageBlobCache: Map<string, string>) => {
|
export const getImageBlobUrl = (src: string): string | null => {
|
||||||
|
return imageBlobCache.get(src) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearImageBlobCache = () => {
|
||||||
imageBlobCache.forEach(url => {
|
imageBlobCache.forEach(url => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
});
|
});
|
||||||
|
|
@ -50,7 +54,7 @@ export const clearImageBlobCache = (imageBlobCache: Map<string, string>) => {
|
||||||
const StyledErrorContainer = styled('div')(({ theme }) => ({
|
const StyledErrorContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: theme.spacing(1, 6),
|
padding: theme.spacing(2),
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
|
|
@ -67,7 +71,7 @@ const StyledErrorContainer = styled('div')(({ theme }) => ({
|
||||||
|
|
||||||
const StyledErrorText = styled('div')(() => ({
|
const StyledErrorText = styled('div')(() => ({
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
marginBottom: 10,
|
marginBottom: 16,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ImageErrorIcon = (props: SvgIconProps) => {
|
export const ImageErrorIcon = (props: SvgIconProps) => {
|
||||||
|
|
@ -98,7 +102,7 @@ export const ImageErrorIcon = (props: SvgIconProps) => {
|
||||||
const ImageErrorDisplay: React.FC = () => (
|
const ImageErrorDisplay: React.FC = () => (
|
||||||
<StyledErrorContainer>
|
<StyledErrorContainer>
|
||||||
<ImageErrorIcon
|
<ImageErrorIcon
|
||||||
sx={{ color: 'var(--mui-palette-text-tertiary)', fontSize: 140 }}
|
sx={{ color: 'var(--mui-palette-text-tertiary)', fontSize: 160 }}
|
||||||
/>
|
/>
|
||||||
<StyledErrorText>图片加载失败</StyledErrorText>
|
<StyledErrorText>图片加载失败</StyledErrorText>
|
||||||
</StyledErrorContainer>
|
</StyledErrorContainer>
|
||||||
|
|
@ -112,7 +116,7 @@ interface ImageComponentProps {
|
||||||
imageIndex: number;
|
imageIndex: number;
|
||||||
onLoad: (index: number, html: string) => void;
|
onLoad: (index: number, html: string) => void;
|
||||||
onError: (index: number, html: string) => void;
|
onError: (index: number, html: string) => void;
|
||||||
imageBlobCache: Map<string, string>;
|
onImageClick: (src: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 图片组件 ====================
|
// ==================== 图片组件 ====================
|
||||||
|
|
@ -123,7 +127,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
imageIndex,
|
imageIndex,
|
||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
imageBlobCache,
|
onImageClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>(
|
const [status, setStatus] = useState<'loading' | 'success' | 'error'>(
|
||||||
'loading',
|
'loading',
|
||||||
|
|
@ -145,7 +149,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
// 获取图片 blob URL
|
// 获取图片 blob URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
fetchImageAsBlob(src, imageBlobCache)
|
fetchImageAsBlob(src)
|
||||||
.then(url => {
|
.then(url => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setBlobUrl(url);
|
setBlobUrl(url);
|
||||||
|
|
@ -162,7 +166,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
};
|
};
|
||||||
}, [src, imageBlobCache]);
|
}, [src]);
|
||||||
|
|
||||||
// 解析自定义样式
|
// 解析自定义样式
|
||||||
const parseStyleString = (styleStr: string) => {
|
const parseStyleString = (styleStr: string) => {
|
||||||
|
|
@ -234,8 +238,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
referrerPolicy='no-referrer'
|
referrerPolicy='no-referrer'
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
data-original-src={src}
|
onClick={() => onImageClick(src)} // 传递原始 src 用于预览
|
||||||
className='markdown-image'
|
|
||||||
{...getOtherProps()}
|
{...getOtherProps()}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -261,13 +264,12 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
export interface ImageRendererOptions {
|
export interface ImageRendererOptions {
|
||||||
onImageLoad: (index: number, html: string) => void;
|
onImageLoad: (index: number, html: string) => void;
|
||||||
onImageError: (index: number, html: string) => void;
|
onImageError: (index: number, html: string) => void;
|
||||||
|
onImageClick: (src: string) => void;
|
||||||
imageRenderCache: Map<number, string>;
|
imageRenderCache: Map<number, string>;
|
||||||
imageBlobCache: Map<string, string>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createImageRenderer = (options: ImageRendererOptions) => {
|
export const createImageRenderer = (options: ImageRendererOptions) => {
|
||||||
const { onImageLoad, onImageError, imageRenderCache, imageBlobCache } =
|
const { onImageLoad, onImageError, imageRenderCache, onImageClick } = options;
|
||||||
options;
|
|
||||||
return (
|
return (
|
||||||
src: string,
|
src: string,
|
||||||
alt: string,
|
alt: string,
|
||||||
|
|
@ -277,6 +279,29 @@ export const createImageRenderer = (options: ImageRendererOptions) => {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
const cached = imageRenderCache.get(imageIndex);
|
const cached = imageRenderCache.get(imageIndex);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
// 下一帧对已缓存的DOM绑定原生点击事件,避免事件丢失且不引起重渲染
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const container = document.querySelector(
|
||||||
|
`.image-container-${imageIndex}`,
|
||||||
|
) as HTMLElement | null;
|
||||||
|
if (!container) return;
|
||||||
|
const img = container.querySelector('img') as HTMLImageElement | null;
|
||||||
|
if (!img) return;
|
||||||
|
const alreadyBound = (img as HTMLElement).getAttribute(
|
||||||
|
'data-click-bound',
|
||||||
|
);
|
||||||
|
if (!alreadyBound) {
|
||||||
|
(img as HTMLElement).setAttribute('data-click-bound', '1');
|
||||||
|
img.style.cursor = img.style.cursor || 'pointer';
|
||||||
|
img.addEventListener('click', () => {
|
||||||
|
try {
|
||||||
|
onImageClick(img.src);
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,7 +323,7 @@ export const createImageRenderer = (options: ImageRendererOptions) => {
|
||||||
imageIndex={imageIndex}
|
imageIndex={imageIndex}
|
||||||
onLoad={onImageLoad}
|
onLoad={onImageLoad}
|
||||||
onError={onImageError}
|
onError={onImageError}
|
||||||
imageBlobCache={imageBlobCache}
|
onImageClick={onImageClick}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@ import React, {
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSmartScroll } from '@/hooks';
|
import { useSmartScroll } from '@/hooks';
|
||||||
import { clearImageBlobCache, createImageRenderer } from './imageRenderer';
|
import {
|
||||||
|
clearImageBlobCache,
|
||||||
|
createImageRenderer,
|
||||||
|
getImageBlobUrl,
|
||||||
|
} from './imageRenderer';
|
||||||
import { incrementalRender } from './incrementalRenderer';
|
import { incrementalRender } from './incrementalRenderer';
|
||||||
import { createMermaidRenderer } from './mermaidRenderer';
|
import { createMermaidRenderer } from './mermaidRenderer';
|
||||||
import {
|
import {
|
||||||
|
|
@ -84,8 +88,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
const lastContentRef = useRef<string>('');
|
const lastContentRef = useRef<string>('');
|
||||||
const mdRef = useRef<MarkdownIt | null>(null);
|
const mdRef = useRef<MarkdownIt | null>(null);
|
||||||
const mermaidSuccessIdRef = useRef<Map<number, string>>(new Map());
|
const mermaidSuccessIdRef = useRef<Map<number, string>>(new Map());
|
||||||
const imageRenderCacheRef = useRef<Map<number, string>>(new Map()); // 图片渲染缓存(HTML)
|
const imageRenderCacheRef = useRef<Map<number, string>>(new Map()); // 图片渲染缓存
|
||||||
const imageBlobCacheRef = useRef<Map<string, string>>(new Map()); // 图片 blob URL 缓存
|
|
||||||
|
|
||||||
// 使用智能滚动 hook
|
// 使用智能滚动 hook
|
||||||
const { scrollToBottom } = useSmartScroll({
|
const { scrollToBottom } = useSmartScroll({
|
||||||
|
|
@ -122,8 +125,13 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
createImageRenderer({
|
createImageRenderer({
|
||||||
onImageLoad: handleImageLoad,
|
onImageLoad: handleImageLoad,
|
||||||
onImageError: handleImageError,
|
onImageError: handleImageError,
|
||||||
|
onImageClick: (src: string) => {
|
||||||
|
// 尝试获取缓存的 blob URL,如果不存在则使用原始 src
|
||||||
|
const blobUrl = getImageBlobUrl(src);
|
||||||
|
setPreviewImgBlobUrl(blobUrl || src);
|
||||||
|
setPreviewOpen(true);
|
||||||
|
},
|
||||||
imageRenderCache: imageRenderCacheRef.current,
|
imageRenderCache: imageRenderCacheRef.current,
|
||||||
imageBlobCache: imageBlobCacheRef.current,
|
|
||||||
}),
|
}),
|
||||||
[handleImageLoad, handleImageError],
|
[handleImageLoad, handleImageError],
|
||||||
);
|
);
|
||||||
|
|
@ -150,7 +158,6 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
const originalFenceRender = md.renderer.rules.fence;
|
const originalFenceRender = md.renderer.rules.fence;
|
||||||
// 自定义图片渲染
|
// 自定义图片渲染
|
||||||
let imageCount = 0;
|
let imageCount = 0;
|
||||||
let htmlImageCount = 0; // HTML 标签图片计数
|
|
||||||
let mermaidCount = 0;
|
let mermaidCount = 0;
|
||||||
md.renderer.rules.image = (tokens, idx) => {
|
md.renderer.rules.image = (tokens, idx) => {
|
||||||
imageCount++;
|
imageCount++;
|
||||||
|
|
@ -233,38 +240,6 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 解析 HTML img 标签并提取属性
|
|
||||||
const parseImgTag = (
|
|
||||||
html: string,
|
|
||||||
): {
|
|
||||||
src: string;
|
|
||||||
alt: string;
|
|
||||||
attrs: [string, string][];
|
|
||||||
} | null => {
|
|
||||||
// 匹配 <img> 标签(支持自闭合和普通标签)
|
|
||||||
const imgMatch = html.match(/<img\s+([^>]*?)\/?>/i);
|
|
||||||
if (!imgMatch) return null;
|
|
||||||
|
|
||||||
const attrsString = imgMatch[1];
|
|
||||||
const attrs: [string, string][] = [];
|
|
||||||
let src = '';
|
|
||||||
let alt = '';
|
|
||||||
|
|
||||||
// 解析属性:匹配 name="value" 或 name='value' 或 name=value
|
|
||||||
const attrRegex =
|
|
||||||
/(\w+)(?:=["']([^"']*)["']|=(?:["'])?([^\s>]+)(?:["'])?)?/g;
|
|
||||||
let attrMatch;
|
|
||||||
while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
|
|
||||||
const name = attrMatch[1].toLowerCase();
|
|
||||||
const value = attrMatch[2] || attrMatch[3] || '';
|
|
||||||
attrs.push([name, value]);
|
|
||||||
if (name === 'src') src = value;
|
|
||||||
if (name === 'alt') alt = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { src, alt, attrs };
|
|
||||||
};
|
|
||||||
|
|
||||||
md.renderer.rules.html_block = (
|
md.renderer.rules.html_block = (
|
||||||
tokens,
|
tokens,
|
||||||
idx,
|
idx,
|
||||||
|
|
@ -303,21 +278,6 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
if (content.includes('<error>')) return '<span class="chat-error">';
|
if (content.includes('<error>')) return '<span class="chat-error">';
|
||||||
if (content.includes('</error>')) return '</span>';
|
if (content.includes('</error>')) return '</span>';
|
||||||
|
|
||||||
// 处理 img 标签
|
|
||||||
if (content.includes('<img')) {
|
|
||||||
const imgData = parseImgTag(content);
|
|
||||||
if (imgData && imgData.src) {
|
|
||||||
const imageIndex = imageCount + htmlImageCount;
|
|
||||||
htmlImageCount++;
|
|
||||||
return renderImage(
|
|
||||||
imgData.src,
|
|
||||||
imgData.alt,
|
|
||||||
imgData.attrs,
|
|
||||||
imageIndex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔒 安全检查:不在白名单的标签,转义输出
|
// 🔒 安全检查:不在白名单的标签,转义输出
|
||||||
if (!isAllowedTag(content)) {
|
if (!isAllowedTag(content)) {
|
||||||
return md.utils.escapeHtml(content);
|
return md.utils.escapeHtml(content);
|
||||||
|
|
@ -341,21 +301,6 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
if (content.includes('<error>')) return '<span class="chat-error">';
|
if (content.includes('<error>')) return '<span class="chat-error">';
|
||||||
if (content.includes('</error>')) return '</span>';
|
if (content.includes('</error>')) return '</span>';
|
||||||
|
|
||||||
// 处理 img 标签
|
|
||||||
if (content.includes('<img')) {
|
|
||||||
const imgData = parseImgTag(content);
|
|
||||||
if (imgData && imgData.src) {
|
|
||||||
const imageIndex = imageCount + htmlImageCount;
|
|
||||||
htmlImageCount++;
|
|
||||||
return renderImage(
|
|
||||||
imgData.src,
|
|
||||||
imgData.alt,
|
|
||||||
imgData.attrs,
|
|
||||||
imageIndex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔒 安全检查:不在白名单的标签,转义输出
|
// 🔒 安全检查:不在白名单的标签,转义输出
|
||||||
if (!isAllowedTag(content)) {
|
if (!isAllowedTag(content)) {
|
||||||
return md.utils.escapeHtml(content);
|
return md.utils.escapeHtml(content);
|
||||||
|
|
@ -407,7 +352,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
}
|
}
|
||||||
}, [content, customizeRenderer, scrollToBottom]);
|
}, [content, customizeRenderer, scrollToBottom]);
|
||||||
|
|
||||||
// 添加代码块点击复制和图片点击预览功能(事件代理)
|
// 添加代码块点击复制功能
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
@ -415,21 +360,6 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
|
|
||||||
// 检查是否点击了图片
|
|
||||||
const imgElement = target.closest(
|
|
||||||
'img.markdown-image',
|
|
||||||
) as HTMLImageElement;
|
|
||||||
if (imgElement) {
|
|
||||||
const originalSrc = imgElement.getAttribute('data-original-src');
|
|
||||||
if (originalSrc) {
|
|
||||||
// 尝试获取缓存的 blob URL,如果不存在则使用原始 src
|
|
||||||
const blobUrl = imageBlobCacheRef.current.get(originalSrc);
|
|
||||||
setPreviewImgBlobUrl(blobUrl || originalSrc);
|
|
||||||
setPreviewOpen(true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否点击了代码块
|
// 检查是否点击了代码块
|
||||||
const preElement = target.closest('pre.hljs');
|
const preElement = target.closest('pre.hljs');
|
||||||
if (preElement) {
|
if (preElement) {
|
||||||
|
|
@ -438,7 +368,6 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
const code = codeElement.textContent || '';
|
const code = codeElement.textContent || '';
|
||||||
copyText(code.replace(/\n$/, ''));
|
copyText(code.replace(/\n$/, ''));
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否点击了行内代码
|
// 检查是否点击了行内代码
|
||||||
|
|
@ -451,7 +380,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
container.addEventListener('click', handleClick);
|
container.addEventListener('click', handleClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearImageBlobCache(imageBlobCacheRef.current);
|
clearImageBlobCache();
|
||||||
container.removeEventListener('click', handleClick);
|
container.removeEventListener('click', handleClick);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -477,9 +406,6 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
},
|
},
|
||||||
'.markdown-image': {
|
|
||||||
cursor: 'pointer',
|
|
||||||
},
|
|
||||||
'.image-error': {
|
'.image-error': {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { getShareV1AppWidgetInfo } from './request/ShareApp';
|
||||||
|
import { middleware as homeMiddleware } from './middleware/home';
|
||||||
|
|
||||||
|
const proxyShare = async (request: NextRequest) => {
|
||||||
|
// 转发到 process.env.TARGET
|
||||||
|
const kb_id = request.headers.get('x-kb-id') || process.env.DEV_KB_ID || '';
|
||||||
|
|
||||||
|
const targetOrigin = process.env.TARGET!;
|
||||||
|
const targetUrl = new URL(
|
||||||
|
request.nextUrl.pathname + request.nextUrl.search,
|
||||||
|
targetOrigin,
|
||||||
|
);
|
||||||
|
// 构造 fetch 选项
|
||||||
|
const fetchHeaders = new Headers(request.headers);
|
||||||
|
fetchHeaders.set('x-kb-id', kb_id);
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
method: request.method,
|
||||||
|
headers: fetchHeaders,
|
||||||
|
body: ['GET', 'HEAD'].includes(request.method) ? undefined : request.body,
|
||||||
|
redirect: 'manual',
|
||||||
|
};
|
||||||
|
const proxyRes = await fetch(targetUrl.toString(), fetchOptions);
|
||||||
|
const nextRes = new NextResponse(proxyRes.body, {
|
||||||
|
status: proxyRes.status,
|
||||||
|
headers: proxyRes.headers,
|
||||||
|
statusText: proxyRes.statusText,
|
||||||
|
});
|
||||||
|
return nextRes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function middleware(request: NextRequest) {
|
||||||
|
const url = request.nextUrl.clone();
|
||||||
|
const pathname = url.pathname;
|
||||||
|
if (pathname.startsWith('/widget')) {
|
||||||
|
const widgetInfo: any = await getShareV1AppWidgetInfo();
|
||||||
|
if (widgetInfo) {
|
||||||
|
if (!widgetInfo?.settings?.widget_bot_settings?.is_open) {
|
||||||
|
return NextResponse.rewrite(new URL('/not-fount', request.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
for (const [key, value] of request.headers.entries()) {
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sessionId = request.cookies.get('x-pw-session-id')?.value || '';
|
||||||
|
let needSetSessionId = false;
|
||||||
|
|
||||||
|
if (!sessionId) {
|
||||||
|
sessionId = uuidv4();
|
||||||
|
needSetSessionId = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response: NextResponse;
|
||||||
|
|
||||||
|
if (pathname.startsWith('/share/')) {
|
||||||
|
response = await proxyShare(request);
|
||||||
|
} else {
|
||||||
|
response = await homeMiddleware(request, headers, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needSetSessionId) {
|
||||||
|
response.cookies.set('x-pw-session-id', sessionId, {
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 60 * 60 * 24 * 365, // 1 年
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!pathname.startsWith('/share')) {
|
||||||
|
response.headers.set('x-current-path', pathname);
|
||||||
|
response.headers.set('x-current-search', url.search);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
'/',
|
||||||
|
'/home',
|
||||||
|
'/share/:path*',
|
||||||
|
'/chat/:path*',
|
||||||
|
'/widget',
|
||||||
|
'/welcome',
|
||||||
|
'/auth/login',
|
||||||
|
'/node/:path*',
|
||||||
|
'/node',
|
||||||
|
// '/client/:path*',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { parsePathname } from '@/utils';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { postShareV1StatPage } from '@/request/ShareStat';
|
||||||
|
import { getShareV1NodeList } from '@/request/ShareNode';
|
||||||
|
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||||
|
import { filterEmptyFolders, convertToTree } from '@/utils/drag';
|
||||||
|
import { deepSearchFirstNode } from '@/utils';
|
||||||
|
|
||||||
|
const StatPage = {
|
||||||
|
welcome: 1,
|
||||||
|
node: 2,
|
||||||
|
chat: 3,
|
||||||
|
auth: 4,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const getFirstNode = async () => {
|
||||||
|
const nodeListResult: any = await getShareV1NodeList();
|
||||||
|
const tree = filterEmptyFolders(convertToTree(nodeListResult || []));
|
||||||
|
return deepSearchFirstNode(tree);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHomePath = async () => {
|
||||||
|
const info = await getShareV1AppWebInfo();
|
||||||
|
return info?.settings?.home_page_setting;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function middleware(
|
||||||
|
request: NextRequest,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
session: string,
|
||||||
|
) {
|
||||||
|
const url = request.nextUrl.clone();
|
||||||
|
const { page, id } = parsePathname(url.pathname);
|
||||||
|
try {
|
||||||
|
// 获取节点列表
|
||||||
|
if (url.pathname === '/') {
|
||||||
|
const homePath = await getHomePath();
|
||||||
|
if (homePath === 'custom') {
|
||||||
|
return NextResponse.rewrite(new URL('/home', request.url));
|
||||||
|
} else {
|
||||||
|
const [firstNode] = await Promise.all([getFirstNode(), getHomePath()]);
|
||||||
|
if (firstNode) {
|
||||||
|
return NextResponse.rewrite(
|
||||||
|
new URL(`/node/${firstNode.id}`, request.url),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.rewrite(new URL('/node', request.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面上报
|
||||||
|
const pages = Object.keys(StatPage);
|
||||||
|
if (pages.includes(page) || pages.includes(id)) {
|
||||||
|
postShareV1StatPage(
|
||||||
|
{
|
||||||
|
scene: StatPage[page as keyof typeof StatPage],
|
||||||
|
node_id: id || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'x-pw-session-id': session,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
typeof error === 'object' &&
|
||||||
|
error !== null &&
|
||||||
|
'message' in error &&
|
||||||
|
error.message === 'NEXT_REDIRECT'
|
||||||
|
) {
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL(
|
||||||
|
`/auth/login?redirect=${encodeURIComponent(url.pathname + url.search)}`,
|
||||||
|
request.url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
@ -35,10 +35,6 @@ export const ThemeStoreProvider = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Cookies.set('theme_mode', themeMode, { expires: 365 * 10 });
|
Cookies.set('theme_mode', themeMode, { expires: 365 * 10 });
|
||||||
}, [themeMode]);
|
}, [themeMode]);
|
||||||
|
|
||||||
console.log('themeMode-------', themeMode);
|
|
||||||
console.log('themeMode-------', theme);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ themeMode, setThemeMode }}>
|
<ThemeContext.Provider value={{ themeMode, setThemeMode }}>
|
||||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { getShareV1AppWidgetInfo } from './request/ShareApp';
|
|
||||||
|
|
||||||
import { parsePathname } from '@/utils';
|
|
||||||
import { postShareV1StatPage } from '@/request/ShareStat';
|
|
||||||
import { getShareV1NodeList } from '@/request/ShareNode';
|
|
||||||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
|
||||||
import { filterEmptyFolders, convertToTree } from '@/utils/drag';
|
|
||||||
import { deepSearchFirstNode } from '@/utils';
|
|
||||||
|
|
||||||
const StatPage = {
|
|
||||||
welcome: 1,
|
|
||||||
node: 2,
|
|
||||||
chat: 3,
|
|
||||||
auth: 4,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const getFirstNode = async () => {
|
|
||||||
const nodeListResult: any = await getShareV1NodeList();
|
|
||||||
const tree = filterEmptyFolders(convertToTree(nodeListResult || []));
|
|
||||||
return deepSearchFirstNode(tree);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHomePath = async () => {
|
|
||||||
const info = await getShareV1AppWebInfo();
|
|
||||||
return info?.settings?.home_page_setting;
|
|
||||||
};
|
|
||||||
|
|
||||||
const homeProxy = async (
|
|
||||||
request: NextRequest,
|
|
||||||
headers: Record<string, string>,
|
|
||||||
session: string,
|
|
||||||
) => {
|
|
||||||
const url = request.nextUrl.clone();
|
|
||||||
const { page, id } = parsePathname(url.pathname);
|
|
||||||
try {
|
|
||||||
// 获取节点列表
|
|
||||||
if (url.pathname === '/') {
|
|
||||||
const homePath = await getHomePath();
|
|
||||||
if (homePath === 'custom') {
|
|
||||||
return NextResponse.rewrite(new URL('/home', request.url));
|
|
||||||
} else {
|
|
||||||
const [firstNode] = await Promise.all([getFirstNode(), getHomePath()]);
|
|
||||||
if (firstNode) {
|
|
||||||
return NextResponse.rewrite(
|
|
||||||
new URL(`/node/${firstNode.id}`, request.url),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return NextResponse.rewrite(new URL('/node', request.url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面上报
|
|
||||||
const pages = Object.keys(StatPage);
|
|
||||||
if (pages.includes(page) || pages.includes(id)) {
|
|
||||||
postShareV1StatPage(
|
|
||||||
{
|
|
||||||
scene: StatPage[page as keyof typeof StatPage],
|
|
||||||
node_id: id || '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'x-pw-session-id': session,
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next();
|
|
||||||
} catch (error) {
|
|
||||||
if (
|
|
||||||
typeof error === 'object' &&
|
|
||||||
error !== null &&
|
|
||||||
'message' in error &&
|
|
||||||
error.message === 'NEXT_REDIRECT'
|
|
||||||
) {
|
|
||||||
return NextResponse.redirect(
|
|
||||||
new URL(
|
|
||||||
`/auth/login?redirect=${encodeURIComponent(url.pathname + url.search)}`,
|
|
||||||
request.url,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next();
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxyShare = async (request: NextRequest) => {
|
|
||||||
// 转发到 process.env.TARGET
|
|
||||||
const kb_id = request.headers.get('x-kb-id') || process.env.DEV_KB_ID || '';
|
|
||||||
|
|
||||||
const targetOrigin = process.env.TARGET!;
|
|
||||||
const targetUrl = new URL(
|
|
||||||
request.nextUrl.pathname + request.nextUrl.search,
|
|
||||||
targetOrigin,
|
|
||||||
);
|
|
||||||
// 构造 fetch 选项
|
|
||||||
const fetchHeaders = new Headers(request.headers);
|
|
||||||
fetchHeaders.set('x-kb-id', kb_id);
|
|
||||||
|
|
||||||
const hasBody = !['GET', 'HEAD'].includes(request.method);
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method: request.method,
|
|
||||||
headers: fetchHeaders,
|
|
||||||
body: hasBody ? request.body : undefined,
|
|
||||||
redirect: 'manual',
|
|
||||||
...(hasBody && { duplex: 'half' as const }),
|
|
||||||
};
|
|
||||||
const proxyRes = await fetch(targetUrl.toString(), fetchOptions);
|
|
||||||
const nextRes = new NextResponse(proxyRes.body, {
|
|
||||||
status: proxyRes.status,
|
|
||||||
headers: proxyRes.headers,
|
|
||||||
statusText: proxyRes.statusText,
|
|
||||||
});
|
|
||||||
return nextRes;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function proxy(request: NextRequest) {
|
|
||||||
const url = request.nextUrl.clone();
|
|
||||||
const pathname = url.pathname;
|
|
||||||
if (pathname.startsWith('/widget')) {
|
|
||||||
const widgetInfo: any = await getShareV1AppWidgetInfo();
|
|
||||||
if (widgetInfo) {
|
|
||||||
if (!widgetInfo?.settings?.widget_bot_settings?.is_open) {
|
|
||||||
return NextResponse.rewrite(new URL('/not-found', request.url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {};
|
|
||||||
for (const [key, value] of request.headers.entries()) {
|
|
||||||
headers[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sessionId = request.cookies.get('x-pw-session-id')?.value || '';
|
|
||||||
let needSetSessionId = false;
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
sessionId = uuidv4();
|
|
||||||
needSetSessionId = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: NextResponse;
|
|
||||||
|
|
||||||
if (pathname.startsWith('/share/')) {
|
|
||||||
response = await proxyShare(request);
|
|
||||||
} else {
|
|
||||||
response = await homeProxy(request, headers, sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needSetSessionId) {
|
|
||||||
response.cookies.set('x-pw-session-id', sessionId, {
|
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 60 * 60 * 24 * 365, // 1 年
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!pathname.startsWith('/share')) {
|
|
||||||
response.headers.set('x-current-path', pathname);
|
|
||||||
response.headers.set('x-current-search', url.search);
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
'/',
|
|
||||||
'/home',
|
|
||||||
'/share/:path*',
|
|
||||||
'/chat/:path*',
|
|
||||||
'/widget',
|
|
||||||
'/welcome',
|
|
||||||
'/auth/login',
|
|
||||||
'/node/:path*',
|
|
||||||
'/node',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
DomainOpenAICompletionsResponse,
|
DomainOpenAICompletionsResponse,
|
||||||
DomainResponse,
|
DomainResponse,
|
||||||
PostShareV1ChatMessageParams,
|
PostShareV1ChatMessageParams,
|
||||||
|
PostShareV1ChatWidgetParams,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -91,3 +92,28 @@ export const postShareV1ChatMessage = (
|
||||||
format: "json",
|
format: "json",
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description ChatWidget
|
||||||
|
*
|
||||||
|
* @tags share_chat
|
||||||
|
* @name PostShareV1ChatWidget
|
||||||
|
* @summary ChatWidget
|
||||||
|
* @request POST:/share/v1/chat/widget
|
||||||
|
* @response `200` `DomainResponse` OK
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const postShareV1ChatWidget = (
|
||||||
|
query: PostShareV1ChatWidgetParams,
|
||||||
|
request: DomainChatRequest,
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
httpRequest<DomainResponse>({
|
||||||
|
path: `/share/v1/chat/widget`,
|
||||||
|
method: "POST",
|
||||||
|
query: query,
|
||||||
|
body: request,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
/* 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 {
|
|
||||||
DomainChatRequest,
|
|
||||||
DomainChatSearchReq,
|
|
||||||
DomainChatSearchResp,
|
|
||||||
DomainResponse,
|
|
||||||
PostShareV1ChatWidgetParams,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description ChatWidget
|
|
||||||
*
|
|
||||||
* @tags Widget
|
|
||||||
* @name PostShareV1ChatWidget
|
|
||||||
* @summary ChatWidget
|
|
||||||
* @request POST:/share/v1/chat/widget
|
|
||||||
* @response `200` `DomainResponse` OK
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postShareV1ChatWidget = (
|
|
||||||
query: PostShareV1ChatWidgetParams,
|
|
||||||
request: DomainChatRequest,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
httpRequest<DomainResponse>({
|
|
||||||
path: `/share/v1/chat/widget`,
|
|
||||||
method: "POST",
|
|
||||||
query: query,
|
|
||||||
body: request,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description WidgetSearch
|
|
||||||
*
|
|
||||||
* @tags Widget
|
|
||||||
* @name PostShareV1ChatWidgetSearch
|
|
||||||
* @summary WidgetSearch
|
|
||||||
* @request POST:/share/v1/chat/widget/search
|
|
||||||
* @response `200` `(DomainResponse & {
|
|
||||||
data?: DomainChatSearchResp,
|
|
||||||
|
|
||||||
})` OK
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postShareV1ChatWidgetSearch = (
|
|
||||||
request: DomainChatSearchReq,
|
|
||||||
params: RequestParams = {},
|
|
||||||
) =>
|
|
||||||
httpRequest<
|
|
||||||
DomainResponse & {
|
|
||||||
data?: DomainChatSearchResp;
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
path: `/share/v1/chat/widget/search`,
|
|
||||||
method: "POST",
|
|
||||||
body: request,
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
|
|
@ -10,6 +10,5 @@ export * from './ShareNode'
|
||||||
export * from './ShareOpenapi'
|
export * from './ShareOpenapi'
|
||||||
export * from './ShareStat'
|
export * from './ShareStat'
|
||||||
export * from './Wechat'
|
export * from './Wechat'
|
||||||
export * from './Widget'
|
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import {
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
||||||
|
|
@ -207,32 +206,6 @@ export const postShareProV1AuthLdap = (
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 用户登出
|
|
||||||
*
|
|
||||||
* @tags ShareAuth
|
|
||||||
* @name PostShareProV1AuthLogout
|
|
||||||
* @summary 用户登出
|
|
||||||
* @request POST:/share/pro/v1/auth/logout
|
|
||||||
* @response `200` `(DomainPWResponse & {
|
|
||||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
|
||||||
|
|
||||||
})` OK
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postShareProV1AuthLogout = (params: RequestParams = {}) =>
|
|
||||||
httpRequest<
|
|
||||||
DomainPWResponse & {
|
|
||||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp;
|
|
||||||
}
|
|
||||||
>({
|
|
||||||
path: `/share/pro/v1/auth/logout`,
|
|
||||||
method: "POST",
|
|
||||||
type: ContentType.Json,
|
|
||||||
format: "json",
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description OAuth登录
|
* @description OAuth登录
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,10 @@ export enum ConstsSourceType {
|
||||||
export enum ConstsLicenseEdition {
|
export enum ConstsLicenseEdition {
|
||||||
/** 开源版 */
|
/** 开源版 */
|
||||||
LicenseEditionFree = 0,
|
LicenseEditionFree = 0,
|
||||||
/** 专业版 */
|
/** 联创版 */
|
||||||
LicenseEditionProfession = 1,
|
LicenseEditionContributor = 1,
|
||||||
/** 企业版 */
|
/** 企业版 */
|
||||||
LicenseEditionEnterprise = 2,
|
LicenseEditionEnterprise = 2,
|
||||||
/** 商业版 */
|
|
||||||
LicenseEditionBusiness = 3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConstsContributeType {
|
export enum ConstsContributeType {
|
||||||
|
|
@ -457,11 +455,6 @@ export type GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp = Record<
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp = Record<
|
|
||||||
string,
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
redirect_url?: string;
|
redirect_url?: string;
|
||||||
|
|
@ -676,6 +669,8 @@ export interface GetApiProV1TokenListParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostApiV1LicensePayload {
|
export interface PostApiV1LicensePayload {
|
||||||
|
/** license edition */
|
||||||
|
license_edition: "contributor" | "enterprise";
|
||||||
/** license type */
|
/** license type */
|
||||||
license_type: "file" | "code";
|
license_type: "file" | "code";
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -171,21 +171,14 @@ export enum ConstsNodeAccessPerm {
|
||||||
NodeAccessPermClosed = "closed",
|
NodeAccessPermClosed = "closed",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConstsModelSettingMode {
|
|
||||||
ModelSettingModeManual = "manual",
|
|
||||||
ModelSettingModeAuto = "auto",
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @format int32 */
|
/** @format int32 */
|
||||||
export enum ConstsLicenseEdition {
|
export enum ConstsLicenseEdition {
|
||||||
/** 开源版 */
|
/** 开源版 */
|
||||||
LicenseEditionFree = 0,
|
LicenseEditionFree = 0,
|
||||||
/** 专业版 */
|
/** 联创版 */
|
||||||
LicenseEditionProfession = 1,
|
LicenseEditionContributor = 1,
|
||||||
/** 企业版 */
|
/** 企业版 */
|
||||||
LicenseEditionEnterprise = 2,
|
LicenseEditionEnterprise = 2,
|
||||||
/** 商业版 */
|
|
||||||
LicenseEditionBusiness = 3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConstsHomePageSetting {
|
export enum ConstsHomePageSetting {
|
||||||
|
|
@ -929,17 +922,6 @@ export interface DomainMetricsConfig {
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainModelModeSetting {
|
|
||||||
/** 百智云 API Key */
|
|
||||||
auto_mode_api_key?: string;
|
|
||||||
/** 自定义对话模型名称 */
|
|
||||||
chat_model?: string;
|
|
||||||
/** 手动模式下嵌入模型是否更新 */
|
|
||||||
is_manual_embedding_updated?: boolean;
|
|
||||||
/** 模式: manual 或 auto */
|
|
||||||
mode?: ConstsModelSettingMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DomainMoveNodeReq {
|
export interface DomainMoveNodeReq {
|
||||||
id: string;
|
id: string;
|
||||||
kb_id: string;
|
kb_id: string;
|
||||||
|
|
@ -1189,17 +1171,6 @@ export interface DomainShareConversationMessage {
|
||||||
role?: SchemaRoleType;
|
role?: SchemaRoleType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainShareNodeListItemResp {
|
|
||||||
emoji?: string;
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
parent_id?: string;
|
|
||||||
permissions?: DomainNodePermissions;
|
|
||||||
position?: number;
|
|
||||||
type?: DomainNodeType;
|
|
||||||
updated_at?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DomainSimpleAuth {
|
export interface DomainSimpleAuth {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
@ -1224,18 +1195,6 @@ export interface DomainStatPageReq {
|
||||||
scene: 1 | 2 | 3 | 4;
|
scene: 1 | 2 | 3 | 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainSwitchModeReq {
|
|
||||||
/** 百智云 API Key */
|
|
||||||
auto_mode_api_key?: string;
|
|
||||||
/** 自定义对话模型名称 */
|
|
||||||
chat_model?: string;
|
|
||||||
mode: "manual" | "auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DomainSwitchModeResp {
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DomainTextConfig {
|
export interface DomainTextConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
@ -1377,18 +1336,11 @@ export interface DomainWecomAIBotSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainWidgetBotSettings {
|
export interface DomainWidgetBotSettings {
|
||||||
btn_id?: string;
|
|
||||||
btn_logo?: string;
|
btn_logo?: string;
|
||||||
btn_position?: string;
|
|
||||||
btn_style?: string;
|
|
||||||
btn_text?: string;
|
btn_text?: string;
|
||||||
disclaimer?: string;
|
|
||||||
is_open?: boolean;
|
is_open?: boolean;
|
||||||
modal_position?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
recommend_node_ids?: string[];
|
recommend_node_ids?: string[];
|
||||||
recommend_questions?: string[];
|
recommend_questions?: string[];
|
||||||
search_mode?: string;
|
|
||||||
theme_mode?: string;
|
theme_mode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1694,7 +1646,6 @@ export interface V1ShareNodeDetailResp {
|
||||||
editor_id?: string;
|
editor_id?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
list?: DomainShareNodeListItemResp[];
|
|
||||||
meta?: DomainNodeMeta;
|
meta?: DomainNodeMeta;
|
||||||
name?: string;
|
name?: string;
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ const DocContent = ({
|
||||||
setCommentImages([]);
|
setCommentImages([]);
|
||||||
message.success(
|
message.success(
|
||||||
appDetail?.web_app_comment_settings?.moderation_enable
|
appDetail?.web_app_comment_settings?.moderation_enable
|
||||||
? '评论已提交,请耐心等待审核'
|
? '正在审核中...'
|
||||||
: '评论成功',
|
: '评论成功',
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { IconArrowUp } from '@/components/icons';
|
||||||
|
import { useStore } from '@/provider';
|
||||||
|
import { Box, IconButton, TextField } from '@mui/material';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import ChatLoading from '../chat/ChatLoading';
|
||||||
|
import { AnswerStatus } from '../chat/constant';
|
||||||
|
|
||||||
|
interface ChatInputProps {
|
||||||
|
loading: boolean;
|
||||||
|
thinking: keyof typeof AnswerStatus;
|
||||||
|
onSearch: (input: string) => void;
|
||||||
|
handleSearchAbort: () => void;
|
||||||
|
setThinking: (thinking: keyof typeof AnswerStatus) => void;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatInput = ({
|
||||||
|
loading,
|
||||||
|
onSearch,
|
||||||
|
thinking,
|
||||||
|
setThinking,
|
||||||
|
handleSearchAbort,
|
||||||
|
placeholder,
|
||||||
|
}: ChatInputProps) => {
|
||||||
|
const { themeMode } = useStore();
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
if (input.length > 0) {
|
||||||
|
onSearch(input);
|
||||||
|
setInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderRadius: '10px',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
bgcolor:
|
||||||
|
themeMode === 'dark' ? 'background.paper' : 'background.default',
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{
|
||||||
|
'.MuiInputBase-root': {
|
||||||
|
p: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '52px !important',
|
||||||
|
transition: 'all 0.5s ease-in-out',
|
||||||
|
bgcolor:
|
||||||
|
themeMode === 'dark' ? 'background.paper' : 'background.default',
|
||||||
|
},
|
||||||
|
textarea: {
|
||||||
|
lineHeight: '26px',
|
||||||
|
height: '52px !important',
|
||||||
|
borderRadius: 0,
|
||||||
|
transition: 'all 0.5s ease-in-out',
|
||||||
|
'&::placeholder': {
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
msOverflowStyle: 'none',
|
||||||
|
},
|
||||||
|
fieldset: {
|
||||||
|
border: 'none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
size='small'
|
||||||
|
value={input}
|
||||||
|
onChange={e => setInput(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
const isComposing =
|
||||||
|
e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
|
||||||
|
if (
|
||||||
|
e.key === 'Enter' &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
input.length > 0 &&
|
||||||
|
!isComposing
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={placeholder}
|
||||||
|
autoComplete='off'
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
sx: {
|
||||||
|
gap: 2,
|
||||||
|
alignItems: loading ? 'flex-start' : 'flex-end',
|
||||||
|
mr: loading ? 10 : 4,
|
||||||
|
},
|
||||||
|
endAdornment: (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
fontSize: 12,
|
||||||
|
flexShrink: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ChatLoading
|
||||||
|
thinking={thinking}
|
||||||
|
onClick={() => {
|
||||||
|
setThinking(4);
|
||||||
|
handleSearchAbort();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={() => {
|
||||||
|
if (input.length > 0) {
|
||||||
|
handleSearchAbort();
|
||||||
|
setThinking(1);
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconArrowUp sx={{ fontSize: 12 }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatInput;
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ConversationItem } from '@/assets/type';
|
||||||
|
import Feedback from '@/components/feedback';
|
||||||
|
import { IconCai, IconCaied, IconZan, IconZaned } from '@/components/icons';
|
||||||
|
import MarkDown from '@/components/markdown';
|
||||||
|
import { useStore } from '@/provider';
|
||||||
|
import { postShareV1ChatFeedback } from '@/request/ShareChat';
|
||||||
|
import { AnswerStatus } from '@/views/chat/constant';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { message } from '@ctzhian/ui';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import ChatInput from './ChatInput';
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
|
interface ChatWindowProps {
|
||||||
|
placeholder: string;
|
||||||
|
conversation: ConversationItem[];
|
||||||
|
conversation_id: string;
|
||||||
|
setConversation: (conversation: ConversationItem[]) => void;
|
||||||
|
answer: string;
|
||||||
|
loading: boolean;
|
||||||
|
thinking: keyof typeof AnswerStatus;
|
||||||
|
onSearch: (input: string) => void;
|
||||||
|
handleSearchAbort: () => void;
|
||||||
|
setThinking: (thinking: keyof typeof AnswerStatus) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatWindow = ({
|
||||||
|
conversation,
|
||||||
|
conversation_id,
|
||||||
|
setConversation,
|
||||||
|
answer,
|
||||||
|
loading,
|
||||||
|
thinking,
|
||||||
|
onSearch,
|
||||||
|
handleSearchAbort,
|
||||||
|
setThinking,
|
||||||
|
placeholder,
|
||||||
|
}: ChatWindowProps) => {
|
||||||
|
const [conversationItem, setConversationItem] =
|
||||||
|
useState<ConversationItem | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { themeMode = 'light', widget, kbDetail } = useStore();
|
||||||
|
|
||||||
|
const handleScore = async (
|
||||||
|
message_id: string,
|
||||||
|
score: number,
|
||||||
|
type?: string,
|
||||||
|
content?: string,
|
||||||
|
) => {
|
||||||
|
const data: any = {
|
||||||
|
conversation_id,
|
||||||
|
message_id,
|
||||||
|
score,
|
||||||
|
};
|
||||||
|
if (type) data.type = type;
|
||||||
|
if (content) data.feedback_content = content;
|
||||||
|
await postShareV1ChatFeedback(data);
|
||||||
|
message.success('反馈成功');
|
||||||
|
setConversation(
|
||||||
|
conversation.map(item => {
|
||||||
|
return item.message_id === message_id ? { ...item, score } : item;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFeedbackEnabled =
|
||||||
|
// @ts-ignore
|
||||||
|
kbDetail?.settings?.ai_feedback_settings?.is_enabled ?? true;
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const container = document.querySelector('.conversation-container');
|
||||||
|
if (container) {
|
||||||
|
container.scrollTo({
|
||||||
|
top: container.scrollHeight,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [answer, conversation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
'&::-webkit-scrollbar': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
mb: 0,
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
msOverflowStyle: 'none',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction='column'
|
||||||
|
gap={2}
|
||||||
|
className='conversation-container'
|
||||||
|
sx={{
|
||||||
|
overflow: 'auto',
|
||||||
|
height: 'calc(100% - 100px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{conversation.map((item, index) => (
|
||||||
|
<Box key={index}>
|
||||||
|
<Accordion
|
||||||
|
defaultExpanded={true}
|
||||||
|
sx={{
|
||||||
|
bgcolor:
|
||||||
|
themeMode === 'dark'
|
||||||
|
? 'background.default'
|
||||||
|
: 'background.paper3',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
||||||
|
sx={{
|
||||||
|
userSelect: 'text',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
fontWeight: '700',
|
||||||
|
lineHeight: '24px',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.q}
|
||||||
|
</Box>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<MarkDown content={item.a} />
|
||||||
|
{index === conversation.length - 1 && loading && !answer && (
|
||||||
|
<>
|
||||||
|
<Skeleton variant='text' width='100%' />
|
||||||
|
<Skeleton variant='text' width='70%' />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{index === conversation.length - 1 && answer && (
|
||||||
|
<MarkDown content={answer} />
|
||||||
|
)}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
{(index !== conversation.length - 1 || !loading) && (
|
||||||
|
<Stack
|
||||||
|
direction='row'
|
||||||
|
alignItems='center'
|
||||||
|
justifyContent='space-between'
|
||||||
|
gap={3}
|
||||||
|
sx={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: 'text.tertiary',
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>{kbDetail?.settings?.disclaimer_settings?.content}</Box>
|
||||||
|
<Stack direction='row' gap={3} alignItems='center'>
|
||||||
|
<span>生成于 {dayjs(item.update_time).fromNow()}</span>
|
||||||
|
|
||||||
|
{isFeedbackEnabled && (
|
||||||
|
<>
|
||||||
|
{item.score === 1 && (
|
||||||
|
<IconZaned sx={{ cursor: 'pointer' }} />
|
||||||
|
)}
|
||||||
|
{item.score !== 1 && (
|
||||||
|
<IconZan
|
||||||
|
sx={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
if (item.score === 0)
|
||||||
|
handleScore(item.message_id, 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.score !== -1 && (
|
||||||
|
<IconCai
|
||||||
|
sx={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
if (item.score === 0) {
|
||||||
|
setConversationItem(item);
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.score === -1 && (
|
||||||
|
<IconCaied sx={{ cursor: 'pointer' }} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChatInput
|
||||||
|
onSearch={onSearch}
|
||||||
|
thinking={thinking}
|
||||||
|
loading={loading}
|
||||||
|
handleSearchAbort={handleSearchAbort}
|
||||||
|
setThinking={setThinking}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Feedback
|
||||||
|
open={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
onSubmit={handleScore}
|
||||||
|
data={conversationItem}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatWindow;
|
||||||
|
|
@ -1,434 +0,0 @@
|
||||||
'use client';
|
|
||||||
import Logo from '@/assets/images/logo.png';
|
|
||||||
import noDocImage from '@/assets/images/no-doc.png';
|
|
||||||
import { useStore } from '@/provider';
|
|
||||||
import { postShareV1ChatWidgetSearch } from '@/request';
|
|
||||||
import { DomainNodeContentChunkSSE } from '@/request/types';
|
|
||||||
import { message } from '@ctzhian/ui';
|
|
||||||
import {
|
|
||||||
alpha,
|
|
||||||
Box,
|
|
||||||
CircularProgress,
|
|
||||||
IconButton,
|
|
||||||
InputAdornment,
|
|
||||||
Skeleton,
|
|
||||||
Stack,
|
|
||||||
styled,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import {
|
|
||||||
IconFasong,
|
|
||||||
IconJinsousuo,
|
|
||||||
IconMianbaoxie,
|
|
||||||
IconWenjian,
|
|
||||||
} from '@panda-wiki/icons';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
const StyledSearchResultItem = styled(Stack)(({ theme }) => ({
|
|
||||||
position: 'relative',
|
|
||||||
'&::before': {
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
borderBottom: '1px dashed',
|
|
||||||
borderColor: alpha(theme.palette.text.primary, 0.1),
|
|
||||||
},
|
|
||||||
'&::after': {
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
borderBottom: '1px dashed',
|
|
||||||
borderColor: alpha(theme.palette.text.primary, 0.1),
|
|
||||||
},
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
borderRadius: '8px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s ease-in-out',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: alpha(theme.palette.text.primary, 0.02),
|
|
||||||
'.hover-primary': {
|
|
||||||
color: 'primary.main',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const SearchDocSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<StyledSearchResultItem>
|
|
||||||
<Stack gap={1}>
|
|
||||||
<Skeleton variant='rounded' height={16} width={200} />
|
|
||||||
<Skeleton variant='rounded' height={22} width={400} />
|
|
||||||
<Skeleton variant='rounded' height={16} width={500} />
|
|
||||||
</Stack>
|
|
||||||
</StyledSearchResultItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
interface SearchDocContentProps {
|
|
||||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
|
||||||
placeholder: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchDocContent: React.FC<SearchDocContentProps> = ({
|
|
||||||
inputRef,
|
|
||||||
placeholder,
|
|
||||||
}) => {
|
|
||||||
const { kbDetail } = useStore();
|
|
||||||
// 模糊搜索相关状态
|
|
||||||
const [fuzzySuggestions, setFuzzySuggestions] = useState<string[]>([]);
|
|
||||||
const [showFuzzySuggestions, setShowFuzzySuggestions] = useState(false);
|
|
||||||
const [input, setInput] = useState('');
|
|
||||||
const [hasSearch, setHasSearch] = useState(false);
|
|
||||||
// 搜索结果相关状态
|
|
||||||
const [searchResults, setSearchResults] = useState<
|
|
||||||
DomainNodeContentChunkSSE[]
|
|
||||||
>([]);
|
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
|
|
||||||
// 处理输入变化,显示模糊搜索建议
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setInput(value);
|
|
||||||
|
|
||||||
// if (value.trim().length > 0) {
|
|
||||||
// // 改进的模糊搜索逻辑
|
|
||||||
// const filtered = mockFuzzySuggestions
|
|
||||||
// .filter(suggestion => {
|
|
||||||
// const lowerSuggestion = suggestion.toLowerCase();
|
|
||||||
// const lowerValue = value.toLowerCase();
|
|
||||||
// // 支持前缀匹配和包含匹配
|
|
||||||
// return (
|
|
||||||
// lowerSuggestion.startsWith(lowerValue) ||
|
|
||||||
// lowerSuggestion.includes(lowerValue)
|
|
||||||
// );
|
|
||||||
// })
|
|
||||||
// .slice(0, 5); // 限制显示数量
|
|
||||||
|
|
||||||
// setFuzzySuggestions(filtered);
|
|
||||||
// setShowFuzzySuggestions(true);
|
|
||||||
// } else {
|
|
||||||
// setShowFuzzySuggestions(false);
|
|
||||||
// setFuzzySuggestions([]);
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
// 选择模糊搜索建议
|
|
||||||
const handleFuzzySuggestionClick = (suggestion: string) => {
|
|
||||||
setInput(suggestion);
|
|
||||||
setShowFuzzySuggestions(false);
|
|
||||||
setFuzzySuggestions([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 执行搜索
|
|
||||||
const handleSearch = async () => {
|
|
||||||
if (isSearching) return;
|
|
||||||
if (!input.trim()) return;
|
|
||||||
|
|
||||||
setIsSearching(true);
|
|
||||||
setSearchResults([]);
|
|
||||||
setShowFuzzySuggestions(false);
|
|
||||||
setFuzzySuggestions([]);
|
|
||||||
|
|
||||||
let token = '';
|
|
||||||
const Cap = (await import('@cap.js/widget')).default;
|
|
||||||
const cap = new Cap({
|
|
||||||
apiEndpoint: '/share/v1/captcha/',
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const solution = await cap.solve();
|
|
||||||
token = solution.token;
|
|
||||||
} catch (error) {
|
|
||||||
message.error('验证失败');
|
|
||||||
console.log(error, 'error---------');
|
|
||||||
setIsSearching(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
postShareV1ChatWidgetSearch({ message: input, captcha_token: token })
|
|
||||||
.then(res => {
|
|
||||||
setSearchResults(res.node_result || []);
|
|
||||||
setHasSearch(true);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsSearching(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理搜索结果点击
|
|
||||||
const handleSearchResultClick = (result: DomainNodeContentChunkSSE) => {
|
|
||||||
window.open(`/node/${result.node_id}`, '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理键盘事件
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSearch();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 高亮显示匹配的文本
|
|
||||||
const highlightMatch = (text: string, query: string) => {
|
|
||||||
if (!query.trim()) return text;
|
|
||||||
|
|
||||||
// 转义特殊字符,避免正则表达式错误
|
|
||||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
|
||||||
const parts = text.split(regex);
|
|
||||||
|
|
||||||
return parts.map((part, index) => {
|
|
||||||
// 检查是否匹配(不区分大小写)
|
|
||||||
if (part.toLowerCase() === query.toLowerCase()) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
component='span'
|
|
||||||
key={index}
|
|
||||||
sx={{
|
|
||||||
color: 'primary.main',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{part}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return part;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Stack
|
|
||||||
direction='row'
|
|
||||||
alignItems='center'
|
|
||||||
justifyContent='center'
|
|
||||||
gap={2}
|
|
||||||
sx={{ mb: 3, mt: 1 }}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={kbDetail?.settings?.icon || Logo.src}
|
|
||||||
alt='logo'
|
|
||||||
width={46}
|
|
||||||
height={46}
|
|
||||||
unoptimized
|
|
||||||
style={{
|
|
||||||
objectFit: 'contain',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography
|
|
||||||
variant='h6'
|
|
||||||
sx={{ fontSize: 32, color: 'text.primary', fontWeight: 700 }}
|
|
||||||
>
|
|
||||||
{kbDetail?.settings?.title}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
{/* 搜索输入框 */}
|
|
||||||
<TextField
|
|
||||||
ref={inputRef}
|
|
||||||
value={input}
|
|
||||||
placeholder={placeholder}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
fullWidth
|
|
||||||
autoFocus
|
|
||||||
sx={theme => ({
|
|
||||||
boxShadow: `0px 20px 40px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
|
||||||
borderRadius: 2,
|
|
||||||
'& .MuiInputBase-root': {
|
|
||||||
fontSize: 16,
|
|
||||||
backgroundColor: theme.palette.background.default,
|
|
||||||
'& fieldset': {
|
|
||||||
borderColor: alpha(theme.palette.text.primary, 0.1),
|
|
||||||
},
|
|
||||||
'&:hover fieldset': {
|
|
||||||
borderColor: 'primary.main',
|
|
||||||
},
|
|
||||||
'&.Mui-focused fieldset': {
|
|
||||||
borderColor: `${theme.palette.primary.main} !important`,
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'& .MuiInputBase-input': {
|
|
||||||
py: 1.5,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
slotProps={{
|
|
||||||
input: {
|
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position='start'>
|
|
||||||
<IconJinsousuo sx={{ fontSize: 20, color: 'text.secondary' }} />
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position='end'>
|
|
||||||
<IconButton
|
|
||||||
size='small'
|
|
||||||
onClick={handleSearch}
|
|
||||||
disabled={!input.trim() || isSearching}
|
|
||||||
sx={{
|
|
||||||
color: 'primary.main',
|
|
||||||
'&:hover': { bgcolor: 'primary.lighter' },
|
|
||||||
'&.Mui-disabled': { color: 'action.disabled' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isSearching ? (
|
|
||||||
<CircularProgress size={20} />
|
|
||||||
) : (
|
|
||||||
<IconFasong
|
|
||||||
sx={{
|
|
||||||
fontSize: 22,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* 模糊搜索建议列表 */}
|
|
||||||
{showFuzzySuggestions && fuzzySuggestions.length > 0 && (
|
|
||||||
<Stack
|
|
||||||
sx={{
|
|
||||||
mt: 1,
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 1000,
|
|
||||||
}}
|
|
||||||
gap={0.5}
|
|
||||||
>
|
|
||||||
{fuzzySuggestions.map((suggestion, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
onClick={() => handleFuzzySuggestionClick(suggestion)}
|
|
||||||
sx={{
|
|
||||||
py: 1,
|
|
||||||
px: 2,
|
|
||||||
borderRadius: '6px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
bgcolor: 'transparent',
|
|
||||||
color: 'text.primary',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: 'action.hover',
|
|
||||||
},
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: 'auto',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 400,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{highlightMatch(suggestion, input)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{/* 搜索结果列表 */}
|
|
||||||
{searchResults.length > 0 && (
|
|
||||||
<Box sx={{ mt: 2 }}>
|
|
||||||
{/* 搜索结果统计 */}
|
|
||||||
<Typography
|
|
||||||
variant='body2'
|
|
||||||
sx={{
|
|
||||||
color: 'text.tertiary',
|
|
||||||
mb: 2,
|
|
||||||
fontSize: 14,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
共找到 {searchResults.length} 个结果
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* 搜索结果列表 */}
|
|
||||||
<Stack sx={{ overflow: 'auto', maxHeight: 'calc(100vh - 334px)' }}>
|
|
||||||
{searchResults.map((result, index) => (
|
|
||||||
<StyledSearchResultItem
|
|
||||||
direction='row'
|
|
||||||
justifyContent='space-between'
|
|
||||||
alignItems='center'
|
|
||||||
key={result.node_id}
|
|
||||||
gap={2}
|
|
||||||
onClick={() => handleSearchResultClick(result)}
|
|
||||||
>
|
|
||||||
<Stack sx={{ flex: 1, width: 0 }} gap={0.5}>
|
|
||||||
{/* 路径 */}
|
|
||||||
<Typography
|
|
||||||
variant='caption'
|
|
||||||
sx={{
|
|
||||||
color: 'text.tertiary',
|
|
||||||
fontSize: 12,
|
|
||||||
display: 'block',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(result.node_path_names || []).length > 0
|
|
||||||
? result.node_path_names?.join(' > ')
|
|
||||||
: result.name}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* 标题和图标 */}
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
variant='h6'
|
|
||||||
className='hover-primary'
|
|
||||||
sx={{
|
|
||||||
gap: 0.5,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'text.primary',
|
|
||||||
flex: 1,
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{result.emoji || <IconWenjian />} {result.name}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* 描述 */}
|
|
||||||
<Typography
|
|
||||||
variant='body2'
|
|
||||||
sx={{
|
|
||||||
color: 'text.tertiary',
|
|
||||||
fontSize: 12,
|
|
||||||
lineHeight: 1.5,
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{result.summary || '暂无摘要'}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<IconMianbaoxie sx={{ fontSize: 12 }} />
|
|
||||||
</StyledSearchResultItem>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{searchResults.length === 0 && !isSearching && hasSearch && (
|
|
||||||
<Box sx={{ my: 5, textAlign: 'center' }}>
|
|
||||||
<Image src={noDocImage} alt='暂无结果' width={250} />
|
|
||||||
<Typography variant='body2' sx={{ color: 'text.tertiary' }}>
|
|
||||||
暂无相关结果
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 搜索中状态 */}
|
|
||||||
{isSearching && (
|
|
||||||
<Stack sx={{ mt: 2 }}>
|
|
||||||
{[...Array(3)].map((_, index) => (
|
|
||||||
<SearchDocSkeleton key={index} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchDocContent;
|
|
||||||
|
|
@ -1,344 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Box,
|
|
||||||
IconButton,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
alpha,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
// 布局容器组件
|
|
||||||
export const StyledMainContainer = styled(Box)(() => ({
|
|
||||||
flex: 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledConversationContainer = styled(Stack)(() => ({
|
|
||||||
maxHeight: 'calc(100vh - 332px)',
|
|
||||||
overflow: 'auto',
|
|
||||||
scrollbarWidth: 'none',
|
|
||||||
msOverflowStyle: 'none',
|
|
||||||
'&::-webkit-scrollbar': {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledConversationItem = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 聊天气泡相关组件
|
|
||||||
export const StyledUserBubble = styled(Box)(({ theme }) => ({
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
maxWidth: '75%',
|
|
||||||
padding: theme.spacing(1, 2),
|
|
||||||
borderRadius: '10px 10px 0px 10px',
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
color: theme.palette.primary.contrastText,
|
|
||||||
fontSize: 14,
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
width: '100%',
|
|
||||||
gap: theme.spacing(3),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledAiBubbleContent = styled(Box)(() => ({
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 对话相关组件
|
|
||||||
export const StyledAccordion = styled(Accordion)(() => ({
|
|
||||||
padding: 0,
|
|
||||||
border: 'none',
|
|
||||||
'&:before': {
|
|
||||||
content: '""',
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
background: 'transparent',
|
|
||||||
backgroundImage: 'none',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
paddingRight: theme.spacing(2),
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
paddingBottom: theme.spacing(1),
|
|
||||||
userSelect: 'text',
|
|
||||||
borderRadius: '10px',
|
|
||||||
backgroundColor: theme.palette.background.paper3,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
borderTop: 'none',
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledQuestionText = styled(Box)(() => ({
|
|
||||||
fontWeight: '700',
|
|
||||||
fontSize: 16,
|
|
||||||
lineHeight: '24px',
|
|
||||||
wordBreak: 'break-all',
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 搜索结果相关组件
|
|
||||||
export const StyledChunkAccordion = styled(Accordion)(({ theme }) => ({
|
|
||||||
backgroundImage: 'none',
|
|
||||||
background: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
padding: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledChunkAccordionSummary = styled(AccordionSummary)(
|
|
||||||
({ theme }) => ({
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
'.MuiAccordionSummary-content': {
|
|
||||||
flexGrow: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledChunkAccordionDetails = styled(AccordionDetails)(
|
|
||||||
({ theme }) => ({
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
borderTop: 'none',
|
|
||||||
borderLeft: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledChunkItem = styled(Box)(({ theme }) => ({
|
|
||||||
cursor: 'pointer',
|
|
||||||
'&:hover': {
|
|
||||||
'.hover-primary': {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 思考过程相关组件
|
|
||||||
export const StyledThinkingAccordion = styled(Accordion)(({ theme }) => ({
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: 'none',
|
|
||||||
padding: 0,
|
|
||||||
paddingBottom: theme.spacing(2),
|
|
||||||
'&:before': {
|
|
||||||
content: '""',
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledThinkingAccordionSummary = styled(AccordionSummary)(
|
|
||||||
({ theme }) => ({
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
'.MuiAccordionSummary-content': {
|
|
||||||
flexGrow: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const StyledThinkingAccordionDetails = styled(AccordionDetails)(
|
|
||||||
({ theme }) => ({
|
|
||||||
paddingTop: 0,
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
borderTop: 'none',
|
|
||||||
borderLeft: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
'.markdown-body': {
|
|
||||||
opacity: 0.75,
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 操作区域组件
|
|
||||||
export const StyledActionStack = styled(Stack)(({ theme }) => ({
|
|
||||||
fontSize: 12,
|
|
||||||
color: alpha(theme.palette.text.primary, 0.35),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 输入区域组件
|
|
||||||
export const StyledInputContainer = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledInputWrapper = styled(Stack)(({ theme }) => ({
|
|
||||||
paddingLeft: theme.spacing(1.5),
|
|
||||||
paddingRight: theme.spacing(1.5),
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
paddingBottom: theme.spacing(1),
|
|
||||||
borderRadius: '10px',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: alpha(theme.palette.text.primary, 0.1),
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'flex-end',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
backgroundColor: theme.palette.background.default,
|
|
||||||
boxShadow: `0px 20px 40px 0px ${alpha(theme.palette.text.primary, 0.06)}`,
|
|
||||||
transition: 'border-color 0.2s ease-in-out',
|
|
||||||
'&:hover': {
|
|
||||||
borderColor: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
'&:focus-within': {
|
|
||||||
borderColor: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 图片预览组件
|
|
||||||
export const StyledImagePreviewStack = styled(Stack)(() => ({
|
|
||||||
width: '100%',
|
|
||||||
zIndex: 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledImagePreviewItem = styled(Box)(({ theme }) => ({
|
|
||||||
position: 'relative',
|
|
||||||
borderRadius: '8px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledImageRemoveButton = styled(IconButton)(({ theme }) => ({
|
|
||||||
position: 'absolute',
|
|
||||||
top: 2,
|
|
||||||
right: 2,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
transition: 'opacity 0.2s',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 输入框组件
|
|
||||||
export const StyledTextField = styled(TextField)(({ theme }) => ({
|
|
||||||
backgroundColor: theme.palette.background.default,
|
|
||||||
'.MuiInputBase-root': {
|
|
||||||
padding: 0,
|
|
||||||
overflow: 'hidden',
|
|
||||||
height: '52px !important',
|
|
||||||
},
|
|
||||||
textarea: {
|
|
||||||
borderRadius: 0,
|
|
||||||
'&::-webkit-scrollbar': {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
scrollbarWidth: 'none',
|
|
||||||
msOverflowStyle: 'none',
|
|
||||||
padding: '2px',
|
|
||||||
},
|
|
||||||
fieldset: {
|
|
||||||
border: 'none',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 操作按钮组件
|
|
||||||
export const StyledActionButtonStack = styled(Stack)(() => ({
|
|
||||||
width: '100%',
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 搜索建议组件
|
|
||||||
export const StyledFuzzySuggestionsStack = styled(Stack)(({ theme }) => ({
|
|
||||||
marginTop: theme.spacing(1),
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 1000,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledFuzzySuggestionItem = styled(Box)(({ theme }) => ({
|
|
||||||
paddingTop: theme.spacing(1),
|
|
||||||
paddingBottom: theme.spacing(1),
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
paddingRight: theme.spacing(2),
|
|
||||||
borderRadius: '6px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
},
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: 'auto',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 400,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 热门搜索组件
|
|
||||||
export const StyledHotSearchStack = styled(Stack)(({ theme }) => ({
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const StyledHotSearchItem = styled(Box)(({ theme }) => ({
|
|
||||||
paddingTop: theme.spacing(0.75),
|
|
||||||
paddingBottom: theme.spacing(0.75),
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
paddingRight: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
borderRadius: '10px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
backgroundColor: alpha(theme.palette.text.primary, 0.02),
|
|
||||||
border: `1px solid ${alpha(theme.palette.text.primary, 0.01)}`,
|
|
||||||
color: alpha(theme.palette.text.primary, 0.75),
|
|
||||||
'&:hover': {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: 'auto',
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 热门搜索容器
|
|
||||||
export const StyledHotSearchContainer = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 热门搜索列
|
|
||||||
export const StyledHotSearchColumn = styled(Box)(({ theme }) => ({
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
paddingLeft: theme.spacing(2),
|
|
||||||
borderLeft: `1px solid ${alpha(theme.palette.text.primary, 0.06)}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 热门搜索列项目
|
|
||||||
export const StyledHotSearchColumnItem = styled(Box)(({ theme }) => ({
|
|
||||||
paddingRight: theme.spacing(2),
|
|
||||||
borderRadius: '10px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 400,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
'&:hover': {
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
// 常量定义
|
|
||||||
export const MAX_IMAGES = 9;
|
|
||||||
export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
||||||
export const CONVERSATION_MAX_HEIGHT = 'calc(100vh - 334px)';
|
|
||||||
export const FUZZY_SUGGESTIONS_LIMIT = 5;
|
|
||||||
|
|
||||||
// 回答状态
|
|
||||||
export const AnswerStatus = {
|
|
||||||
1: '正在搜索结果...',
|
|
||||||
2: '思考中...',
|
|
||||||
3: '正在回答',
|
|
||||||
4: '',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type AnswerStatusType = keyof typeof AnswerStatus;
|
|
||||||
|
|
||||||
// CAP配置
|
|
||||||
export const CAP_CONFIG = {
|
|
||||||
apiEndpoint: '/share/v1/captcha/',
|
|
||||||
wasmUrl: '/cap@0.0.6/cap_wasm.min.js',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// SSE配置
|
|
||||||
export const SSE_CONFIG = {
|
|
||||||
url: '/share/v1/chat/message',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import { useState, useCallback, useRef } from 'react';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { ConversationItem } from '../types';
|
|
||||||
import { ChunkResultItem } from '@/assets/type';
|
|
||||||
|
|
||||||
export const useConversation = () => {
|
|
||||||
const [conversation, setConversation] = useState<ConversationItem[]>([]);
|
|
||||||
const [fullAnswer, setFullAnswer] = useState<string>('');
|
|
||||||
const [chunkResult, setChunkResult] = useState<ChunkResultItem[]>([]);
|
|
||||||
const [thinkingContent, setThinkingContent] = useState<string>('');
|
|
||||||
const [answer, setAnswer] = useState('');
|
|
||||||
const [isChunkResult, setIsChunkResult] = useState(false);
|
|
||||||
const [isThinking, setIsThinking] = useState(false);
|
|
||||||
|
|
||||||
const messageIdRef = useRef('');
|
|
||||||
|
|
||||||
const addQuestion = useCallback(
|
|
||||||
(q: string, reset: boolean = false) => {
|
|
||||||
const newConversation = reset
|
|
||||||
? []
|
|
||||||
: conversation.some(item => item.source === 'history')
|
|
||||||
? []
|
|
||||||
: [...conversation];
|
|
||||||
|
|
||||||
newConversation.push({
|
|
||||||
q,
|
|
||||||
a: '',
|
|
||||||
score: 0,
|
|
||||||
message_id: '',
|
|
||||||
update_time: '',
|
|
||||||
source: 'chat',
|
|
||||||
chunk_result: [],
|
|
||||||
thinking_content: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
messageIdRef.current = '';
|
|
||||||
setConversation(newConversation);
|
|
||||||
setChunkResult([]);
|
|
||||||
setThinkingContent('');
|
|
||||||
setAnswer('');
|
|
||||||
setFullAnswer('');
|
|
||||||
},
|
|
||||||
[conversation],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateLastConversation = useCallback(() => {
|
|
||||||
setAnswer(prevAnswer => {
|
|
||||||
setThinkingContent(prevThinkingContent => {
|
|
||||||
setChunkResult(prevChunkResult => {
|
|
||||||
setConversation(prev => {
|
|
||||||
const newConversation = [...prev];
|
|
||||||
const lastConversation =
|
|
||||||
newConversation[newConversation.length - 1];
|
|
||||||
if (lastConversation) {
|
|
||||||
lastConversation.a = prevAnswer;
|
|
||||||
lastConversation.update_time = dayjs().format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
);
|
|
||||||
lastConversation.message_id = messageIdRef.current;
|
|
||||||
lastConversation.source = 'chat';
|
|
||||||
lastConversation.chunk_result = prevChunkResult;
|
|
||||||
lastConversation.thinking_content = prevThinkingContent;
|
|
||||||
}
|
|
||||||
return newConversation;
|
|
||||||
});
|
|
||||||
return prevChunkResult;
|
|
||||||
});
|
|
||||||
return prevThinkingContent;
|
|
||||||
});
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
setFullAnswer('');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateConversationScore = useCallback(
|
|
||||||
(message_id: string, score: number) => {
|
|
||||||
setConversation(prev =>
|
|
||||||
prev.map(item =>
|
|
||||||
item.message_id === message_id ? { ...item, score } : item,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetConversation = useCallback(() => {
|
|
||||||
setConversation([]);
|
|
||||||
setChunkResult([]);
|
|
||||||
setAnswer('');
|
|
||||||
setFullAnswer('');
|
|
||||||
setThinkingContent('');
|
|
||||||
messageIdRef.current = '';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
conversation,
|
|
||||||
setConversation,
|
|
||||||
fullAnswer,
|
|
||||||
setFullAnswer,
|
|
||||||
chunkResult,
|
|
||||||
setChunkResult,
|
|
||||||
thinkingContent,
|
|
||||||
setThinkingContent,
|
|
||||||
answer,
|
|
||||||
setAnswer,
|
|
||||||
isChunkResult,
|
|
||||||
setIsChunkResult,
|
|
||||||
isThinking,
|
|
||||||
setIsThinking,
|
|
||||||
messageIdRef,
|
|
||||||
addQuestion,
|
|
||||||
updateLastConversation,
|
|
||||||
updateConversationScore,
|
|
||||||
resetConversation,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
import { useState, useRef, useCallback } from 'react';
|
|
||||||
import { message } from '@ctzhian/ui';
|
|
||||||
import { UploadedImage } from '../types';
|
|
||||||
import { MAX_IMAGES, MAX_IMAGE_SIZE } from '../constants';
|
|
||||||
|
|
||||||
export const useImageUpload = () => {
|
|
||||||
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const cleanupImageUrls = useCallback((images: UploadedImage[]) => {
|
|
||||||
images.forEach(img => {
|
|
||||||
if (img.url.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(img.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleImageSelect = useCallback(
|
|
||||||
async (files: FileList | null) => {
|
|
||||||
if (!files || files.length === 0) return;
|
|
||||||
|
|
||||||
const remainingSlots = MAX_IMAGES - uploadedImages.length;
|
|
||||||
if (remainingSlots <= 0) {
|
|
||||||
message.warning(`最多只能上传 ${MAX_IMAGES} 张图片`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesToAdd = Array.from(files).slice(0, remainingSlots);
|
|
||||||
const newImages: UploadedImage[] = [];
|
|
||||||
|
|
||||||
for (const file of filesToAdd) {
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
message.error('只支持上传图片文件');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > MAX_IMAGE_SIZE) {
|
|
||||||
message.error('图片大小不能超过 10MB');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const localUrl = URL.createObjectURL(file);
|
|
||||||
newImages.push({
|
|
||||||
id: Date.now().toString() + Math.random(),
|
|
||||||
url: localUrl,
|
|
||||||
file,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploadedImages(prev => [...prev, ...newImages]);
|
|
||||||
},
|
|
||||||
[uploadedImages.length],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleImageUpload = useCallback(
|
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
handleImageSelect(event.target.files);
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = '';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleImageSelect],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRemoveImage = useCallback((id: string) => {
|
|
||||||
setUploadedImages(prev => {
|
|
||||||
const imageToRemove = prev.find(img => img.id === id);
|
|
||||||
if (imageToRemove && imageToRemove.url.startsWith('blob:')) {
|
|
||||||
URL.revokeObjectURL(imageToRemove.url);
|
|
||||||
}
|
|
||||||
return prev.filter(img => img.id !== id);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePaste = useCallback(
|
|
||||||
async (e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
||||||
const items = e.clipboardData?.items;
|
|
||||||
if (!items) return;
|
|
||||||
|
|
||||||
const imageFiles: File[] = [];
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
if (item.type.startsWith('image/')) {
|
|
||||||
const file = item.getAsFile();
|
|
||||||
if (file) imageFiles.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageFiles.length > 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
imageFiles.forEach(file => dataTransfer.items.add(file));
|
|
||||||
await handleImageSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleImageSelect],
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearImages = useCallback(() => {
|
|
||||||
cleanupImageUrls(uploadedImages);
|
|
||||||
setUploadedImages([]);
|
|
||||||
}, [uploadedImages, cleanupImageUrls]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
uploadedImages,
|
|
||||||
fileInputRef,
|
|
||||||
handleImageUpload,
|
|
||||||
handleRemoveImage,
|
|
||||||
handlePaste,
|
|
||||||
clearImages,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
import SSEClient from '@/utils/fetch';
|
|
||||||
import { message } from '@ctzhian/ui';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useCallback, useRef } from 'react';
|
|
||||||
import { AnswerStatusType, CAP_CONFIG, SSE_CONFIG } from '../constants';
|
|
||||||
import { ChatRequestData, SSEMessageData } from '../types';
|
|
||||||
import { handleThinkingContent } from '../utils';
|
|
||||||
|
|
||||||
interface UseSSEChatProps {
|
|
||||||
conversationId: string;
|
|
||||||
setConversationId: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
nonce: string;
|
|
||||||
setNonce: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
messageIdRef: React.MutableRefObject<string>;
|
|
||||||
setFullAnswer: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
setAnswer: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
setThinkingContent: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
setChunkResult: React.Dispatch<React.SetStateAction<any[]>>;
|
|
||||||
setConversation: React.Dispatch<React.SetStateAction<any[]>>;
|
|
||||||
setIsChunkResult: (value: boolean) => void;
|
|
||||||
setIsThinking: (value: boolean) => void;
|
|
||||||
setThinking: (value: AnswerStatusType) => void;
|
|
||||||
setLoading: (value: boolean) => void;
|
|
||||||
scrollToBottom: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSSEChat = ({
|
|
||||||
conversationId,
|
|
||||||
setConversationId,
|
|
||||||
nonce,
|
|
||||||
setNonce,
|
|
||||||
messageIdRef,
|
|
||||||
setFullAnswer,
|
|
||||||
setAnswer,
|
|
||||||
setThinkingContent,
|
|
||||||
setChunkResult,
|
|
||||||
setConversation,
|
|
||||||
setIsChunkResult,
|
|
||||||
setIsThinking,
|
|
||||||
setThinking,
|
|
||||||
setLoading,
|
|
||||||
scrollToBottom,
|
|
||||||
}: UseSSEChatProps) => {
|
|
||||||
const sseClientRef = useRef<SSEClient<SSEMessageData> | null>(null);
|
|
||||||
|
|
||||||
const initializeSSE = useCallback(() => {
|
|
||||||
sseClientRef.current = new SSEClient({
|
|
||||||
url: SSE_CONFIG.url,
|
|
||||||
headers: SSE_CONFIG.headers,
|
|
||||||
onCancel: () => {
|
|
||||||
setLoading(false);
|
|
||||||
setThinking(4);
|
|
||||||
setAnswer(prev => {
|
|
||||||
let value = '';
|
|
||||||
if (prev) {
|
|
||||||
value = prev + '\n\n<error>Request canceled</error>';
|
|
||||||
}
|
|
||||||
setConversation(prev => {
|
|
||||||
const newConversation = [...prev];
|
|
||||||
if (newConversation[newConversation.length - 1]) {
|
|
||||||
newConversation[newConversation.length - 1].a = value;
|
|
||||||
newConversation[newConversation.length - 1].update_time =
|
|
||||||
dayjs().format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
newConversation[newConversation.length - 1].message_id =
|
|
||||||
messageIdRef.current;
|
|
||||||
}
|
|
||||||
return newConversation;
|
|
||||||
});
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [messageIdRef, setAnswer, setConversation, setLoading, setThinking]);
|
|
||||||
|
|
||||||
const chatAnswer = useCallback(
|
|
||||||
async (q: string) => {
|
|
||||||
setLoading(true);
|
|
||||||
setThinking(1);
|
|
||||||
|
|
||||||
let token = '';
|
|
||||||
try {
|
|
||||||
const Cap = (await import('@cap.js/widget')).default;
|
|
||||||
const cap = new Cap({ apiEndpoint: CAP_CONFIG.apiEndpoint });
|
|
||||||
const solution = await cap.solve();
|
|
||||||
token = solution.token;
|
|
||||||
} catch (error) {
|
|
||||||
message.error('验证失败');
|
|
||||||
console.error('Captcha error:', error);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reqData: ChatRequestData = {
|
|
||||||
message: q,
|
|
||||||
nonce: nonce || '',
|
|
||||||
conversation_id: conversationId || '',
|
|
||||||
app_type: 1,
|
|
||||||
captcha_token: token,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sseClientRef.current) {
|
|
||||||
sseClientRef.current.subscribe(
|
|
||||||
JSON.stringify(reqData),
|
|
||||||
({ type, content, chunk_result }) => {
|
|
||||||
if (type === 'conversation_id') {
|
|
||||||
setConversationId(prev => prev + content);
|
|
||||||
} else if (type === 'message_id') {
|
|
||||||
messageIdRef.current += content;
|
|
||||||
} else if (type === 'nonce') {
|
|
||||||
setNonce(prev => prev + content);
|
|
||||||
} else if (type === 'error') {
|
|
||||||
setLoading(false);
|
|
||||||
setIsChunkResult(false);
|
|
||||||
setIsThinking(false);
|
|
||||||
setThinking(4);
|
|
||||||
setAnswer(prev => {
|
|
||||||
if (content) {
|
|
||||||
return prev + `\n\n回答出现错误:<error>${content}</error>`;
|
|
||||||
}
|
|
||||||
return prev + '\n\n回答出现错误,请重试';
|
|
||||||
});
|
|
||||||
if (content) message.error(content);
|
|
||||||
} else if (type === 'done') {
|
|
||||||
setAnswer(prevAnswer => {
|
|
||||||
setThinkingContent(prevThinkingContent => {
|
|
||||||
setChunkResult(prevChunkResult => {
|
|
||||||
setConversation(prev => {
|
|
||||||
const newConversation = [...prev];
|
|
||||||
const lastConversation =
|
|
||||||
newConversation[newConversation.length - 1];
|
|
||||||
if (lastConversation) {
|
|
||||||
lastConversation.a = prevAnswer;
|
|
||||||
lastConversation.update_time = dayjs().format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
);
|
|
||||||
lastConversation.message_id = messageIdRef.current;
|
|
||||||
lastConversation.source = 'chat';
|
|
||||||
lastConversation.chunk_result = prevChunkResult;
|
|
||||||
lastConversation.thinking_content = prevThinkingContent;
|
|
||||||
}
|
|
||||||
return newConversation;
|
|
||||||
});
|
|
||||||
return prevChunkResult;
|
|
||||||
});
|
|
||||||
return prevThinkingContent;
|
|
||||||
});
|
|
||||||
return '';
|
|
||||||
});
|
|
||||||
|
|
||||||
setFullAnswer('');
|
|
||||||
setLoading(false);
|
|
||||||
setIsChunkResult(false);
|
|
||||||
setIsThinking(false);
|
|
||||||
setThinking(4);
|
|
||||||
} else if (type === 'data') {
|
|
||||||
setIsChunkResult(false);
|
|
||||||
setFullAnswer(prevFullAnswer => {
|
|
||||||
const newFullAnswer = prevFullAnswer + content;
|
|
||||||
const { thinkingContent, answerContent } =
|
|
||||||
handleThinkingContent(newFullAnswer);
|
|
||||||
|
|
||||||
setThinkingContent(thinkingContent);
|
|
||||||
setAnswer(answerContent);
|
|
||||||
|
|
||||||
if (newFullAnswer.includes('</think>')) {
|
|
||||||
setIsThinking(false);
|
|
||||||
setThinking(3);
|
|
||||||
} else if (newFullAnswer.includes('<think>')) {
|
|
||||||
setIsThinking(true);
|
|
||||||
setThinking(2);
|
|
||||||
} else {
|
|
||||||
setThinking(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newFullAnswer;
|
|
||||||
});
|
|
||||||
} else if (type === 'chunk_result') {
|
|
||||||
setChunkResult(prev => [...prev, chunk_result]);
|
|
||||||
setIsChunkResult(true);
|
|
||||||
setTimeout(scrollToBottom, 200);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
conversationId,
|
|
||||||
nonce,
|
|
||||||
messageIdRef,
|
|
||||||
setConversationId,
|
|
||||||
setNonce,
|
|
||||||
setLoading,
|
|
||||||
setThinking,
|
|
||||||
setAnswer,
|
|
||||||
setFullAnswer,
|
|
||||||
setThinkingContent,
|
|
||||||
setChunkResult,
|
|
||||||
setConversation,
|
|
||||||
setIsChunkResult,
|
|
||||||
setIsThinking,
|
|
||||||
scrollToBottom,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSearchAbort = useCallback(() => {
|
|
||||||
sseClientRef.current?.unsubscribe();
|
|
||||||
setLoading(false);
|
|
||||||
setThinking(4);
|
|
||||||
}, [setLoading, setThinking]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sseClientRef,
|
|
||||||
initializeSSE,
|
|
||||||
chatAnswer,
|
|
||||||
handleSearchAbort,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,222 +1,413 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { WidgetInfo } from '@/assets/type';
|
|
||||||
|
import { ChunkResultItem, ConversationItem } from '@/assets/type';
|
||||||
|
import { IconFile, IconFolder, IconLogo } from '@/components/icons';
|
||||||
import { useStore } from '@/provider';
|
import { useStore } from '@/provider';
|
||||||
import {
|
import SSEClient from '@/utils/fetch';
|
||||||
alpha,
|
import { Box, Stack, useMediaQuery } from '@mui/material';
|
||||||
Box,
|
import { Ellipsis, message } from '@ctzhian/ui';
|
||||||
Button,
|
import dayjs from 'dayjs';
|
||||||
lighten,
|
import Link from 'next/link';
|
||||||
Stack,
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
styled,
|
import { AnswerStatus } from '../chat/constant';
|
||||||
Tab,
|
import ChatInput from './ChatInput';
|
||||||
Tabs,
|
import ChatWindow from './ChatWindow';
|
||||||
Typography,
|
import WaterMarkProvider from '@/components/watermark/WaterMarkProvider';
|
||||||
} from '@mui/material';
|
|
||||||
import { IconJinsousuo, IconZhinengwenda } from '@panda-wiki/icons';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import AiQaContent from './AiQaContent';
|
|
||||||
import SearchDocContent from './SearchDocContent';
|
|
||||||
|
|
||||||
const StyledTabs = styled(Tabs)(({ theme }) => ({
|
|
||||||
minHeight: 'auto',
|
|
||||||
position: 'relative',
|
|
||||||
borderRadius: '10px',
|
|
||||||
padding: theme.spacing(0.5),
|
|
||||||
border: `1px solid ${alpha(theme.palette.text.primary, 0.1)}`,
|
|
||||||
'& .MuiTabs-indicator': {
|
|
||||||
height: '100%',
|
|
||||||
borderRadius: '8px',
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
zIndex: 0,
|
|
||||||
},
|
|
||||||
'& .MuiTabs-flexContainer': {
|
|
||||||
gap: theme.spacing(0.5),
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 样式化的 Tab 组件 - 白色背景,圆角,深灰色文字
|
|
||||||
const StyledTab = styled(Tab)(({ theme }) => ({
|
|
||||||
minHeight: 'auto',
|
|
||||||
padding: theme.spacing(0.75, 2),
|
|
||||||
borderRadius: '6px',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 400,
|
|
||||||
textTransform: 'none',
|
|
||||||
transition: 'color 0.3s ease-in-out',
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 1,
|
|
||||||
lineHeight: 1,
|
|
||||||
'&:hover': {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
'&.Mui-selected': {
|
|
||||||
color: theme.palette.primary.contrastText,
|
|
||||||
fontWeight: 500,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Widget = () => {
|
const Widget = () => {
|
||||||
const { widget, mobile } = useStore();
|
const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm'));
|
||||||
|
const { widget, themeMode } = useStore();
|
||||||
|
|
||||||
const defaultSearchMode = useMemo(() => {
|
const chatContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
return widget?.settings?.widget_bot_settings?.search_mode || 'all';
|
const sseClientRef = useRef<SSEClient<{
|
||||||
}, [widget]);
|
type: string;
|
||||||
|
content: string;
|
||||||
|
chunk_result: ChunkResultItem[];
|
||||||
|
}> | null>(null);
|
||||||
|
|
||||||
const [searchMode, setSearchMode] = useState<
|
const messageIdRef = useRef<string>('');
|
||||||
WidgetInfo['settings']['widget_bot_settings']['search_mode']
|
const [conversation, setConversation] = useState<ConversationItem[]>([]);
|
||||||
>(defaultSearchMode !== 'doc' ? 'qa' : 'doc');
|
const [loading, setLoading] = useState(false);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const [thinking, setThinking] = useState<keyof typeof AnswerStatus>(4);
|
||||||
const aiQaInputRef = useRef<HTMLInputElement>(null);
|
const [nonce, setNonce] = useState('');
|
||||||
|
const [conversationId, setConversationId] = useState('');
|
||||||
|
const [answer, setAnswer] = useState('');
|
||||||
|
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||||
|
|
||||||
const placeholder = useMemo(() => {
|
const chatAnswer = async (q: string) => {
|
||||||
return widget?.settings?.widget_bot_settings?.placeholder || '搜索...';
|
setLoading(true);
|
||||||
}, [widget]);
|
setThinking(1);
|
||||||
|
setIsUserScrolling(false);
|
||||||
|
|
||||||
const hotSearch = useMemo(() => {
|
const reqData = {
|
||||||
return widget?.settings?.widget_bot_settings?.recommend_questions || [];
|
message: q,
|
||||||
}, [widget]);
|
nonce: '',
|
||||||
|
conversation_id: '',
|
||||||
|
app_type: 2,
|
||||||
|
};
|
||||||
|
if (conversationId) reqData.conversation_id = conversationId;
|
||||||
|
if (nonce) reqData.nonce = nonce;
|
||||||
|
|
||||||
// modal打开时自动聚焦
|
if (sseClientRef.current) {
|
||||||
useEffect(() => {
|
sseClientRef.current.subscribe(
|
||||||
setTimeout(() => {
|
JSON.stringify(reqData),
|
||||||
if (searchMode === 'qa') {
|
({ type, content }) => {
|
||||||
aiQaInputRef.current?.querySelector('textarea')?.focus();
|
if (type === 'conversation_id') {
|
||||||
} else {
|
setConversationId(prev => prev + content);
|
||||||
inputRef.current?.querySelector('input')?.focus();
|
} else if (type === 'message_id') {
|
||||||
|
messageIdRef.current += content;
|
||||||
|
} else if (type === 'nonce') {
|
||||||
|
setNonce(prev => prev + content);
|
||||||
|
} else if (type === 'error') {
|
||||||
|
setLoading(false);
|
||||||
|
setThinking(4);
|
||||||
|
setAnswer(prev => {
|
||||||
|
if (content) {
|
||||||
|
return prev + `\n\n回答出现错误:<error>${content}</error>`;
|
||||||
}
|
}
|
||||||
}, 100);
|
return prev + '\n\n回答出现错误,请重试';
|
||||||
}, [searchMode]);
|
});
|
||||||
|
if (content) message.error(content);
|
||||||
|
} else if (type === 'done') {
|
||||||
|
setAnswer(prevAnswer => {
|
||||||
|
setConversation(prev => {
|
||||||
|
const newConversation = [...prev];
|
||||||
|
newConversation[newConversation.length - 1].a = prevAnswer;
|
||||||
|
newConversation[newConversation.length - 1].update_time =
|
||||||
|
dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
newConversation[newConversation.length - 1].message_id =
|
||||||
|
messageIdRef.current;
|
||||||
|
return newConversation;
|
||||||
|
});
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
setThinking(4);
|
||||||
|
} else if (type === 'data') {
|
||||||
|
setAnswer(prev => {
|
||||||
|
const newAnswer = prev + content;
|
||||||
|
if (newAnswer.includes('</think>')) {
|
||||||
|
setThinking(3);
|
||||||
|
return newAnswer;
|
||||||
|
}
|
||||||
|
if (newAnswer.includes('<think>')) {
|
||||||
|
setThinking(2);
|
||||||
|
return newAnswer;
|
||||||
|
}
|
||||||
|
setThinking(3);
|
||||||
|
return newAnswer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = (q: string, reset: boolean = false) => {
|
||||||
|
if (loading || !q.trim()) return;
|
||||||
|
const newConversation = reset ? [] : [...conversation];
|
||||||
|
newConversation.push({
|
||||||
|
q,
|
||||||
|
a: '',
|
||||||
|
score: 0,
|
||||||
|
update_time: '',
|
||||||
|
message_id: '',
|
||||||
|
source: 'chat',
|
||||||
|
chunk_result: [],
|
||||||
|
});
|
||||||
|
messageIdRef.current = '';
|
||||||
|
setConversation(newConversation);
|
||||||
|
setAnswer('');
|
||||||
|
setTimeout(() => {
|
||||||
|
chatAnswer(q);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchAbort = () => {
|
||||||
|
if (loading) {
|
||||||
|
sseClientRef.current?.unsubscribe();
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
setThinking(4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (chatContainerRef?.current) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } =
|
||||||
|
chatContainerRef.current;
|
||||||
|
setIsUserScrolling(scrollTop + clientHeight < scrollHeight);
|
||||||
|
}
|
||||||
|
}, [chatContainerRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUserScrolling && chatContainerRef?.current) {
|
||||||
|
chatContainerRef.current.scrollTop =
|
||||||
|
chatContainerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [answer, isUserScrolling]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const chatContainer = chatContainerRef?.current;
|
||||||
|
chatContainer?.addEventListener('scroll', handleScroll);
|
||||||
|
return () => {
|
||||||
|
chatContainer?.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sseClientRef.current = new SSEClient({
|
||||||
|
url: `/share/v1/chat/widget`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
setLoading(false);
|
||||||
|
setThinking(4);
|
||||||
|
setAnswer(prev => {
|
||||||
|
let value = '';
|
||||||
|
if (prev) {
|
||||||
|
value = prev + '\n\n<error>Request canceled</error>';
|
||||||
|
}
|
||||||
|
setConversation(prev => {
|
||||||
|
const newConversation = [...prev];
|
||||||
|
if (newConversation.length > 0) {
|
||||||
|
newConversation[newConversation.length - 1].a = value;
|
||||||
|
newConversation[newConversation.length - 1].update_time =
|
||||||
|
dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
newConversation[newConversation.length - 1].message_id =
|
||||||
|
messageIdRef.current;
|
||||||
|
}
|
||||||
|
return newConversation;
|
||||||
|
});
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<WaterMarkProvider>
|
||||||
sx={theme => ({
|
<Stack
|
||||||
display: 'flex',
|
direction={'row'}
|
||||||
flexDirection: 'column',
|
alignItems={'flex-start'}
|
||||||
flex: 1,
|
justifyContent={'space-between'}
|
||||||
maxWidth: '100vw',
|
gap={2}
|
||||||
height: '100vh',
|
|
||||||
backgroundColor: lighten(theme.palette.background.default, 0.05),
|
|
||||||
borderRadius: '10px',
|
|
||||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.12)',
|
|
||||||
overflow: 'hidden',
|
|
||||||
outline: 'none',
|
|
||||||
pb: 2,
|
|
||||||
})}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{/* 顶部标签栏 */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
p: 3,
|
||||||
alignItems: 'center',
|
bgcolor: 'primary.main',
|
||||||
justifyContent: 'space-between',
|
pb: '36px',
|
||||||
px: 2,
|
|
||||||
pt: 2,
|
|
||||||
pb: 2.5,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{defaultSearchMode === 'all' ? (
|
<Box sx={{ flex: 1, width: 0, color: 'light.main' }}>
|
||||||
<StyledTabs
|
<Stack
|
||||||
value={searchMode}
|
direction={'row'}
|
||||||
onChange={(_, value) => {
|
alignItems={'center'}
|
||||||
setSearchMode(value as 'qa' | 'doc');
|
gap={1}
|
||||||
}}
|
sx={{ lineHeight: '28px', fontSize: 20 }}
|
||||||
variant='scrollable'
|
|
||||||
scrollButtons={false}
|
|
||||||
>
|
>
|
||||||
<StyledTab
|
{widget?.settings?.widget_bot_settings?.btn_logo ||
|
||||||
label={
|
widget?.settings?.icon ? (
|
||||||
<Stack direction='row' gap={0.5} alignItems='center'>
|
<img
|
||||||
<IconZhinengwenda sx={{ fontSize: 16 }} />
|
src={
|
||||||
{!mobile && <span>智能问答</span>}
|
widget?.settings?.widget_bot_settings?.btn_logo ||
|
||||||
</Stack>
|
widget?.settings?.icon
|
||||||
}
|
}
|
||||||
value='qa'
|
height={24}
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
<StyledTab
|
|
||||||
label={
|
|
||||||
<Stack direction='row' gap={0.5} alignItems='center'>
|
|
||||||
<IconJinsousuo sx={{ fontSize: 16 }} />
|
|
||||||
{!mobile && <span>仅搜索文档</span>}
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
value='doc'
|
|
||||||
/>
|
|
||||||
</StyledTabs>
|
|
||||||
) : (
|
) : (
|
||||||
<Box></Box>
|
<IconLogo sx={{ fontSize: 24 }} />
|
||||||
)}
|
)}
|
||||||
<Button
|
<Ellipsis sx={{ pr: 2 }}>
|
||||||
variant='outlined'
|
|
||||||
color='primary'
|
|
||||||
size='small'
|
|
||||||
sx={theme => ({
|
|
||||||
minWidth: 'auto',
|
|
||||||
px: 1,
|
|
||||||
py: '1px',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 500,
|
|
||||||
textTransform: 'none',
|
|
||||||
color: 'text.secondary',
|
|
||||||
borderColor: alpha(theme.palette.text.primary, 0.1),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Esc
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
component={'span'}
|
||||||
px: 3,
|
sx={{ cursor: 'pointer' }}
|
||||||
flex: 1,
|
onClick={() => {
|
||||||
display: searchMode === 'qa' ? 'flex' : 'none',
|
handleSearchAbort();
|
||||||
flexDirection: 'column',
|
setConversation([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AiQaContent
|
{widget?.settings?.title || '在线客服'}
|
||||||
hotSearch={hotSearch}
|
</Box>
|
||||||
placeholder={placeholder}
|
</Ellipsis>
|
||||||
inputRef={aiQaInputRef}
|
</Stack>
|
||||||
|
<Ellipsis sx={{ fontSize: 14, opacity: 0.7, mt: 0.5 }}>
|
||||||
|
{widget?.settings?.welcome_str || '在线客服'}
|
||||||
|
</Ellipsis>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: themeMode === 'light' ? 'light.main' : 'dark.light',
|
||||||
|
p: 3,
|
||||||
|
mt: -2,
|
||||||
|
borderRadius: '12px 12px 0 0',
|
||||||
|
height: 'calc(100vh - 96px - 24px)',
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{conversation.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<Box>
|
||||||
|
<ChatInput
|
||||||
|
loading={loading}
|
||||||
|
thinking={thinking}
|
||||||
|
setThinking={setThinking}
|
||||||
|
onSearch={onSearch}
|
||||||
|
handleSearchAbort={handleSearchAbort}
|
||||||
|
placeholder={
|
||||||
|
widget?.settings?.search_placeholder || '请输入问题'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Stack
|
||||||
|
direction='row'
|
||||||
|
alignItems={'center'}
|
||||||
|
flexWrap='wrap'
|
||||||
|
gap={1.5}
|
||||||
sx={{
|
sx={{
|
||||||
px: 3,
|
mt: 2,
|
||||||
flex: 1,
|
|
||||||
display: searchMode === 'doc' ? 'flex' : 'none',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SearchDocContent inputRef={inputRef} placeholder={placeholder} />
|
{widget?.settings?.recommend_questions?.map(item => (
|
||||||
|
<Box
|
||||||
|
key={item}
|
||||||
|
onClick={() => onSearch(item, true)}
|
||||||
|
sx={{
|
||||||
|
border: '1px solid',
|
||||||
|
borderRadius: '16px',
|
||||||
|
fontSize: 14,
|
||||||
|
color: 'text.secondary',
|
||||||
|
lineHeight: '32px',
|
||||||
|
height: '32px',
|
||||||
|
borderColor: 'divider',
|
||||||
|
px: 2,
|
||||||
|
cursor: 'pointer',
|
||||||
|
bgcolor:
|
||||||
|
themeMode === 'dark'
|
||||||
|
? 'background.paper3'
|
||||||
|
: 'background.default',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
color: 'primary.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
</Box>
|
</Box>
|
||||||
|
))}
|
||||||
{/* 底部AI生成提示 */}
|
</Stack>
|
||||||
|
{widget?.recommend_nodes && widget.recommend_nodes.length > 0 && (
|
||||||
|
<Box sx={{ mt: 4.5 }}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
px: 3,
|
color: 'text.tertiary',
|
||||||
pt: widget?.settings?.widget_bot_settings?.disclaimer ? 2 : 0,
|
lineHeight: '22px',
|
||||||
display: 'flex',
|
fontSize: 14,
|
||||||
alignItems: 'center',
|
fontWeight: 'bold',
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
推荐内容
|
||||||
variant='caption'
|
</Box>
|
||||||
|
<Stack direction={'row'} flexWrap={'wrap'}>
|
||||||
|
{widget.recommend_nodes.map(it => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/node/${it.id}`}
|
||||||
|
target='_blank'
|
||||||
|
prefetch={false}
|
||||||
|
key={it.id}
|
||||||
|
style={{ width: isMobile ? '100%' : '50%' }}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={'row'}
|
||||||
|
alignItems={'center'}
|
||||||
|
gap={1}
|
||||||
|
key={it.id}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'text.disabled',
|
py: 2,
|
||||||
|
pr: isMobile ? 0 : 2,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
display: 'flex',
|
height: 53,
|
||||||
alignItems: 'center',
|
borderBottom: '1px solid',
|
||||||
gap: 1,
|
borderColor: 'divider',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: 'text.primary',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'primary.main',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>本网站由 PandaWiki 提供技术支持</Box>
|
{it.emoji ? (
|
||||||
</Typography>
|
<Box>{it.emoji}</Box>
|
||||||
|
) : it.type === 1 ? (
|
||||||
|
<IconFolder />
|
||||||
|
) : (
|
||||||
|
<IconFile />
|
||||||
|
)}
|
||||||
|
<Box>{it.name}</Box>
|
||||||
|
</Stack>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ChatWindow
|
||||||
|
conversation_id={conversationId}
|
||||||
|
conversation={conversation}
|
||||||
|
setConversation={setConversation}
|
||||||
|
answer={answer}
|
||||||
|
loading={loading}
|
||||||
|
thinking={thinking}
|
||||||
|
setThinking={setThinking}
|
||||||
|
onSearch={onSearch}
|
||||||
|
handleSearchAbort={handleSearchAbort}
|
||||||
|
placeholder={widget?.settings?.search_placeholder || '请输入问题'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
<Stack
|
||||||
|
direction={'row'}
|
||||||
|
alignItems={'center'}
|
||||||
|
gap={1}
|
||||||
|
justifyContent={'center'}
|
||||||
|
sx={{
|
||||||
|
height: 24,
|
||||||
|
fontSize: 12,
|
||||||
|
bgcolor: themeMode === 'light' ? 'light.main' : 'dark.light',
|
||||||
|
color: 'text.primary',
|
||||||
|
a: {
|
||||||
|
color: 'primary.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
本插件由
|
||||||
|
<Link
|
||||||
|
href={'https://pandawiki.docs.baizhi.cloud/'}
|
||||||
|
target='_blank'
|
||||||
|
prefetch={false}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={'row'}
|
||||||
|
alignItems={'center'}
|
||||||
|
gap={0.5}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'primary.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconLogo sx={{ fontSize: 16 }} />
|
||||||
|
<Box sx={{ fontWeight: 'bold' }}>PandaWiki</Box>
|
||||||
|
</Stack>
|
||||||
|
</Link>
|
||||||
|
提供技术支持
|
||||||
|
</Stack>
|
||||||
|
</WaterMarkProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { ChunkResultItem } from '@/assets/type';
|
|
||||||
|
|
||||||
export interface ConversationItem {
|
|
||||||
q: string;
|
|
||||||
a: string;
|
|
||||||
score: number;
|
|
||||||
update_time: string;
|
|
||||||
message_id: string;
|
|
||||||
source: 'history' | 'chat';
|
|
||||||
chunk_result: ChunkResultItem[];
|
|
||||||
thinking_content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UploadedImage {
|
|
||||||
id: string;
|
|
||||||
url: string;
|
|
||||||
file: File;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SSEMessageData {
|
|
||||||
type: string;
|
|
||||||
content: string;
|
|
||||||
chunk_result: ChunkResultItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatRequestData {
|
|
||||||
message: string;
|
|
||||||
nonce: string;
|
|
||||||
conversation_id: string;
|
|
||||||
app_type: number;
|
|
||||||
captcha_token: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
export const handleThinkingContent = (content: string) => {
|
|
||||||
const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|$)/g;
|
|
||||||
const thinkMatches = [];
|
|
||||||
let match;
|
|
||||||
while ((match = thinkRegex.exec(content)) !== null) {
|
|
||||||
thinkMatches.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let answerContent = content.replace(/<think>[\s\S]*?<\/think>/g, '');
|
|
||||||
answerContent = answerContent.replace(/<think>[\s\S]*$/, '');
|
|
||||||
|
|
||||||
return {
|
|
||||||
thinkingContent: thinkMatches.join(''),
|
|
||||||
answerContent: answerContent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"packageManager": "pnpm@10.12.1",
|
"packageManager": "pnpm@10.12.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctzhian/tiptap": "^1.13.2",
|
"@ctzhian/tiptap": "^1.12.21",
|
||||||
"@ctzhian/ui": "^7.0.5",
|
"@ctzhian/ui": "^7.0.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
|
|
||||||
1346
web/pnpm-lock.yaml
1346
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue