feat: add save feedback prompts, featured agent fields, dynamic Dify API config, and chat improvements

- Add success/error feedback messages near submit buttons in all admin forms
- Display success prompt for 1s before redirect after save
- Show API error details on save failure
- Add isFeatured/featuredOrder fields to Agent model and admin UI
- Add difyApiUrl/difyApiKey fields for per-agent Dify API configuration
- Show featured badge column in agent admin list
- Display featured agents on homepage sorted by order
- Refactor chat page streaming with AbortController and stable userId
- Improve Dify API proxy to use per-agent credentials
This commit is contained in:
root
2026-05-08 20:15:54 +08:00
parent 362c37fb42
commit f2d7037ca2
16 changed files with 298 additions and 102 deletions
+42 -42
View File
@@ -48,8 +48,10 @@ export default function AgentChatPage() {
const [currentConversationId, setCurrentConversationId] = useState<number | null>(null)
const [upstreamConversationId, setUpstreamConversationId] = useState<string>('')
const [loadingConversations, setLoadingConversations] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const userIdRef = useRef<string>('user-' + Math.random().toString(36).slice(2, 10))
const CHAT_API_URL = '/api/chat'
@@ -190,8 +192,6 @@ export default function AgentChatPage() {
})
const assistantMessageId = (Date.now() + 1).toString()
let assistantContent = ''
let upstreamConvId = upstreamConversationId
setMessages(prev => [...prev, {
id: assistantMessageId,
@@ -200,70 +200,70 @@ export default function AgentChatPage() {
timestamp: new Date(),
}])
const controller = new AbortController()
setAbortController(controller)
const response = await fetch(CHAT_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agentId: agent.id,
inputs: {},
query: currentInput,
response_mode: 'streaming',
conversation_id: upstreamConversationId,
user: 'user-' + Date.now(),
user: userIdRef.current,
}),
signal: controller.signal,
})
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!response.ok) {
throw new Error('Chat request failed')
}
if (reader) {
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const json = JSON.parse(line.slice(6))
if (json.conversation_id) {
upstreamConvId = json.conversation_id
}
const token = json.answer || json.message || ''
if (token) {
assistantContent += token
setMessages(prev => prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, content: assistantContent }
: msg
))
}
} catch (e) {
// ignore parse errors
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
let accumulatedContent = ''
let finalConvId = upstreamConversationId
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6))
if (data.event === 'message' && data.answer) {
accumulatedContent += data.answer
setMessages(prev => prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, content: accumulatedContent }
: msg
))
} else if (data.event === 'message_end') {
finalConvId = data.conversation_id || finalConvId
}
} catch (e) {
// skip invalid JSON
}
}
}
}
setUpstreamConversationId(upstreamConvId)
if (!assistantContent) {
setMessages(prev => prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, content: '抱歉,我暂时无法回答这个问题。' }
: msg
))
assistantContent = '抱歉,我暂时无法回答这个问题。'
}
setUpstreamConversationId(finalConvId)
await fetch(`/api/conversations/${conversationId}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
role: 'assistant',
content: assistantContent,
content: accumulatedContent,
}),
})
} catch (error) {