Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
bb8923e7
authored
May 29, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/ui/refactor' into ui/refactor
parents
8f6b0ede
66927d05
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
1278 additions
and
316 deletions
+1278
-316
dto/openai_request.go
+1
-1
relay/channel/api_request.go
+6
-5
relay/relay-image.go
+29
-63
web/src/index.css
+111
-11
web/src/pages/Playground/Playground.js
+1131
-236
No files found.
dto/openai_request.go
View file @
bb8923e7
...
@@ -43,7 +43,7 @@ type GeneralOpenAIRequest struct {
...
@@ -43,7 +43,7 @@ type GeneralOpenAIRequest struct {
ResponseFormat
*
ResponseFormat
`json:"response_format,omitempty"`
ResponseFormat
*
ResponseFormat
`json:"response_format,omitempty"`
EncodingFormat
any
`json:"encoding_format,omitempty"`
EncodingFormat
any
`json:"encoding_format,omitempty"`
Seed
float64
`json:"seed,omitempty"`
Seed
float64
`json:"seed,omitempty"`
ParallelTooCalls
bool
`json:"parallel_tool_calls,omitempty"`
ParallelTooCalls
*
bool
`json:"parallel_tool_calls,omitempty"`
Tools
[]
ToolCallRequest
`json:"tools,omitempty"`
Tools
[]
ToolCallRequest
`json:"tools,omitempty"`
ToolChoice
any
`json:"tool_choice,omitempty"`
ToolChoice
any
`json:"tool_choice,omitempty"`
User
string
`json:"user,omitempty"`
User
string
`json:"user,omitempty"`
...
...
relay/channel/api_request.go
View file @
bb8923e7
...
@@ -122,11 +122,13 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
...
@@ -122,11 +122,13 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
var
pingerWg
sync
.
WaitGroup
var
pingerWg
sync
.
WaitGroup
if
info
.
IsStream
{
if
info
.
IsStream
{
helper
.
SetEventStreamHeaders
(
c
)
helper
.
SetEventStreamHeaders
(
c
)
pingInterval
:=
time
.
Duration
(
generalSettings
.
PingIntervalSeconds
)
*
time
.
Second
var
pingerCtx
context
.
Context
pingerCtx
,
stopPinger
=
context
.
WithCancel
(
c
.
Request
.
Context
())
if
pingEnabled
{
if
pingEnabled
{
pingInterval
:=
time
.
Duration
(
generalSettings
.
PingIntervalSeconds
)
*
time
.
Second
var
pingerCtx
context
.
Context
pingerCtx
,
stopPinger
=
context
.
WithCancel
(
c
.
Request
.
Context
())
// 退出时清理 pingerCtx 防止泄露
defer
stopPinger
()
pingerWg
.
Add
(
1
)
pingerWg
.
Add
(
1
)
gopool
.
Go
(
func
()
{
gopool
.
Go
(
func
()
{
defer
pingerWg
.
Done
()
defer
pingerWg
.
Done
()
...
@@ -166,9 +168,8 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
...
@@ -166,9 +168,8 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
}
}
resp
,
err
:=
client
.
Do
(
req
)
resp
,
err
:=
client
.
Do
(
req
)
// request结束后
停止ping
// request结束后
等待 ping goroutine 完成
if
info
.
IsStream
&&
pingEnabled
{
if
info
.
IsStream
&&
pingEnabled
{
stopPinger
()
pingerWg
.
Wait
()
pingerWg
.
Wait
()
}
}
if
err
!=
nil
{
if
err
!=
nil
{
...
...
relay/relay-image.go
View file @
bb8923e7
...
@@ -46,11 +46,23 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
...
@@ -46,11 +46,23 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
if
imageRequest
.
Model
==
""
{
imageRequest
.
Model
=
"dall-e-3"
}
if
strings
.
Contains
(
imageRequest
.
Size
,
"×"
)
{
return
nil
,
errors
.
New
(
"size an unexpected error occurred in the parameter, please use 'x' instead of the multiplication sign '×'"
)
}
// Not "256x256", "512x512", or "1024x1024"
// Not "256x256", "512x512", or "1024x1024"
if
imageRequest
.
Model
==
"dall-e-2"
||
imageRequest
.
Model
==
"dall-e"
{
if
imageRequest
.
Model
==
"dall-e-2"
||
imageRequest
.
Model
==
"dall-e"
{
if
imageRequest
.
Size
!=
""
&&
imageRequest
.
Size
!=
"256x256"
&&
imageRequest
.
Size
!=
"512x512"
&&
imageRequest
.
Size
!=
"1024x1024"
{
if
imageRequest
.
Size
!=
""
&&
imageRequest
.
Size
!=
"256x256"
&&
imageRequest
.
Size
!=
"512x512"
&&
imageRequest
.
Size
!=
"1024x1024"
{
return
nil
,
errors
.
New
(
"size must be one of 256x256, 512x512, or 1024x1024 for dall-e-2 or dall-e"
)
return
nil
,
errors
.
New
(
"size must be one of 256x256, 512x512, or 1024x1024 for dall-e-2 or dall-e"
)
}
}
if
imageRequest
.
Size
==
""
{
imageRequest
.
Size
=
"1024x1024"
}
}
else
if
imageRequest
.
Model
==
"dall-e-3"
{
}
else
if
imageRequest
.
Model
==
"dall-e-3"
{
if
imageRequest
.
Size
!=
""
&&
imageRequest
.
Size
!=
"1024x1024"
&&
imageRequest
.
Size
!=
"1024x1792"
&&
imageRequest
.
Size
!=
"1792x1024"
{
if
imageRequest
.
Size
!=
""
&&
imageRequest
.
Size
!=
"1024x1024"
&&
imageRequest
.
Size
!=
"1024x1792"
&&
imageRequest
.
Size
!=
"1792x1024"
{
return
nil
,
errors
.
New
(
"size must be one of 1024x1024, 1024x1792 or 1792x1024 for dall-e-3"
)
return
nil
,
errors
.
New
(
"size must be one of 1024x1024, 1024x1792 or 1792x1024 for dall-e-3"
)
...
@@ -58,74 +70,24 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
...
@@ -58,74 +70,24 @@ func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
if
imageRequest
.
Quality
==
""
{
if
imageRequest
.
Quality
==
""
{
imageRequest
.
Quality
=
"standard"
imageRequest
.
Quality
=
"standard"
}
}
// N should between 1 and 10
if
imageRequest
.
Size
==
""
{
//if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
imageRequest
.
Size
=
"1024x1024"
// return service.OpenAIErrorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
}
//}
}
else
if
imageRequest
.
Model
==
"gpt-image-1"
{
if
imageRequest
.
Quality
==
""
{
imageRequest
.
Quality
=
"auto"
}
}
}
}
if
imageRequest
.
Prompt
==
""
{
return
nil
,
errors
.
New
(
"prompt is required"
)
}
if
imageRequest
.
Model
==
""
{
if
imageRequest
.
Prompt
==
""
{
imageRequest
.
Model
=
"dall-e-2"
return
nil
,
errors
.
New
(
"prompt is required"
)
}
if
strings
.
Contains
(
imageRequest
.
Size
,
"×"
)
{
return
nil
,
errors
.
New
(
"size an unexpected error occurred in the parameter, please use 'x' instead of the multiplication sign '×'"
)
}
if
imageRequest
.
N
==
0
{
imageRequest
.
N
=
1
}
if
imageRequest
.
Size
==
""
{
imageRequest
.
Size
=
"1024x1024"
}
err
:=
common
.
UnmarshalBodyReusable
(
c
,
imageRequest
)
if
err
!=
nil
{
return
nil
,
err
}
if
imageRequest
.
Prompt
==
""
{
return
nil
,
errors
.
New
(
"prompt is required"
)
}
if
strings
.
Contains
(
imageRequest
.
Size
,
"×"
)
{
return
nil
,
errors
.
New
(
"size an unexpected error occurred in the parameter, please use 'x' instead of the multiplication sign '×'"
)
}
if
imageRequest
.
N
==
0
{
imageRequest
.
N
=
1
}
if
imageRequest
.
Size
==
""
{
imageRequest
.
Size
=
"1024x1024"
}
if
imageRequest
.
Model
==
""
{
imageRequest
.
Model
=
"dall-e-2"
}
// x.ai grok-2-image not support size, quality or style
if
imageRequest
.
Size
==
"empty"
{
imageRequest
.
Size
=
""
}
// Not "256x256", "512x512", or "1024x1024"
if
imageRequest
.
Model
==
"dall-e-2"
||
imageRequest
.
Model
==
"dall-e"
{
if
imageRequest
.
Size
!=
""
&&
imageRequest
.
Size
!=
"256x256"
&&
imageRequest
.
Size
!=
"512x512"
&&
imageRequest
.
Size
!=
"1024x1024"
{
return
nil
,
errors
.
New
(
"size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024"
)
}
}
}
else
if
imageRequest
.
Model
==
"dall-e-3"
{
if
imageRequest
.
Size
!=
""
&&
imageRequest
.
Size
!=
"1024x1024"
&&
imageRequest
.
Size
!=
"1024x1792"
&&
imageRequest
.
Size
!=
"1792x1024"
{
if
imageRequest
.
N
==
0
{
return
nil
,
errors
.
New
(
"size must be one of 256x256, 512x512, or 1024x1024, dall-e-3 1024x1792 or 1792x1024"
)
imageRequest
.
N
=
1
}
if
imageRequest
.
Quality
==
""
{
imageRequest
.
Quality
=
"standard"
}
}
//if imageRequest.N != 1 {
// return nil, errors.New("n must be 1")
//}
}
}
// N should between 1 and 10
//if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) {
// return service.OpenAIErrorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest)
//}
if
setting
.
ShouldCheckPromptSensitive
()
{
if
setting
.
ShouldCheckPromptSensitive
()
{
words
,
err
:=
service
.
CheckSensitiveInput
(
imageRequest
.
Prompt
)
words
,
err
:=
service
.
CheckSensitiveInput
(
imageRequest
.
Prompt
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -229,6 +191,10 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
...
@@ -229,6 +191,10 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
requestBody
=
bytes
.
NewBuffer
(
jsonData
)
requestBody
=
bytes
.
NewBuffer
(
jsonData
)
}
}
if
common
.
DebugEnabled
{
println
(
fmt
.
Sprintf
(
"image request body: %s"
,
requestBody
))
}
statusCodeMappingStr
:=
c
.
GetString
(
"status_code_mapping"
)
statusCodeMappingStr
:=
c
.
GetString
(
"status_code_mapping"
)
resp
,
err
:=
adaptor
.
DoRequest
(
c
,
relayInfo
,
requestBody
)
resp
,
err
:=
adaptor
.
DoRequest
(
c
,
relayInfo
,
requestBody
)
...
...
web/src/index.css
View file @
bb8923e7
...
@@ -98,17 +98,6 @@ body {
...
@@ -98,17 +98,6 @@ body {
display
:
revert
;
display
:
revert
;
}
}
.semi-chat
{
padding-top
:
0
!important
;
padding-bottom
:
0
!important
;
height
:
100%
;
}
.semi-chat-chatBox-content
{
min-width
:
auto
;
word-break
:
break-word
;
}
.tableHiddle
{
.tableHiddle
{
display
:
none
!important
;
display
:
none
!important
;
}
}
...
@@ -158,4 +147,114 @@ code {
...
@@ -158,4 +147,114 @@ code {
.semi-tabs-content
{
.semi-tabs-content
{
padding
:
0
!important
;
padding
:
0
!important
;
}
/* 聊天 */
.semi-chat
{
padding-top
:
0
!important
;
padding-bottom
:
0
!important
;
height
:
100%
;
max-width
:
100%
!important
;
width
:
100%
!important
;
overflow
:
hidden
!important
;
}
.semi-chat-chatBox
{
max-width
:
100%
!important
;
overflow
:
hidden
!important
;
}
.semi-chat-chatBox-wrap
{
max-width
:
100%
!important
;
overflow
:
hidden
!important
;
}
.semi-chat-chatBox-content
{
min-width
:
auto
;
word-break
:
break-word
;
max-width
:
100%
!important
;
overflow-wrap
:
break-word
!important
;
}
.semi-chat-content
{
max-width
:
100%
!important
;
overflow
:
hidden
!important
;
}
.semi-chat-list
{
max-width
:
100%
!important
;
overflow-x
:
hidden
!important
;
}
.semi-chat-container
{
overflow-x
:
hidden
!important
;
}
.semi-chat-container
::-webkit-scrollbar
{
width
:
4px
;
}
.semi-chat-container
::-webkit-scrollbar-thumb
{
background
:
rgba
(
0
,
0
,
0
,
0.1
);
border-radius
:
2px
;
}
.semi-chat-container
::-webkit-scrollbar-thumb:hover
{
background
:
rgba
(
0
,
0
,
0
,
0.15
);
}
.semi-chat-container
::-webkit-scrollbar-track
{
background
:
transparent
;
}
/* 隐藏模型设置区域的滚动条 */
.model-settings-scroll
::-webkit-scrollbar
{
display
:
none
;
}
.model-settings-scroll
{
-ms-overflow-style
:
none
;
scrollbar-width
:
none
;
}
/* 调试面板代码样式 */
.debug-code
{
font-family
:
'Monaco'
,
'Menlo'
,
'Ubuntu Mono'
,
monospace
;
font-size
:
11px
;
line-height
:
1.4
;
}
/* 调试面板标签样式 */
.semi-tabs-content
{
height
:
calc
(
100%
-
40px
)
!important
;
flex
:
1
!important
;
}
.semi-tabs-content
.semi-tabs-pane
{
height
:
100%
!important
;
overflow
:
hidden
!important
;
}
.semi-tabs-content
.semi-tabs-pane
>
div
{
height
:
100%
!important
;
}
/* 调试面板特定样式 */
.debug-panel
.semi-tabs
{
height
:
100%
!important
;
display
:
flex
!important
;
flex-direction
:
column
!important
;
}
.debug-panel
.semi-tabs-bar
{
flex-shrink
:
0
!important
;
}
.debug-panel
.semi-tabs-content
{
flex
:
1
!important
;
overflow
:
hidden
!important
;
}
.semi-chat-chatBox-action
{
column-gap
:
0
!important
;
}
}
\ No newline at end of file
web/src/pages/Playground/Playground.js
View file @
bb8923e7
...
@@ -19,12 +19,47 @@ import {
...
@@ -19,12 +19,47 @@ import {
Button
,
Button
,
MarkdownRender
,
MarkdownRender
,
Tag
,
Tag
,
Tabs
,
TabPane
,
Toast
,
Tooltip
,
Modal
,
}
from
'@douyinfe/semi-ui'
;
}
from
'@douyinfe/semi-ui'
;
import
{
SSE
}
from
'sse'
;
import
{
SSE
}
from
'sse'
;
import
{
IconSetting
,
IconSpin
,
IconChevronRight
,
IconChevronUp
}
from
'@douyinfe/semi-icons'
;
import
{
Settings
,
Sparkles
,
ChevronRight
,
ChevronUp
,
Brain
,
Zap
,
MessageSquare
,
SlidersHorizontal
,
Hash
,
Thermometer
,
Type
,
Users
,
Loader2
,
Target
,
Repeat
,
Ban
,
Shuffle
,
ToggleLeft
,
Code
,
Eye
,
EyeOff
,
FileText
,
Clock
,
Check
,
X
,
Copy
,
RefreshCw
,
Trash2
,
}
from
'lucide-react'
;
import
{
StyleContext
}
from
'../../context/Style/index.js'
;
import
{
StyleContext
}
from
'../../context/Style/index.js'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
renderGroupOption
,
truncateText
,
stringToColor
}
from
'../../helpers/render.js'
;
import
{
renderGroupOption
,
truncateText
,
stringToColor
}
from
'../../helpers/render.js'
;
import
{
IconSend
}
from
'@douyinfe/semi-icons'
;
let
id
=
4
;
let
id
=
4
;
function
getId
()
{
function
getId
()
{
...
@@ -89,6 +124,19 @@ const Playground = () => {
...
@@ -89,6 +124,19 @@ const Playground = () => {
group
:
''
,
group
:
''
,
max_tokens
:
0
,
max_tokens
:
0
,
temperature
:
0
,
temperature
:
0
,
top_p
:
1
,
frequency_penalty
:
0
,
presence_penalty
:
0
,
seed
:
null
,
stream
:
true
,
});
const
[
parameterEnabled
,
setParameterEnabled
]
=
useState
({
max_tokens
:
true
,
temperature
:
true
,
top_p
:
false
,
frequency_penalty
:
false
,
presence_penalty
:
false
,
seed
:
false
,
});
});
const
[
searchParams
,
setSearchParams
]
=
useSearchParams
();
const
[
searchParams
,
setSearchParams
]
=
useSearchParams
();
const
[
status
,
setStatus
]
=
useState
({});
const
[
status
,
setStatus
]
=
useState
({});
...
@@ -99,6 +147,13 @@ const Playground = () => {
...
@@ -99,6 +147,13 @@ const Playground = () => {
const
[
models
,
setModels
]
=
useState
([]);
const
[
models
,
setModels
]
=
useState
([]);
const
[
groups
,
setGroups
]
=
useState
([]);
const
[
groups
,
setGroups
]
=
useState
([]);
const
[
showSettings
,
setShowSettings
]
=
useState
(
true
);
const
[
showSettings
,
setShowSettings
]
=
useState
(
true
);
const
[
showDebugPanel
,
setShowDebugPanel
]
=
useState
(
true
);
const
[
debugData
,
setDebugData
]
=
useState
({
request
:
null
,
response
:
null
,
timestamp
:
null
});
const
[
activeDebugTab
,
setActiveDebugTab
]
=
useState
(
'request'
);
const
[
styleState
,
styleDispatch
]
=
useContext
(
StyleContext
);
const
[
styleState
,
styleDispatch
]
=
useContext
(
StyleContext
);
const
sseSourceRef
=
useRef
(
null
);
const
sseSourceRef
=
useRef
(
null
);
...
@@ -106,6 +161,13 @@ const Playground = () => {
...
@@ -106,6 +161,13 @@ const Playground = () => {
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
};
};
const
handleParameterToggle
=
(
paramName
)
=>
{
setParameterEnabled
(
prev
=>
({
...
prev
,
[
paramName
]:
!
prev
[
paramName
]
}));
};
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
searchParams
.
get
(
'expired'
))
{
if
(
searchParams
.
get
(
'expired'
))
{
showError
(
t
(
'未登录或登录已过期,请重新登录!'
));
showError
(
t
(
'未登录或登录已过期,请重新登录!'
));
...
@@ -128,7 +190,6 @@ const Playground = () => {
...
@@ -128,7 +190,6 @@ const Playground = () => {
value
:
model
,
value
:
model
,
}));
}));
setModels
(
localModelOptions
);
setModels
(
localModelOptions
);
// if default model is not in the list, set the first one as default
const
hasDefault
=
localModelOptions
.
some
(
option
=>
option
.
value
===
defaultModel
);
const
hasDefault
=
localModelOptions
.
some
(
option
=>
option
.
value
===
defaultModel
);
if
(
!
hasDefault
&&
localModelOptions
.
length
>
0
)
{
if
(
!
hasDefault
&&
localModelOptions
.
length
>
0
)
{
setInputs
((
inputs
)
=>
({
...
inputs
,
model
:
localModelOptions
[
0
].
value
}));
setInputs
((
inputs
)
=>
({
...
inputs
,
model
:
localModelOptions
[
0
].
value
}));
...
@@ -184,12 +245,6 @@ const Playground = () => {
...
@@ -184,12 +245,6 @@ const Playground = () => {
}
}
};
};
const
commonOuterStyle
=
{
border
:
'1px solid var(--semi-color-border)'
,
borderRadius
:
'16px'
,
margin
:
'0px 8px'
,
};
const
getSystemMessage
=
()
=>
{
const
getSystemMessage
=
()
=>
{
if
(
systemPrompt
!==
''
)
{
if
(
systemPrompt
!==
''
)
{
return
{
return
{
...
@@ -201,7 +256,152 @@ const Playground = () => {
...
@@ -201,7 +256,152 @@ const Playground = () => {
}
}
};
};
let
handleNonStreamRequest
=
async
(
payload
)
=>
{
setDebugData
(
prev
=>
({
...
prev
,
request
:
payload
,
timestamp
:
new
Date
().
toISOString
(),
response
:
null
}));
setActiveDebugTab
(
'request'
);
try
{
const
response
=
await
fetch
(
'/pg/chat/completions'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
'New-Api-User'
:
getUserIdFromLocalStorage
(),
},
body
:
JSON
.
stringify
(
payload
),
});
if
(
!
response
.
ok
)
{
let
errorBody
=
''
;
try
{
errorBody
=
await
response
.
text
();
}
catch
(
e
)
{
errorBody
=
'无法读取错误响应体'
;
}
const
errorInfo
=
{
error
:
'HTTP错误'
,
status
:
response
.
status
,
statusText
:
response
.
statusText
,
body
:
errorBody
,
timestamp
:
new
Date
().
toISOString
()
};
setDebugData
(
prev
=>
({
...
prev
,
response
:
JSON
.
stringify
(
errorInfo
,
null
,
2
)
}));
setActiveDebugTab
(
'response'
);
throw
new
Error
(
`HTTP error! status:
${
response
.
status
}
, body:
${
errorBody
}
`
);
}
const
data
=
await
response
.
json
();
setDebugData
(
prev
=>
({
...
prev
,
response
:
JSON
.
stringify
(
data
,
null
,
2
)
}));
setActiveDebugTab
(
'response'
);
if
(
data
.
choices
&&
data
.
choices
[
0
])
{
const
choice
=
data
.
choices
[
0
];
let
content
=
choice
.
message
?.
content
||
''
;
let
reasoningContent
=
choice
.
message
?.
reasoning_content
||
''
;
if
(
content
.
includes
(
'<think>'
))
{
const
thinkTagRegex
=
/<think>
([\s\S]
*
?)
<
\/
think>/g
;
let
thoughts
=
[];
let
replyParts
=
[];
let
lastIndex
=
0
;
let
match
;
thinkTagRegex
.
lastIndex
=
0
;
while
((
match
=
thinkTagRegex
.
exec
(
content
))
!==
null
)
{
replyParts
.
push
(
content
.
substring
(
lastIndex
,
match
.
index
));
thoughts
.
push
(
match
[
1
]);
lastIndex
=
match
.
index
+
match
[
0
].
length
;
}
replyParts
.
push
(
content
.
substring
(
lastIndex
));
content
=
replyParts
.
join
(
''
);
if
(
thoughts
.
length
>
0
)
{
if
(
reasoningContent
)
{
reasoningContent
+=
'\n\n---\n\n'
+
thoughts
.
join
(
'\n\n---\n\n'
);
}
else
{
reasoningContent
=
thoughts
.
join
(
'\n\n---\n\n'
);
}
}
}
content
=
content
.
replace
(
/<
\/?
think>/g
,
''
).
trim
();
setMessage
((
prevMessage
)
=>
{
const
newMessages
=
[...
prevMessage
];
const
lastMessage
=
newMessages
[
newMessages
.
length
-
1
];
if
(
lastMessage
&&
lastMessage
.
status
===
'loading'
)
{
newMessages
[
newMessages
.
length
-
1
]
=
{
...
lastMessage
,
content
:
content
,
reasoningContent
:
reasoningContent
,
status
:
'complete'
,
isReasoningExpanded
:
false
};
}
return
newMessages
;
});
}
}
catch
(
error
)
{
console
.
error
(
'Non-stream request error:'
,
error
);
const
errorInfo
=
{
error
:
'非流式请求错误'
,
message
:
error
.
message
,
timestamp
:
new
Date
().
toISOString
(),
stack
:
error
.
stack
};
if
(
error
.
message
.
includes
(
'HTTP error'
))
{
errorInfo
.
details
=
'服务器返回了错误状态码'
;
}
else
if
(
error
.
message
.
includes
(
'Failed to fetch'
))
{
errorInfo
.
details
=
'网络连接失败或服务器无响应'
;
}
setDebugData
(
prev
=>
({
...
prev
,
response
:
JSON
.
stringify
(
errorInfo
,
null
,
2
)
}));
setActiveDebugTab
(
'response'
);
setMessage
((
prevMessage
)
=>
{
const
newMessages
=
[...
prevMessage
];
const
lastMessage
=
newMessages
[
newMessages
.
length
-
1
];
if
(
lastMessage
&&
lastMessage
.
status
===
'loading'
)
{
newMessages
[
newMessages
.
length
-
1
]
=
{
...
lastMessage
,
content
:
t
(
'请求发生错误: '
)
+
error
.
message
,
status
:
'error'
,
isReasoningExpanded
:
false
};
}
return
newMessages
;
});
}
};
let
handleSSE
=
(
payload
)
=>
{
let
handleSSE
=
(
payload
)
=>
{
setDebugData
(
prev
=>
({
...
prev
,
request
:
payload
,
timestamp
:
new
Date
().
toISOString
(),
response
:
null
}));
setActiveDebugTab
(
'request'
);
let
source
=
new
SSE
(
'/pg/chat/completions'
,
{
let
source
=
new
SSE
(
'/pg/chat/completions'
,
{
headers
:
{
headers
:
{
'Content-Type'
:
'application/json'
,
'Content-Type'
:
'application/json'
,
...
@@ -211,19 +411,32 @@ const Playground = () => {
...
@@ -211,19 +411,32 @@ const Playground = () => {
payload
:
JSON
.
stringify
(
payload
),
payload
:
JSON
.
stringify
(
payload
),
});
});
// 保存 source 引用以便后续停止生成
sseSourceRef
.
current
=
source
;
sseSourceRef
.
current
=
source
;
let
responseData
=
''
;
let
hasReceivedFirstResponse
=
false
;
source
.
addEventListener
(
'message'
,
(
e
)
=>
{
source
.
addEventListener
(
'message'
,
(
e
)
=>
{
if
(
e
.
data
===
'[DONE]'
)
{
if
(
e
.
data
===
'[DONE]'
)
{
source
.
close
();
source
.
close
();
sseSourceRef
.
current
=
null
;
sseSourceRef
.
current
=
null
;
setDebugData
(
prev
=>
({
...
prev
,
response
:
responseData
}));
completeMessage
();
completeMessage
();
return
;
return
;
}
}
try
{
try
{
let
payload
=
JSON
.
parse
(
e
.
data
);
let
payload
=
JSON
.
parse
(
e
.
data
);
responseData
+=
e
.
data
+
'\n'
;
if
(
!
hasReceivedFirstResponse
)
{
setActiveDebugTab
(
'response'
);
hasReceivedFirstResponse
=
true
;
}
const
delta
=
payload
.
choices
?.[
0
]?.
delta
;
const
delta
=
payload
.
choices
?.[
0
]?.
delta
;
if
(
delta
)
{
if
(
delta
)
{
if
(
delta
.
reasoning_content
)
{
if
(
delta
.
reasoning_content
)
{
...
@@ -235,6 +448,14 @@ const Playground = () => {
...
@@ -235,6 +448,14 @@ const Playground = () => {
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'Failed to parse SSE message:'
,
error
);
console
.
error
(
'Failed to parse SSE message:'
,
error
);
const
errorInfo
=
`解析错误:
${
error
.
message
}
`
;
setDebugData
(
prev
=>
({
...
prev
,
response
:
responseData
+
`\n\nError:
${
errorInfo
}
`
}));
setActiveDebugTab
(
'response'
);
streamMessageUpdate
(
t
(
'解析响应数据时发生错误'
),
'content'
);
streamMessageUpdate
(
t
(
'解析响应数据时发生错误'
),
'content'
);
completeMessage
(
'error'
);
completeMessage
(
'error'
);
}
}
...
@@ -243,6 +464,21 @@ const Playground = () => {
...
@@ -243,6 +464,21 @@ const Playground = () => {
source
.
addEventListener
(
'error'
,
(
e
)
=>
{
source
.
addEventListener
(
'error'
,
(
e
)
=>
{
console
.
error
(
'SSE Error:'
,
e
);
console
.
error
(
'SSE Error:'
,
e
);
const
errorMessage
=
e
.
data
||
t
(
'请求发生错误'
);
const
errorMessage
=
e
.
data
||
t
(
'请求发生错误'
);
const
errorInfo
=
{
error
:
'SSE连接错误'
,
message
:
errorMessage
,
status
:
source
.
status
,
readyState
:
source
.
readyState
,
timestamp
:
new
Date
().
toISOString
()
};
setDebugData
(
prev
=>
({
...
prev
,
response
:
responseData
+
'\n\nSSE Error:\n'
+
JSON
.
stringify
(
errorInfo
,
null
,
2
)
}));
setActiveDebugTab
(
'response'
);
streamMessageUpdate
(
errorMessage
,
'content'
);
streamMessageUpdate
(
errorMessage
,
'content'
);
completeMessage
(
'error'
);
completeMessage
(
'error'
);
sseSourceRef
.
current
=
null
;
sseSourceRef
.
current
=
null
;
...
@@ -252,6 +488,19 @@ const Playground = () => {
...
@@ -252,6 +488,19 @@ const Playground = () => {
source
.
addEventListener
(
'readystatechange'
,
(
e
)
=>
{
source
.
addEventListener
(
'readystatechange'
,
(
e
)
=>
{
if
(
e
.
readyState
>=
2
)
{
if
(
e
.
readyState
>=
2
)
{
if
(
source
.
status
!==
undefined
&&
source
.
status
!==
200
)
{
if
(
source
.
status
!==
undefined
&&
source
.
status
!==
200
)
{
const
errorInfo
=
{
error
:
'HTTP状态错误'
,
status
:
source
.
status
,
readyState
:
source
.
readyState
,
timestamp
:
new
Date
().
toISOString
()
};
setDebugData
(
prev
=>
({
...
prev
,
response
:
responseData
+
'\n\nHTTP Error:\n'
+
JSON
.
stringify
(
errorInfo
,
null
,
2
)
}));
setActiveDebugTab
(
'response'
);
source
.
close
();
source
.
close
();
streamMessageUpdate
(
t
(
'连接已断开'
),
'content'
);
streamMessageUpdate
(
t
(
'连接已断开'
),
'content'
);
completeMessage
(
'error'
);
completeMessage
(
'error'
);
...
@@ -263,6 +512,18 @@ const Playground = () => {
...
@@ -263,6 +512,18 @@ const Playground = () => {
source
.
stream
();
source
.
stream
();
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'Failed to start SSE stream:'
,
error
);
console
.
error
(
'Failed to start SSE stream:'
,
error
);
const
errorInfo
=
{
error
:
'启动SSE流失败'
,
message
:
error
.
message
,
timestamp
:
new
Date
().
toISOString
()
};
setDebugData
(
prev
=>
({
...
prev
,
response
:
'Stream启动失败:\n'
+
JSON
.
stringify
(
errorInfo
,
null
,
2
)
}));
setActiveDebugTab
(
'response'
);
streamMessageUpdate
(
t
(
'建立连接时发生错误'
),
'content'
);
streamMessageUpdate
(
t
(
'建立连接时发生错误'
),
'content'
);
completeMessage
(
'error'
);
completeMessage
(
'error'
);
}
}
...
@@ -293,17 +554,43 @@ const Playground = () => {
...
@@ -293,17 +554,43 @@ const Playground = () => {
if
(
systemMessage
)
{
if
(
systemMessage
)
{
messages
.
unshift
(
systemMessage
);
messages
.
unshift
(
systemMessage
);
}
}
return
{
const
payload
=
{
messages
:
messages
,
messages
:
messages
,
stream
:
true
,
stream
:
inputs
.
stream
,
model
:
inputs
.
model
,
model
:
inputs
.
model
,
group
:
inputs
.
group
,
group
:
inputs
.
group
,
max_tokens
:
parseInt
(
inputs
.
max_tokens
),
temperature
:
inputs
.
temperature
,
};
};
if
(
parameterEnabled
.
max_tokens
&&
inputs
.
max_tokens
>
0
)
{
payload
.
max_tokens
=
parseInt
(
inputs
.
max_tokens
);
}
if
(
parameterEnabled
.
temperature
)
{
payload
.
temperature
=
inputs
.
temperature
;
}
if
(
parameterEnabled
.
top_p
)
{
payload
.
top_p
=
inputs
.
top_p
;
}
if
(
parameterEnabled
.
frequency_penalty
)
{
payload
.
frequency_penalty
=
inputs
.
frequency_penalty
;
}
if
(
parameterEnabled
.
presence_penalty
)
{
payload
.
presence_penalty
=
inputs
.
presence_penalty
;
}
if
(
parameterEnabled
.
seed
&&
inputs
.
seed
!==
null
&&
inputs
.
seed
!==
''
)
{
payload
.
seed
=
parseInt
(
inputs
.
seed
);
}
return
payload
;
};
};
handleSSE
(
getPayload
());
const
payload
=
getPayload
();
if
(
inputs
.
stream
)
{
handleSSE
(
payload
);
}
else
{
handleNonStreamRequest
(
payload
);
}
newMessage
.
push
({
newMessage
.
push
({
role
:
'assistant'
,
role
:
'assistant'
,
content
:
''
,
content
:
''
,
...
@@ -334,7 +621,6 @@ const Playground = () => {
...
@@ -334,7 +621,6 @@ const Playground = () => {
const
lastMessage
=
prevMessage
[
prevMessage
.
length
-
1
];
const
lastMessage
=
prevMessage
[
prevMessage
.
length
-
1
];
let
newMessage
=
{
...
lastMessage
};
let
newMessage
=
{
...
lastMessage
};
// 如果消息已经是错误状态,保持错误状态
if
(
lastMessage
.
status
===
'error'
)
{
if
(
lastMessage
.
status
===
'error'
)
{
return
prevMessage
;
return
prevMessage
;
}
}
...
@@ -347,10 +633,23 @@ const Playground = () => {
...
@@ -347,10 +633,23 @@ const Playground = () => {
status
:
'incomplete'
,
status
:
'incomplete'
,
};
};
}
else
if
(
type
===
'content'
)
{
}
else
if
(
type
===
'content'
)
{
const
shouldCollapseReasoning
=
!
lastMessage
.
content
&&
lastMessage
.
reasoningContent
;
const
newContent
=
(
lastMessage
.
content
||
''
)
+
textChunk
;
let
shouldCollapseFromThinkTag
=
false
;
if
(
lastMessage
.
isReasoningExpanded
&&
newContent
.
includes
(
'</think>'
))
{
const
thinkMatches
=
newContent
.
match
(
/<think>/g
);
const
thinkCloseMatches
=
newContent
.
match
(
/<
\/
think>/g
);
if
(
thinkMatches
&&
thinkCloseMatches
&&
thinkCloseMatches
.
length
>=
thinkMatches
.
length
)
{
shouldCollapseFromThinkTag
=
true
;
}
}
newMessage
=
{
newMessage
=
{
...
newMessage
,
...
newMessage
,
content
:
(
lastMessage
.
content
||
''
)
+
textChunk
,
content
:
newContent
,
status
:
'incomplete'
,
status
:
'incomplete'
,
isReasoningExpanded
:
(
shouldCollapseReasoning
||
shouldCollapseFromThinkTag
)
?
false
:
lastMessage
.
isReasoningExpanded
,
};
};
}
}
}
}
...
@@ -358,6 +657,143 @@ const Playground = () => {
...
@@ -358,6 +657,143 @@ const Playground = () => {
});
});
},
[
setMessage
]);
},
[
setMessage
]);
const
handleMessageCopy
=
useCallback
((
message
)
=>
{
if
(
!
message
.
content
)
return
;
if
(
navigator
.
clipboard
&&
navigator
.
clipboard
.
writeText
)
{
navigator
.
clipboard
.
writeText
(
message
.
content
).
then
(()
=>
{
Toast
.
success
({
content
:
t
(
'消息已复制到剪贴板'
),
duration
:
2
,
});
}).
catch
(
err
=>
{
console
.
error
(
'Clipboard API 复制失败:'
,
err
);
fallbackCopyToClipboard
(
message
.
content
);
});
}
else
{
fallbackCopyToClipboard
(
message
.
content
);
}
},
[
t
]);
const
fallbackCopyToClipboard
=
useCallback
((
text
)
=>
{
try
{
if
(
!
document
.
execCommand
)
{
throw
new
Error
(
'execCommand not supported'
);
}
const
textArea
=
document
.
createElement
(
'textarea'
);
textArea
.
value
=
text
;
textArea
.
style
.
position
=
'fixed'
;
textArea
.
style
.
top
=
'-9999px'
;
textArea
.
style
.
left
=
'-9999px'
;
textArea
.
style
.
opacity
=
'0'
;
textArea
.
style
.
pointerEvents
=
'none'
;
textArea
.
style
.
zIndex
=
'-1'
;
textArea
.
setAttribute
(
'readonly'
,
''
);
document
.
body
.
appendChild
(
textArea
);
if
(
textArea
.
select
)
{
textArea
.
select
();
}
if
(
textArea
.
setSelectionRange
)
{
textArea
.
setSelectionRange
(
0
,
text
.
length
);
}
const
successful
=
document
.
execCommand
(
'copy'
);
document
.
body
.
removeChild
(
textArea
);
if
(
successful
)
{
Toast
.
success
({
content
:
t
(
'消息已复制到剪贴板'
),
duration
:
2
,
});
}
else
{
throw
new
Error
(
'execCommand copy failed'
);
}
}
catch
(
err
)
{
console
.
error
(
'回退复制方案也失败:'
,
err
);
let
errorMessage
=
t
(
'复制失败,请手动选择文本复制'
);
if
(
window
.
location
.
protocol
===
'http:'
&&
window
.
location
.
hostname
!==
'localhost'
)
{
errorMessage
=
t
(
'复制功能需要 HTTPS 环境,请手动复制'
);
}
else
if
(
!
navigator
.
clipboard
&&
!
document
.
execCommand
)
{
errorMessage
=
t
(
'浏览器不支持复制功能,请手动复制'
);
}
Toast
.
error
({
content
:
errorMessage
,
duration
:
4
,
});
}
},
[
t
]);
const
handleMessageReset
=
useCallback
((
targetMessage
)
=>
{
setMessage
(
prevMessages
=>
{
const
messageIndex
=
prevMessages
.
findIndex
(
msg
=>
msg
.
id
===
targetMessage
.
id
);
if
(
messageIndex
===
-
1
)
return
prevMessages
;
if
(
targetMessage
.
role
===
'user'
)
{
const
newMessages
=
prevMessages
.
slice
(
0
,
messageIndex
);
setTimeout
(()
=>
{
onMessageSend
(
targetMessage
.
content
);
},
100
);
return
newMessages
;
}
else
if
(
targetMessage
.
role
===
'assistant'
)
{
let
userMessageIndex
=
messageIndex
-
1
;
while
(
userMessageIndex
>=
0
&&
prevMessages
[
userMessageIndex
].
role
!==
'user'
)
{
userMessageIndex
--
;
}
if
(
userMessageIndex
>=
0
)
{
const
userMessage
=
prevMessages
[
userMessageIndex
];
const
newMessages
=
prevMessages
.
slice
(
0
,
userMessageIndex
);
setTimeout
(()
=>
{
onMessageSend
(
userMessage
.
content
);
},
100
);
return
newMessages
;
}
}
return
prevMessages
;
});
},
[
onMessageSend
]);
const
handleMessageDelete
=
useCallback
((
targetMessage
)
=>
{
Modal
.
confirm
({
title
:
t
(
'确认删除'
),
content
:
t
(
'确定要删除这条消息吗?'
),
okText
:
t
(
'确定'
),
cancelText
:
t
(
'取消'
),
okButtonProps
:
{
type
:
'danger'
,
},
onOk
:
()
=>
{
setMessage
(
prevMessages
=>
{
const
messageIndex
=
prevMessages
.
findIndex
(
msg
=>
msg
.
id
===
targetMessage
.
id
);
if
(
messageIndex
===
-
1
)
return
prevMessages
;
if
(
targetMessage
.
role
===
'user'
&&
messageIndex
<
prevMessages
.
length
-
1
)
{
const
nextMessage
=
prevMessages
[
messageIndex
+
1
];
if
(
nextMessage
.
role
===
'assistant'
)
{
Toast
.
success
({
content
:
t
(
'已删除消息及其回复'
),
duration
:
2
,
});
return
prevMessages
.
filter
((
_
,
index
)
=>
index
!==
messageIndex
&&
index
!==
messageIndex
+
1
);
}
}
Toast
.
success
({
content
:
t
(
'消息已删除'
),
duration
:
2
,
});
return
prevMessages
.
filter
(
msg
=>
msg
.
id
!==
targetMessage
.
id
);
});
},
});
},
[
setMessage
,
t
]);
const
onStopGenerator
=
useCallback
(()
=>
{
const
onStopGenerator
=
useCallback
(()
=>
{
if
(
sseSourceRef
.
current
)
{
if
(
sseSourceRef
.
current
)
{
sseSourceRef
.
current
.
close
();
sseSourceRef
.
current
.
close
();
...
@@ -365,37 +801,58 @@ const Playground = () => {
...
@@ -365,37 +801,58 @@ const Playground = () => {
setMessage
((
prevMessage
)
=>
{
setMessage
((
prevMessage
)
=>
{
const
lastMessage
=
prevMessage
[
prevMessage
.
length
-
1
];
const
lastMessage
=
prevMessage
[
prevMessage
.
length
-
1
];
if
(
lastMessage
.
status
===
'loading'
||
lastMessage
.
status
===
'incomplete'
)
{
if
(
lastMessage
.
status
===
'loading'
||
lastMessage
.
status
===
'incomplete'
)
{
let
content
=
lastMessage
.
content
||
''
;
let
c
urrentC
ontent
=
lastMessage
.
content
||
''
;
let
r
easoningContent
=
lastMessage
.
reasoningContent
||
''
;
let
currentR
easoningContent
=
lastMessage
.
reasoningContent
||
''
;
// 处理 <think> 标签格式的思维链
if
(
currentContent
.
includes
(
'<think>'
))
{
if
(
content
.
includes
(
'<think>'
))
{
const
thinkTagRegex
=
/<think>
([\s\S]
*
?)
<
\/
think>/g
;
const
thinkTagRegex
=
/<think>
([\s\S]
*
?)(?:
<
\/
think>|$
)
/g
;
let
match
;
let
thoughts
=
[];
let
thoughts
FromPairedTags
=
[];
let
replyParts
=
[];
let
replyParts
=
[];
let
lastIndex
=
0
;
let
lastIndex
=
0
;
let
match
;
while
((
match
=
thinkTagRegex
.
exec
(
content
))
!==
null
)
{
while
((
match
=
thinkTagRegex
.
exec
(
c
urrentC
ontent
))
!==
null
)
{
replyParts
.
push
(
content
.
substring
(
lastIndex
,
match
.
index
));
replyParts
.
push
(
c
urrentC
ontent
.
substring
(
lastIndex
,
match
.
index
));
thoughts
.
push
(
match
[
1
]);
thoughts
FromPairedTags
.
push
(
match
[
1
]);
lastIndex
=
match
.
index
+
match
[
0
].
length
;
lastIndex
=
match
.
index
+
match
[
0
].
length
;
}
}
replyParts
.
push
(
content
.
substring
(
lastIndex
));
replyParts
.
push
(
c
urrentC
ontent
.
substring
(
lastIndex
));
// 更新内容和思维链
if
(
thoughtsFromPairedTags
.
length
>
0
)
{
content
=
replyParts
.
join
(
''
).
trim
();
const
pairedThoughtsStr
=
thoughtsFromPairedTags
.
join
(
'\n\n---\n\n'
);
if
(
thoughts
.
length
>
0
)
{
if
(
currentReasoningContent
)
{
reasoningContent
=
thoughts
.
join
(
'\n\n---\n\n'
);
currentReasoningContent
+=
'\n\n---\n\n'
+
pairedThoughtsStr
;
}
else
{
currentReasoningContent
=
pairedThoughtsStr
;
}
}
}
currentContent
=
replyParts
.
join
(
''
);
}
}
const
lastOpenThinkIndex
=
currentContent
.
lastIndexOf
(
'<think>'
);
if
(
lastOpenThinkIndex
!==
-
1
)
{
const
fragmentAfterLastOpen
=
currentContent
.
substring
(
lastOpenThinkIndex
);
if
(
!
fragmentAfterLastOpen
.
includes
(
'</think>'
))
{
const
unclosedThought
=
fragmentAfterLastOpen
.
substring
(
'<think>'
.
length
).
trim
();
if
(
unclosedThought
)
{
if
(
currentReasoningContent
)
{
currentReasoningContent
+=
'\n\n---\n\n'
+
unclosedThought
;
}
else
{
currentReasoningContent
=
unclosedThought
;
}
}
currentContent
=
currentContent
.
substring
(
0
,
lastOpenThinkIndex
);
}
}
currentContent
=
currentContent
.
replace
(
/<
\/?
think>/g
,
''
).
trim
();
return
[...
prevMessage
.
slice
(
0
,
-
1
),
{
return
[...
prevMessage
.
slice
(
0
,
-
1
),
{
...
lastMessage
,
...
lastMessage
,
status
:
'complete'
,
status
:
'complete'
,
reasoningContent
:
reasoningContent
,
reasoningContent
:
currentReasoningContent
||
null
,
content
:
content
,
content
:
c
urrentC
ontent
,
isReasoningExpanded
:
false
// 停止时折叠思维链面板
isReasoningExpanded
:
false
}];
}];
}
}
return
prevMessage
;
return
prevMessage
;
...
@@ -403,11 +860,26 @@ const Playground = () => {
...
@@ -403,11 +860,26 @@ const Playground = () => {
}
}
},
[
setMessage
]);
},
[
setMessage
]);
const
DebugToggle
=
()
=>
{
return
(
<
Button
icon
=
{
showDebugPanel
?
<
EyeOff
size
=
{
14
}
/> : <Eye size=
{14}
/
>
}
onClick
=
{()
=>
setShowDebugPanel
(
!
showDebugPanel
)}
theme
=
"borderless"
type
=
"tertiary"
size
=
"small"
className
=
"!rounded-lg !text-gray-600 hover:!text-purple-600 hover:!bg-purple-50"
>
{
showDebugPanel
?
t
(
'隐藏调试'
)
:
t
(
'显示调试'
)}
<
/Button
>
);
};
const
SettingsToggle
=
()
=>
{
const
SettingsToggle
=
()
=>
{
if
(
!
styleState
.
isMobile
)
return
null
;
if
(
!
styleState
.
isMobile
)
return
null
;
return
(
return
(
<
Button
<
Button
icon
=
{
<
IconSetting
/>
}
icon
=
{
<
Settings
size
=
{
16
}
/>
}
style
=
{{
style
=
{{
position
:
'absolute'
,
position
:
'absolute'
,
left
:
showSettings
?
-
10
:
-
20
,
left
:
showSettings
?
-
10
:
-
20
,
...
@@ -433,20 +905,24 @@ const Playground = () => {
...
@@ -433,20 +905,24 @@ const Playground = () => {
detailProps
;
detailProps
;
return
(
return
(
<
div
<
div
className
=
"p-4"
>
style
=
{{
<
div
margin
:
'8px 16px'
,
className
=
"flex items-end gap-3 p-4 bg-gray-50 rounded-2xl shadow-sm hover:shadow-md transition-shadow"
display
:
'flex'
,
style
=
{{
border
:
'1px solid var(--semi-color-border)'
}}
flexDirection
:
'row'
,
onClick
=
{
onClick
}
alignItems
:
'flex-end'
,
>
borderRadius
:
16
,
<
div
className
=
"flex-1"
>
padding
:
10
,
{
inputNode
}
border
:
'1px solid var(--semi-color-border)'
,
<
/div
>
}}
<
Button
onClick
=
{
onClick
}
theme
=
"solid"
>
type
=
"primary"
{
inputNode
}
className
=
"!rounded-lg !bg-purple-500 hover:!bg-purple-600 flex-shrink-0"
{
sendNode
}
icon
=
{
<
IconSend
/>
}
>
{
t
(
'发送'
)}
<
/Button
>
<
/div
>
<
/div
>
<
/div
>
);
);
}
}
...
@@ -455,17 +931,66 @@ const Playground = () => {
...
@@ -455,17 +931,66 @@ const Playground = () => {
return
<
CustomInputRender
{...
props
}
/>
;
return
<
CustomInputRender
{...
props
}
/>
;
},
[]);
},
[]);
const
renderChatBoxAction
=
useCallback
((
props
)
=>
{
const
{
message
}
=
props
;
const
isLoading
=
message
.
status
===
'loading'
||
message
.
status
===
'incomplete'
;
return
(
<
div
className
=
"flex items-center gap-0.5"
>
{
!
isLoading
&&
(
<
Tooltip
content
=
{
t
(
'重试'
)}
position
=
"top"
>
<
Button
theme
=
"borderless"
type
=
"tertiary"
size
=
"small"
icon
=
{
<
RefreshCw
size
=
{
14
}
/>
}
onClick
=
{()
=>
handleMessageReset
(
message
)}
className
=
"!rounded-md !text-gray-400 hover:!text-blue-600 hover:!bg-blue-50 !w-7 !h-7 !p-0 transition-all"
aria
-
label
=
{
t
(
'重试'
)}
/
>
<
/Tooltip
>
)}
{
message
.
content
&&
(
<
Tooltip
content
=
{
t
(
'复制'
)}
position
=
"top"
>
<
Button
theme
=
"borderless"
type
=
"tertiary"
size
=
"small"
icon
=
{
<
Copy
size
=
{
14
}
/>
}
onClick
=
{()
=>
handleMessageCopy
(
message
)}
className
=
"!rounded-md !text-gray-400 hover:!text-green-600 hover:!bg-green-50 !w-7 !h-7 !p-0 transition-all"
aria
-
label
=
{
t
(
'复制'
)}
/
>
<
/Tooltip
>
)}
{
!
isLoading
&&
(
<
Tooltip
content
=
{
t
(
'删除'
)}
position
=
"top"
>
<
Button
theme
=
"borderless"
type
=
"tertiary"
size
=
"small"
icon
=
{
<
Trash2
size
=
{
14
}
/>
}
onClick
=
{()
=>
handleMessageDelete
(
message
)}
className
=
"!rounded-md !text-gray-400 hover:!text-red-600 hover:!bg-red-50 !w-7 !h-7 !p-0 transition-all"
aria
-
label
=
{
t
(
'删除'
)}
/
>
<
/Tooltip
>
)}
<
/div
>
);
},
[
handleMessageReset
,
handleMessageCopy
,
handleMessageDelete
,
t
]);
const
renderCustomChatContent
=
useCallback
(
const
renderCustomChatContent
=
useCallback
(
({
message
,
className
})
=>
{
({
message
,
className
})
=>
{
if
(
message
.
status
===
'error'
)
{
if
(
message
.
status
===
'error'
)
{
return
(
return
(
<
div
className
=
{
className
}
style
=
{{
<
div
className
=
{
`
${
className
}
flex items-center p-4 bg-red-50 rounded-xl`
}
>
display
:
'flex'
,
<
Typography
.
Text
type
=
"danger"
className
=
"text-sm"
>
alignItems
:
'center'
,
{
message
.
content
||
t
(
'请求发生错误'
)}
padding
:
'12px'
,
<
/Typography.Text
>
color
:
'var(--semi-color-danger)'
}}
>
<
Typography
.
Text
type
=
"danger"
>
{
message
.
content
||
t
(
'请求发生错误'
)}
<
/Typography.Text
>
<
/div
>
<
/div
>
);
);
}
}
...
@@ -486,56 +1011,62 @@ const Playground = () => {
...
@@ -486,56 +1011,62 @@ const Playground = () => {
let
thinkingSource
=
null
;
let
thinkingSource
=
null
;
if
(
message
.
role
===
'assistant'
)
{
if
(
message
.
role
===
'assistant'
)
{
let
baseContentForDisplay
=
message
.
content
||
""
;
let
combinedThinkingContent
=
""
;
if
(
message
.
reasoningContent
)
{
if
(
message
.
reasoningContent
)
{
c
urrentExtract
edThinkingContent
=
message
.
reasoningContent
;
c
ombin
edThinkingContent
=
message
.
reasoningContent
;
thinkingSource
=
'reasoningContent'
;
thinkingSource
=
'reasoningContent'
;
}
else
if
(
message
.
content
&&
message
.
content
.
includes
(
'<think'
))
{
}
const
fullContent
=
message
.
content
;
let
thoughts
=
[];
if
(
baseContentForDisplay
.
includes
(
'<think>'
))
{
let
replyParts
=
[];
let
lastIndex
=
0
;
const
thinkTagRegex
=
/<think>
([\s\S]
*
?)
<
\/
think>/g
;
const
thinkTagRegex
=
/<think>
([\s\S]
*
?)
<
\/
think>/g
;
let
match
;
let
match
;
let
thoughtsFromPairedTags
=
[];
let
replyParts
=
[];
let
lastIndex
=
0
;
thinkTagRegex
.
lastIndex
=
0
;
while
((
match
=
thinkTagRegex
.
exec
(
baseContentForDisplay
))
!==
null
)
{
while
((
match
=
thinkTagRegex
.
exec
(
fullContent
))
!==
null
)
{
replyParts
.
push
(
baseContentForDisplay
.
substring
(
lastIndex
,
match
.
index
));
replyParts
.
push
(
fullContent
.
substring
(
lastIndex
,
match
.
index
));
thoughtsFromPairedTags
.
push
(
match
[
1
]);
thoughts
.
push
(
match
[
1
]);
lastIndex
=
match
.
index
+
match
[
0
].
length
;
lastIndex
=
match
.
index
+
match
[
0
].
length
;
}
}
replyParts
.
push
(
fullContent
.
substring
(
lastIndex
));
replyParts
.
push
(
baseContentForDisplay
.
substring
(
lastIndex
));
currentDisplayableFinalContent
=
replyParts
.
join
(
''
).
trim
();
if
(
thoughtsFromPairedTags
.
length
>
0
)
{
const
pairedThoughtsStr
=
thoughtsFromPairedTags
.
join
(
'\n\n---\n\n'
);
if
(
thoughts
.
length
>
0
)
{
if
(
combinedThinkingContent
)
{
currentExtractedThinkingContent
=
thoughts
.
join
(
'\n\n---\n\n'
);
combinedThinkingContent
+=
'\n\n---\n\n'
+
pairedThoughtsStr
;
thinkingSource
=
'<think> tags'
;
}
else
{
combinedThinkingContent
=
pairedThoughtsStr
;
}
thinkingSource
=
thinkingSource
?
thinkingSource
+
' & <think> tags'
:
'<think> tags'
;
}
}
if
(
isThinkingStatus
&&
currentDisplayableFinalContent
.
includes
(
'<think'
))
{
baseContentForDisplay
=
replyParts
.
join
(
''
);
const
lastOpenThinkIndex
=
currentDisplayableFinalContent
.
lastIndexOf
(
'<think>'
);
}
if
(
lastOpenThinkIndex
!==
-
1
)
{
const
fragmentAfterLastOpen
=
currentDisplayableFinalContent
.
substring
(
lastOpenThinkIndex
);
if
(
isThinkingStatus
)
{
if
(
!
fragmentAfterLastOpen
.
substring
(
"<think>"
.
length
).
includes
(
'</think>'
))
{
const
lastOpenThinkIndex
=
baseContentForDisplay
.
lastIndexOf
(
'<think>'
);
const
unclosedThought
=
fragmentAfterLastOpen
.
substring
(
"<think>"
.
length
);
if
(
lastOpenThinkIndex
!==
-
1
)
{
if
(
currentExtractedThinkingContent
)
{
const
fragmentAfterLastOpen
=
baseContentForDisplay
.
substring
(
lastOpenThinkIndex
);
currentExtractedThinkingContent
+=
(
currentExtractedThinkingContent
?
'\n\n---\n\n'
:
''
)
+
unclosedThought
;
if
(
!
fragmentAfterLastOpen
.
includes
(
'</think>'
))
{
const
unclosedThought
=
fragmentAfterLastOpen
.
substring
(
'<think>'
.
length
).
trim
();
if
(
unclosedThought
)
{
if
(
combinedThinkingContent
)
{
combinedThinkingContent
+=
'\n\n---\n\n'
+
unclosedThought
;
}
else
{
}
else
{
c
urrentExtract
edThinkingContent
=
unclosedThought
;
c
ombin
edThinkingContent
=
unclosedThought
;
}
}
if
(
!
thinkingSource
&&
unclosedThought
)
thinkingSource
=
'<think> tags (streaming)'
;
thinkingSource
=
thinkingSource
?
thinkingSource
+
' + streaming <think>'
:
'streaming <think>'
;
currentDisplayableFinalContent
=
currentDisplayableFinalContent
.
substring
(
0
,
lastOpenThinkIndex
).
trim
();
}
}
baseContentForDisplay
=
baseContentForDisplay
.
substring
(
0
,
lastOpenThinkIndex
);
}
}
}
}
}
}
if
(
typeof
currentDisplayableFinalContent
===
'string'
&&
currentDisplayableFinalContent
.
trim
().
startsWith
(
"<think>"
))
{
currentExtractedThinkingContent
=
combinedThinkingContent
||
null
;
const
startsWithCompleteThinkTagRegex
=
/^<think>
[\s\S]
*
?
<
\/
think>/
;
currentDisplayableFinalContent
=
baseContentForDisplay
.
replace
(
/<
\/?
think>/g
,
''
).
trim
();
if
(
!
startsWithCompleteThinkTagRegex
.
test
(
currentDisplayableFinalContent
.
trim
()))
{
currentDisplayableFinalContent
=
""
;
}
}
}
}
const
headerText
=
isThinkingStatus
?
t
(
'思考中...'
)
:
t
(
'思考过程'
);
const
headerText
=
isThinkingStatus
?
t
(
'思考中...'
)
:
t
(
'思考过程'
);
...
@@ -547,9 +1078,18 @@ const Playground = () => {
...
@@ -547,9 +1078,18 @@ const Playground = () => {
!
finalExtractedThinkingContent
&&
!
finalExtractedThinkingContent
&&
(
!
finalDisplayableFinalContent
||
finalDisplayableFinalContent
.
trim
()
===
''
))
{
(
!
finalDisplayableFinalContent
||
finalDisplayableFinalContent
.
trim
()
===
''
))
{
return
(
return
(
<
div
className
=
{
className
}
style
=
{{
display
:
'flex'
,
alignItems
:
'center'
,
padding
:
'12px'
}}
>
<
div
className
=
{
`
${
className
}
flex items-center gap-4 p-6 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-2xl`
}
>
<
IconSpin
spin
/>
<
div
className
=
"w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg"
>
<
Typography
.
Text
type
=
"secondary"
style
=
{{
marginLeft
:
'8px'
}}
>
{
t
(
'正在思考...'
)}
<
/Typography.Text
>
<
Loader2
className
=
"animate-spin text-white"
size
=
{
20
}
/
>
<
/div
>
<
div
className
=
"flex flex-col"
>
<
Typography
.
Text
strong
className
=
"text-gray-800 text-base"
>
{
t
(
'正在思考...'
)}
<
/Typography.Text
>
<
Typography
.
Text
className
=
"text-gray-500 text-sm"
>
AI
正在分析您的问题
<
/Typography.Text
>
<
/div
>
<
/div
>
<
/div
>
);
);
}
}
...
@@ -557,56 +1097,66 @@ const Playground = () => {
...
@@ -557,56 +1097,66 @@ const Playground = () => {
return
(
return
(
<
div
className
=
{
className
}
>
<
div
className
=
{
className
}
>
{
message
.
role
===
'assistant'
&&
finalExtractedThinkingContent
&&
(
{
message
.
role
===
'assistant'
&&
finalExtractedThinkingContent
&&
(
<
div
style
=
{{
<
div
className
=
"bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 rounded-2xl mb-4 overflow-hidden shadow-sm backdrop-blur-sm"
>
background
:
'var(--semi-color-tertiary-light-hover)'
,
borderRadius
:
'16px'
,
marginBottom
:
'8px'
,
overflow
:
'hidden'
,
}}
>
<
div
<
div
style
=
{{
className
=
"flex items-center justify-between p-5 cursor-pointer hover:bg-gradient-to-r hover:from-white/40 hover:to-purple-50/60 transition-all"
display
:
'flex'
,
alignItems
:
'center'
,
justifyContent
:
'space-between'
,
padding
:
'8px 12px'
,
cursor
:
'pointer'
,
height
:
'auto'
,
}}
onClick
=
{()
=>
toggleReasoningExpansion
(
message
.
id
)}
onClick
=
{()
=>
toggleReasoningExpansion
(
message
.
id
)}
>
>
<
div
style
=
{{
display
:
'flex'
,
alignItems
:
'center'
}}
>
<
div
className
=
"flex items-center gap-4"
>
<
Typography
.
Text
strong
=
{
message
.
isReasoningExpanded
}
style
=
{{
fontSize
:
'13px'
,
color
:
'var(--semi-color-text-1)'
}}
>
{
headerText
}
<
/Typography.Text
>
<
div
className
=
"w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg"
>
{
thinkingSource
&&
(
<
Brain
className
=
"text-white"
size
=
{
16
}
/
>
<
Tag
size
=
"small"
color
=
'green'
shape
=
"circle"
style
=
{{
marginLeft
:
'8px'
}}
>
<
/div
>
{
thinkingSource
}
<
div
className
=
"flex flex-col"
>
<
/Tag
>
<
Typography
.
Text
strong
className
=
"text-gray-800 text-base"
>
)}
{
headerText
}
<
/Typography.Text
>
{
thinkingSource
&&
(
<
Typography
.
Text
className
=
"text-gray-500 text-xs mt-0.5"
>
来源
:
{
thinkingSource
}
<
/Typography.Text
>
)}
<
/div
>
<
/div
>
<
/div
>
<
div
>
<
div
className
=
"flex items-center gap-3"
>
{
isThinkingStatus
&&
<
IconSpin
spin
/>
}
{
isThinkingStatus
&&
(
{
!
isThinkingStatus
&&
(
message
.
isReasoningExpanded
?
<
IconChevronUp
size
=
"small"
/>
:
<
IconChevronRight
size
=
"small"
/>
)}
<
div
className
=
"flex items-center gap-2"
>
<
Loader2
className
=
"animate-spin text-purple-500"
size
=
{
18
}
/
>
<
Typography
.
Text
className
=
"text-purple-600 text-sm font-medium"
>
思考中
<
/Typography.Text
>
<
/div
>
)}
{
!
isThinkingStatus
&&
(
<
div
className
=
"w-6 h-6 rounded-full bg-purple-100 flex items-center justify-center"
>
{
message
.
isReasoningExpanded
?
<
ChevronUp
size
=
{
16
}
className
=
"text-purple-600"
/>
:
<
ChevronRight
size
=
{
16
}
className
=
"text-purple-600"
/>
}
<
/div
>
)}
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
div
<
div
style
=
{{
className
=
{
`transition-all duration-500 ease-out
${
message
.
isReasoningExpanded
?
'max-h-96 opacity-100'
:
'max-h-0 opacity-0'
maxHeight
:
message
.
isReasoningExpanded
?
'160px'
:
'0px'
,
}
overflow-hidden`
}
overflowY
:
message
.
isReasoningExpanded
?
'auto'
:
'hidden'
,
overflowX
:
'hidden'
,
transition
:
'max-height 0.3s ease-in-out, padding 0.3s ease-in-out'
,
padding
:
message
.
isReasoningExpanded
?
'0px 12px 12px 12px'
:
'0px 12px'
,
boxSizing
:
'border-box'
,
}}
>
>
<
MarkdownRender
raw
=
{
finalExtractedThinkingContent
}
/
>
{
message
.
isReasoningExpanded
&&
(
<
div
className
=
"p-5 pt-4"
>
<
div
className
=
"bg-white/70 backdrop-blur-sm rounded-xl p-4 shadow-inner overflow-x-auto max-h-50 overflow-y-auto"
>
<
div
className
=
"prose prose-sm prose-purple max-w-none"
>
<
MarkdownRender
raw
=
{
finalExtractedThinkingContent
}
/
>
<
/div
>
<
/div
>
<
/div
>
)}
<
/div
>
<
/div
>
<
/div
>
<
/div
>
)}
)}
{(
finalDisplayableFinalContent
&&
finalDisplayableFinalContent
.
trim
()
!==
''
)
&&
(
{(
finalDisplayableFinalContent
&&
finalDisplayableFinalContent
.
trim
()
!==
''
)
&&
(
<
MarkdownRender
raw
=
{
finalDisplayableFinalContent
}
/
>
<
div
className
=
"prose prose-sm prose-gray max-w-none overflow-x-auto"
>
)}
<
MarkdownRender
raw
=
{
finalDisplayableFinalContent
}
/
>
{
!
(
finalExtractedThinkingContent
)
&&
!
(
finalDisplayableFinalContent
&&
finalDisplayableFinalContent
.
trim
()
!==
''
)
&&
message
.
role
===
'assistant'
&&
(
<
/div
>
<
div
><
/div
>
)}
)}
<
/div
>
<
/div
>
);
);
...
@@ -615,115 +1165,460 @@ const Playground = () => {
...
@@ -615,115 +1165,460 @@ const Playground = () => {
);
);
return
(
return
(
<
Layout
style
=
{{
height
:
'100%'
}}
>
<
div
className
=
"min-h-screen bg-gray-50"
>
{(
showSettings
||
!
styleState
.
isMobile
)
&&
(
<
Layout
style
=
{{
height
:
'100vh'
,
background
:
'transparent'
}}
>
<
Layout
.
Sider
{(
showSettings
||
!
styleState
.
isMobile
)
&&
(
style
=
{{
display
:
styleState
.
isMobile
?
'block'
:
'initial'
}}
<
Layout
.
Sider
>
style
=
{{
<
Card
style
=
{
commonOuterStyle
}
>
background
:
'transparent'
,
<
div
style
=
{{
marginTop
:
10
}}
>
borderRight
:
'none'
,
<
Typography
.
Text
strong
>
{
t
(
'分组'
)}
:
<
/Typography.Text
>
flexShrink
:
0
,
<
/div
>
minWidth
:
320
,
<
Select
maxWidth
:
320
,
placeholder
=
{
t
(
'请选择分组'
)}
height
:
'calc(100vh - 100px)'
,
name
=
'group'
}}
required
width
=
{
320
}
selection
>
onChange
=
{(
value
)
=>
{
<
Card
className
=
"!rounded-2xl h-full flex flex-col"
bodyStyle
=
{{
padding
:
'24px'
,
height
:
'100%'
,
display
:
'flex'
,
flexDirection
:
'column'
}}
>
handleInputChange
(
'group'
,
value
);
<
div
className
=
"flex items-center justify-between mb-6 flex-shrink-0"
>
}}
<
div
className
=
"flex items-center"
>
value
=
{
inputs
.
group
}
<
div
className
=
"w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center mr-3"
>
autoComplete
=
'new-password'
<
SlidersHorizontal
size
=
{
20
}
className
=
"text-white"
/>
optionList
=
{
groups
}
<
/div
>
renderOptionItem
=
{
renderGroupOption
}
<
Typography
.
Title
heading
=
{
5
}
className
=
"mb-0"
>
style
=
{{
width
:
'100%'
}}
{
t
(
'模型设置'
)}
/
>
<
/Typography.Title
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
/div
>
<
Typography
.
Text
strong
>
{
t
(
'模型'
)}
:
<
/Typography.Text
>
<
DebugToggle
/>
<
/div
>
<
/div
>
<
Select
placeholder
=
{
t
(
'请选择模型'
)}
<
div
className
=
"space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll"
>
name
=
'model'
{
/* 分组选择 */
}
required
<
div
>
selection
<
div
className
=
"flex items-center gap-2 mb-2"
>
searchPosition
=
'dropdown'
<
Users
size
=
{
16
}
className
=
"text-gray-500"
/>
filter
<
Typography
.
Text
strong
className
=
"text-sm"
>
onChange
=
{(
value
)
=>
{
{
t
(
'分组'
)}
handleInputChange
(
'model'
,
value
);
<
/Typography.Text
>
}}
<
/div
>
value
=
{
inputs
.
model
}
<
Select
autoComplete
=
'new-password'
placeholder
=
{
t
(
'请选择分组'
)}
optionList
=
{
models
}
name
=
'group'
/
>
required
<
div
style
=
{{
marginTop
:
10
}}
>
selection
<
Typography
.
Text
strong
>
Temperature
:
<
/Typography.Text
>
onChange
=
{(
value
)
=>
handleInputChange
(
'group'
,
value
)}
<
/div
>
value
=
{
inputs
.
group
}
<
Slider
autoComplete
=
'new-password'
step
=
{
0.1
}
optionList
=
{
groups
}
min
=
{
0.1
}
renderOptionItem
=
{
renderGroupOption
}
max
=
{
1
}
style
=
{{
width
:
'100%'
}}
value
=
{
inputs
.
temperature
}
className
=
"!rounded-lg"
onChange
=
{(
value
)
=>
{
/>
handleInputChange
(
'temperature'
,
value
);
<
/div
>
}}
/
>
{
/* 模型选择 */
}
<
div
style
=
{{
marginTop
:
10
}}
>
<
div
>
<
Typography
.
Text
strong
>
MaxTokens
:
<
/Typography.Text
>
<
div
className
=
"flex items-center gap-2 mb-2"
>
<
/div
>
<
Sparkles
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Input
<
Typography
.
Text
strong
className
=
"text-sm"
>
placeholder
=
'MaxTokens'
{
t
(
'模型'
)}
name
=
'max_tokens'
<
/Typography.Text
>
required
<
/div
>
autoComplete
=
'new-password'
<
Select
defaultValue
=
{
0
}
placeholder
=
{
t
(
'请选择模型'
)}
value
=
{
inputs
.
max_tokens
}
name
=
'model'
onChange
=
{(
value
)
=>
{
required
handleInputChange
(
'max_tokens'
,
value
);
selection
}}
searchPosition
=
'dropdown'
/
>
filter
onChange
=
{(
value
)
=>
handleInputChange
(
'model'
,
value
)}
value
=
{
inputs
.
model
}
autoComplete
=
'new-password'
optionList
=
{
models
}
className
=
"!rounded-lg"
/>
<
/div
>
{
/* Temperature */
}
<
div
className
=
{
`transition-opacity duration-200
${
!
parameterEnabled
.
temperature
?
'opacity-50'
:
''
}
`
}
>
<
div
className
=
"flex items-center justify-between mb-2"
>
<
div
className
=
"flex items-center gap-2"
>
<
Thermometer
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
strong
className
=
"text-sm"
>
Temperature
<
/Typography.Text
>
<
Tag
size
=
"small"
className
=
"!rounded-full"
>
{
inputs
.
temperature
}
<
/Tag
>
<
/div
>
<
Button
theme
=
{
parameterEnabled
.
temperature
?
'solid'
:
'borderless'
}
type
=
{
parameterEnabled
.
temperature
?
'primary'
:
'tertiary'
}
size
=
"small"
icon
=
{
parameterEnabled
.
temperature
?
<
Check
size
=
{
10
}
/> : <X size=
{10}
/
>
}
onClick
=
{()
=>
handleParameterToggle
(
'temperature'
)}
className
=
"!rounded-full !w-6 !h-6 !p-0 !min-w-0"
/>
<
/div
>
<
Typography
.
Text
className
=
"text-xs text-gray-500 mb-2"
>
控制输出的随机性和创造性
<
/Typography.Text
>
<
Slider
step
=
{
0.1
}
min
=
{
0.1
}
max
=
{
1
}
value
=
{
inputs
.
temperature
}
onChange
=
{(
value
)
=>
handleInputChange
(
'temperature'
,
value
)}
className
=
"mt-2"
disabled
=
{
!
parameterEnabled
.
temperature
}
/
>
<
/div
>
{
/* Top P */
}
<
div
className
=
{
`transition-opacity duration-200
${
!
parameterEnabled
.
top_p
?
'opacity-50'
:
''
}
`
}
>
<
div
className
=
"flex items-center justify-between mb-2"
>
<
div
className
=
"flex items-center gap-2"
>
<
Target
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
strong
className
=
"text-sm"
>
Top
P
<
/Typography.Text
>
<
Tag
size
=
"small"
className
=
"!rounded-full"
>
{
inputs
.
top_p
}
<
/Tag
>
<
/div
>
<
Button
theme
=
{
parameterEnabled
.
top_p
?
'solid'
:
'borderless'
}
type
=
{
parameterEnabled
.
top_p
?
'primary'
:
'tertiary'
}
size
=
"small"
icon
=
{
parameterEnabled
.
top_p
?
<
Check
size
=
{
10
}
/> : <X size=
{10}
/
>
}
onClick
=
{()
=>
handleParameterToggle
(
'top_p'
)}
className
=
"!rounded-full !w-6 !h-6 !p-0 !min-w-0"
/>
<
/div
>
<
Typography
.
Text
className
=
"text-xs text-gray-500 mb-2"
>
核采样,控制词汇选择的多样性
<
/Typography.Text
>
<
Slider
step
=
{
0.1
}
min
=
{
0.1
}
max
=
{
1
}
value
=
{
inputs
.
top_p
}
onChange
=
{(
value
)
=>
handleInputChange
(
'top_p'
,
value
)}
className
=
"mt-2"
disabled
=
{
!
parameterEnabled
.
top_p
}
/
>
<
/div
>
{
/* Frequency Penalty */
}
<
div
className
=
{
`transition-opacity duration-200
${
!
parameterEnabled
.
frequency_penalty
?
'opacity-50'
:
''
}
`
}
>
<
div
className
=
"flex items-center justify-between mb-2"
>
<
div
className
=
"flex items-center gap-2"
>
<
Repeat
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
strong
className
=
"text-sm"
>
Frequency
Penalty
<
/Typography.Text
>
<
Tag
size
=
"small"
className
=
"!rounded-full"
>
{
inputs
.
frequency_penalty
}
<
/Tag
>
<
/div
>
<
Button
theme
=
{
parameterEnabled
.
frequency_penalty
?
'solid'
:
'borderless'
}
type
=
{
parameterEnabled
.
frequency_penalty
?
'primary'
:
'tertiary'
}
size
=
"small"
icon
=
{
parameterEnabled
.
frequency_penalty
?
<
Check
size
=
{
10
}
/> : <X size=
{10}
/
>
}
onClick
=
{()
=>
handleParameterToggle
(
'frequency_penalty'
)}
className
=
"!rounded-full !w-6 !h-6 !p-0 !min-w-0"
/>
<
/div
>
<
Typography
.
Text
className
=
"text-xs text-gray-500 mb-2"
>
频率惩罚,减少重复词汇的出现
<
/Typography.Text
>
<
Slider
step
=
{
0.1
}
min
=
{
-
2
}
max
=
{
2
}
value
=
{
inputs
.
frequency_penalty
}
onChange
=
{(
value
)
=>
handleInputChange
(
'frequency_penalty'
,
value
)}
className
=
"mt-2"
disabled
=
{
!
parameterEnabled
.
frequency_penalty
}
/
>
<
/div
>
{
/* Presence Penalty */
}
<
div
className
=
{
`transition-opacity duration-200
${
!
parameterEnabled
.
presence_penalty
?
'opacity-50'
:
''
}
`
}
>
<
div
className
=
"flex items-center justify-between mb-2"
>
<
div
className
=
"flex items-center gap-2"
>
<
Ban
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
strong
className
=
"text-sm"
>
Presence
Penalty
<
/Typography.Text
>
<
Tag
size
=
"small"
className
=
"!rounded-full"
>
{
inputs
.
presence_penalty
}
<
/Tag
>
<
/div
>
<
Button
theme
=
{
parameterEnabled
.
presence_penalty
?
'solid'
:
'borderless'
}
type
=
{
parameterEnabled
.
presence_penalty
?
'primary'
:
'tertiary'
}
size
=
"small"
icon
=
{
parameterEnabled
.
presence_penalty
?
<
Check
size
=
{
10
}
/> : <X size=
{10}
/
>
}
onClick
=
{()
=>
handleParameterToggle
(
'presence_penalty'
)}
className
=
"!rounded-full !w-6 !h-6 !p-0 !min-w-0"
/>
<
/div
>
<
Typography
.
Text
className
=
"text-xs text-gray-500 mb-2"
>
存在惩罚,鼓励讨论新话题
<
/Typography.Text
>
<
Slider
step
=
{
0.1
}
min
=
{
-
2
}
max
=
{
2
}
value
=
{
inputs
.
presence_penalty
}
onChange
=
{(
value
)
=>
handleInputChange
(
'presence_penalty'
,
value
)}
className
=
"mt-2"
disabled
=
{
!
parameterEnabled
.
presence_penalty
}
/
>
<
/div
>
{
/* MaxTokens */
}
<
div
className
=
{
`transition-opacity duration-200
${
!
parameterEnabled
.
max_tokens
?
'opacity-50'
:
''
}
`
}
>
<
div
className
=
"flex items-center justify-between mb-2"
>
<
div
className
=
"flex items-center gap-2"
>
<
Hash
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
strong
className
=
"text-sm"
>
Max
Tokens
<
/Typography.Text
>
<
/div
>
<
Button
theme
=
{
parameterEnabled
.
max_tokens
?
'solid'
:
'borderless'
}
type
=
{
parameterEnabled
.
max_tokens
?
'primary'
:
'tertiary'
}
size
=
"small"
icon
=
{
parameterEnabled
.
max_tokens
?
<
Check
size
=
{
10
}
/> : <X size=
{10}
/
>
}
onClick
=
{()
=>
handleParameterToggle
(
'max_tokens'
)}
className
=
"!rounded-full !w-6 !h-6 !p-0 !min-w-0"
/>
<
/div
>
<
Input
placeholder
=
'MaxTokens'
name
=
'max_tokens'
required
autoComplete
=
'new-password'
defaultValue
=
{
0
}
value
=
{
inputs
.
max_tokens
}
onChange
=
{(
value
)
=>
handleInputChange
(
'max_tokens'
,
value
)}
className
=
"!rounded-lg"
disabled
=
{
!
parameterEnabled
.
max_tokens
}
/
>
<
/div
>
{
/* Seed */
}
<
div
className
=
{
`transition-opacity duration-200
${
!
parameterEnabled
.
seed
?
'opacity-50'
:
''
}
`
}
>
<
div
className
=
"flex items-center justify-between mb-2"
>
<
div
className
=
"flex items-center gap-2"
>
<
Shuffle
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
strong
className
=
"text-sm"
>
Seed
<
/Typography.Text
>
<
Typography
.
Text
className
=
"text-xs text-gray-400"
>
(
可选,用于复现结果
)
<
/Typography.Text
>
<
/div
>
<
Button
theme
=
{
parameterEnabled
.
seed
?
'solid'
:
'borderless'
}
type
=
{
parameterEnabled
.
seed
?
'primary'
:
'tertiary'
}
size
=
"small"
icon
=
{
parameterEnabled
.
seed
?
<
Check
size
=
{
10
}
/> : <X size=
{10}
/
>
}
onClick
=
{()
=>
handleParameterToggle
(
'seed'
)}
className
=
"!rounded-full !w-6 !h-6 !p-0 !min-w-0"
/>
<
/div
>
<
Input
placeholder
=
'随机种子 (留空为随机)'
name
=
'seed'
autoComplete
=
'new-password'
value
=
{
inputs
.
seed
||
''
}
onChange
=
{(
value
)
=>
handleInputChange
(
'seed'
,
value
===
''
?
null
:
value
)}
className
=
"!rounded-lg"
disabled
=
{
!
parameterEnabled
.
seed
}
/
>
<
/div
>
{
/* Stream Toggle */
}
<
div
>
<
div
className
=
"flex items-center justify-between"
>
<
div
className
=
"flex items-center gap-2"
>
<
ToggleLeft
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
strong
className
=
"text-sm"
>
流式输出
<
/Typography.Text
>
<
/div
>
<
Button
theme
=
{
inputs
.
stream
?
'solid'
:
'borderless'
}
type
=
{
inputs
.
stream
?
'primary'
:
'tertiary'
}
size
=
"small"
onClick
=
{()
=>
handleInputChange
(
'stream'
,
!
inputs
.
stream
)}
className
=
"!rounded-full"
>
{
inputs
.
stream
?
'开启'
:
'关闭'
}
<
/Button
>
<
/div
>
<
/div
>
{
/* System Prompt */
}
<
div
>
<
div
className
=
"flex items-center gap-2 mb-2"
>
<
Type
size
=
{
16
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
strong
className
=
"text-sm"
>
System
Prompt
<
/Typography.Text
>
<
/div
>
<
TextArea
placeholder
=
'System Prompt'
name
=
'system'
required
autoComplete
=
'new-password'
autosize
defaultValue
=
{
systemPrompt
}
onChange
=
{(
value
)
=>
setSystemPrompt
(
value
)}
className
=
"!rounded-lg"
maxHeight
=
{
200
}
/
>
<
/div
>
<
/div
>
<
/Card
>
<
/Layout.Sider
>
)}
<
div
style
=
{{
marginTop
:
10
}}
>
<
Layout
.
Content
className
=
"relative flex-1 overflow-hidden"
>
<
Typography
.
Text
strong
>
System
:
<
/Typography.Text
>
<
div
className
=
"px-4 overflow-hidden flex gap-4"
style
=
{{
height
:
'calc(100vh - 100px)'
}}
>
<
div
className
=
"flex-1 flex flex-col"
>
<
SettingsToggle
/>
<
Card
className
=
"!rounded-2xl h-full"
bodyStyle
=
{{
padding
:
0
,
height
:
'100%'
,
display
:
'flex'
,
flexDirection
:
'column'
,
overflow
:
'hidden'
}}
>
{
/* 聊天头部 */
}
<
div
className
=
"px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl"
>
<
div
className
=
"flex items-center gap-3"
>
<
div
className
=
"w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center"
>
<
MessageSquare
size
=
{
20
}
className
=
"text-white"
/>
<
/div
>
<
div
>
<
Typography
.
Title
heading
=
{
5
}
className
=
"!text-white mb-0"
>
{
t
(
'AI 对话'
)}
<
/Typography.Title
>
<
Typography
.
Text
className
=
"!text-white/80 text-sm"
>
{
inputs
.
model
||
t
(
'选择模型开始对话'
)}
<
/Typography.Text
>
<
/div
>
<
/div
>
<
/div
>
{
/* 聊天内容区域 */
}
<
div
className
=
"flex-1 overflow-hidden"
>
<
Chat
chatBoxRenderConfig
=
{{
renderChatBoxContent
:
renderCustomChatContent
,
renderChatBoxAction
:
renderChatBoxAction
,
}}
renderInputArea
=
{
renderInputArea
}
roleConfig
=
{
roleInfo
}
style
=
{{
height
:
'100%'
,
maxWidth
:
'100%'
,
overflow
:
'hidden'
}}
chats
=
{
message
}
onMessageSend
=
{
onMessageSend
}
onMessageCopy
=
{
handleMessageCopy
}
onMessageReset
=
{
handleMessageReset
}
onMessageDelete
=
{
handleMessageDelete
}
showClearContext
showStopGenerate
onStopGenerator
=
{
onStopGenerator
}
onClear
=
{()
=>
setMessage
([])}
className
=
"h-full"
placeholder
=
{
t
(
'请输入您的问题...'
)}
/
>
<
/div
>
<
/Card
>
<
/div
>
<
/div
>
<
TextArea
placeholder
=
'System Prompt'
{
/* 调试面板 */
}
name
=
'system'
{
showDebugPanel
&&
(
required
<
div
className
=
"w-96 flex-shrink-0"
>
autoComplete
=
'new-password'
<
Card
className
=
"!rounded-2xl h-full flex flex-col"
bodyStyle
=
{{
padding
:
'24px'
,
height
:
'100%'
,
display
:
'flex'
,
flexDirection
:
'column'
}}
>
autosize
<
div
className
=
"flex items-center mb-6 flex-shrink-0"
>
defaultValue
=
{
systemPrompt
}
<
div
className
=
"w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-blue-500 flex items-center justify-center mr-3"
>
onChange
=
{(
value
)
=>
{
<
Code
size
=
{
20
}
className
=
"text-white"
/>
setSystemPrompt
(
value
);
<
/div
>
}}
<
Typography
.
Title
heading
=
{
5
}
className
=
"mb-0"
>
/
>
{
t
(
'调试信息'
)}
<
/Card
>
<
/Typography.Title
>
<
/Layout.Sider
>
<
/div
>
)}
<
Layout
.
Content
>
<
div
className
=
"flex-1 overflow-hidden debug-panel"
>
<
div
style
=
{{
height
:
'100%'
,
position
:
'relative'
}}
>
<
Tabs
<
SettingsToggle
/>
type
=
"line"
<
Chat
className
=
"h-full"
chatBoxRenderConfig
=
{{
style
=
{{
height
:
'100%'
,
display
:
'flex'
,
flexDirection
:
'column'
}}
renderChatBoxContent
:
renderCustomChatContent
,
activeKey
=
{
activeDebugTab
}
renderChatBoxAction
:
()
=>
{
onChange
=
{
setActiveDebugTab
}
return
<
div
><
/div>
;
>
},
<
TabPane
tab
=
{
}}
<
div
className
=
"flex items-center gap-2"
>
renderInputArea
=
{
renderInputArea
}
<
FileText
size
=
{
16
}
/
>
roleConfig
=
{
roleInfo
}
{
t
(
'请求体'
)}
style
=
{
commonOuterStyle
}
<
/div
>
chats
=
{
message
}
}
itemKey
=
"request"
>
onMessageSend
=
{
onMessageSend
}
<
div
className
=
"h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll"
>
showClearContext
{
debugData
.
request
?
(
showStopGenerate
<
pre
className
=
"debug-code text-gray-700 whitespace-pre-wrap break-words"
>
onStopGenerator
=
{
onStopGenerator
}
{
JSON
.
stringify
(
debugData
.
request
,
null
,
2
)}
onClear
=
{()
=>
{
<
/pre
>
setMessage
([]);
)
:
(
}}
<
Typography
.
Text
type
=
"secondary"
className
=
"text-sm"
>
/
>
{
t
(
'暂无请求数据'
)}
<
/div
>
<
/Typography.Text
>
<
/Layout.Content
>
)}
<
/Layout
>
<
/div
>
<
/TabPane
>
<
TabPane
tab
=
{
<
div
className
=
"flex items-center gap-2"
>
<
Zap
size
=
{
16
}
/
>
{
t
(
'响应内容'
)}
<
/div
>
}
itemKey
=
"response"
>
<
div
className
=
"h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll"
>
{
debugData
.
response
?
(
<
pre
className
=
"debug-code text-gray-700 whitespace-pre-wrap break-words"
>
{
debugData
.
response
}
<
/pre
>
)
:
(
<
Typography
.
Text
type
=
"secondary"
className
=
"text-sm"
>
{
t
(
'暂无响应数据'
)}
<
/Typography.Text
>
)}
<
/div
>
<
/TabPane
>
<
/Tabs
>
<
/div
>
{
debugData
.
timestamp
&&
(
<
div
className
=
"flex items-center gap-2 mt-4 pt-4 flex-shrink-0"
>
<
Clock
size
=
{
14
}
className
=
"text-gray-500"
/>
<
Typography
.
Text
className
=
"text-xs text-gray-500"
>
{
t
(
'最后更新'
)}:
{
new
Date
(
debugData
.
timestamp
).
toLocaleString
()}
<
/Typography.Text
>
<
/div
>
)}
<
/Card
>
<
/div
>
)}
<
/div
>
<
/Layout.Content
>
<
/Layout
>
<
/div
>
);
);
};
};
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment