feat: show active conversations in sidebar
This commit is contained in:
@@ -1592,6 +1592,20 @@ describe('App', () => {
|
|||||||
if (!request) {
|
if (!request) {
|
||||||
throw new Error('Missing request')
|
throw new Error('Missing request')
|
||||||
}
|
}
|
||||||
|
const conversationList =
|
||||||
|
container.querySelector<HTMLElement>('.conversation-list')
|
||||||
|
if (!conversationList) {
|
||||||
|
throw new Error('Missing conversation list')
|
||||||
|
}
|
||||||
|
const activeIndicator = within(conversationList).getByRole('status', {
|
||||||
|
name: '会话正在活动'
|
||||||
|
})
|
||||||
|
expect(activeIndicator).toHaveClass(
|
||||||
|
'conversation-activity-indicator'
|
||||||
|
)
|
||||||
|
expect(activeIndicator.closest('.conversation-row')).toHaveTextContent(
|
||||||
|
'离开页面后继续生成'
|
||||||
|
)
|
||||||
await act(
|
await act(
|
||||||
() =>
|
() =>
|
||||||
new Promise<void>((resolve) =>
|
new Promise<void>((resolve) =>
|
||||||
@@ -1613,6 +1627,11 @@ describe('App', () => {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: '知识库' }))
|
fireEvent.click(screen.getByRole('button', { name: '知识库' }))
|
||||||
expect(chat).toBeInTheDocument()
|
expect(chat).toBeInTheDocument()
|
||||||
expect(chat.closest('[data-route="chat"]')).toHaveAttribute('hidden')
|
expect(chat.closest('[data-route="chat"]')).toHaveAttribute('hidden')
|
||||||
|
expect(
|
||||||
|
within(conversationList).getByRole('status', {
|
||||||
|
name: '会话正在活动'
|
||||||
|
})
|
||||||
|
).toBeInTheDocument()
|
||||||
act(() => {
|
act(() => {
|
||||||
agentListener?.({
|
agentListener?.({
|
||||||
requestId: request.requestId,
|
requestId: request.requestId,
|
||||||
@@ -1620,6 +1639,19 @@ describe('App', () => {
|
|||||||
delta: '后台新增的回复内容'
|
delta: '后台新增的回复内容'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
act(() => {
|
||||||
|
agentListener?.({
|
||||||
|
requestId: request.requestId,
|
||||||
|
type: 'done'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
within(conversationList).queryByRole('status', {
|
||||||
|
name: '会话正在活动'
|
||||||
|
})
|
||||||
|
).not.toBeInTheDocument()
|
||||||
|
)
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '对话' }))
|
fireEvent.click(screen.getByRole('button', { name: '对话' }))
|
||||||
expect(await screen.findByText('后台新增的回复内容')).toBeInTheDocument()
|
expect(await screen.findByText('后台新增的回复内容')).toBeInTheDocument()
|
||||||
|
|||||||
@@ -2046,6 +2046,26 @@ function App(): React.JSX.Element {
|
|||||||
const activityRecordsRef = useRef(activityRecords)
|
const activityRecordsRef = useRef(activityRecords)
|
||||||
const activeRuns = useRef(new Map<string, ActiveRun>())
|
const activeRuns = useRef(new Map<string, ActiveRun>())
|
||||||
const preparingConversations = useRef(new Set<string>())
|
const preparingConversations = useRef(new Set<string>())
|
||||||
|
const [activeConversationIds, setActiveConversationIds] = useState<
|
||||||
|
ReadonlySet<string>
|
||||||
|
>(() => new Set())
|
||||||
|
const setConversationActivity = useCallback(
|
||||||
|
(conversationId: string, active: boolean): void => {
|
||||||
|
setActiveConversationIds((current) => {
|
||||||
|
if (current.has(conversationId) === active) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
const next = new Set(current)
|
||||||
|
if (active) {
|
||||||
|
next.add(conversationId)
|
||||||
|
} else {
|
||||||
|
next.delete(conversationId)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
const hydratingArtifactIds = useRef(new Set<string>())
|
const hydratingArtifactIds = useRef(new Set<string>())
|
||||||
const knowledgeScopeInitialized = useRef(false)
|
const knowledgeScopeInitialized = useRef(false)
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
@@ -3464,6 +3484,7 @@ function App(): React.JSX.Element {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
activeRuns.current.delete(event.requestId)
|
activeRuns.current.delete(event.requestId)
|
||||||
|
setConversationActivity(run.conversationId, false)
|
||||||
flushConversationPersistenceAfterRenderRef.current = true
|
flushConversationPersistenceAfterRenderRef.current = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3471,6 +3492,7 @@ function App(): React.JSX.Element {
|
|||||||
recordActivity,
|
recordActivity,
|
||||||
loadWorkspaceChanges,
|
loadWorkspaceChanges,
|
||||||
refreshTokenUsage,
|
refreshTokenUsage,
|
||||||
|
setConversationActivity,
|
||||||
updateMessage,
|
updateMessage,
|
||||||
updateRequestActivity
|
updateRequestActivity
|
||||||
]
|
]
|
||||||
@@ -4651,6 +4673,7 @@ function App(): React.JSX.Element {
|
|||||||
delete next[conversationId]
|
delete next[conversationId]
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
setConversationActivity(conversationId, false)
|
||||||
const remaining = conversations.filter(
|
const remaining = conversations.filter(
|
||||||
(conversation) => conversation.id !== conversationId
|
(conversation) => conversation.id !== conversationId
|
||||||
)
|
)
|
||||||
@@ -4954,6 +4977,7 @@ function App(): React.JSX.Element {
|
|||||||
runtime.capability === 'image-generation' ? '' : selectedExpertId
|
runtime.capability === 'image-generation' ? '' : selectedExpertId
|
||||||
const workModeSnapshot = effectiveWorkMode
|
const workModeSnapshot = effectiveWorkMode
|
||||||
preparingConversations.current.add(conversationId)
|
preparingConversations.current.add(conversationId)
|
||||||
|
setConversationActivity(conversationId, true)
|
||||||
setInput('')
|
setInput('')
|
||||||
updateAttachments([])
|
updateAttachments([])
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
@@ -5546,6 +5570,7 @@ function App(): React.JSX.Element {
|
|||||||
await window.goodbuddy.agent.cancel(requestId)
|
await window.goodbuddy.agent.cancel(requestId)
|
||||||
}
|
}
|
||||||
activeRuns.current.clear()
|
activeRuns.current.clear()
|
||||||
|
setActiveConversationIds(new Set())
|
||||||
for (const attachments of attachmentsRef.current.values()) {
|
for (const attachments of attachmentsRef.current.values()) {
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
await window.goodbuddy.context.remove(attachment.id)
|
await window.goodbuddy.context.remove(attachment.id)
|
||||||
@@ -5889,6 +5914,14 @@ function App(): React.JSX.Element {
|
|||||||
</span>
|
</span>
|
||||||
<small>{formatTime(conversation.updatedAt, locale)}</small>
|
<small>{formatTime(conversation.updatedAt, locale)}</small>
|
||||||
</button>
|
</button>
|
||||||
|
{activeConversationIds.has(conversation.id) && (
|
||||||
|
<span
|
||||||
|
aria-label={t('conversation.active')}
|
||||||
|
className="conversation-activity-indicator"
|
||||||
|
role="status"
|
||||||
|
title={t('conversation.active')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
aria-controls={`conversation-actions-${conversation.id}`}
|
aria-controls={`conversation-actions-${conversation.id}`}
|
||||||
aria-expanded={
|
aria-expanded={
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export const app = {
|
|||||||
greeting:
|
greeting:
|
||||||
'Hi, I’m GoodBuddy. Ask me a question, add local files, or use your knowledge base. With an Agent Runtime enabled, I can also use tools with your authorization.',
|
'Hi, I’m GoodBuddy. Ask me a question, add local files, or use your knowledge base. With an Agent Runtime enabled, I can also use tools with your authorization.',
|
||||||
interrupted: 'The previous run stopped unexpectedly. You can resend your question.',
|
interrupted: 'The previous run stopped unexpectedly. You can resend your question.',
|
||||||
|
active: 'Conversation is active',
|
||||||
unread: 'Unread',
|
unread: 'Unread',
|
||||||
unreadRemote: 'Unread remote message',
|
unreadRemote: 'Unread remote message',
|
||||||
noRemote: 'No remote conversations yet',
|
noRemote: 'No remote conversations yet',
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export const app = {
|
|||||||
greeting:
|
greeting:
|
||||||
'你好,我是 GoodBuddy。你可以直接向我提问、添加本地文件或使用知识库。启用 Agent Runtime 后,我也可以在你的授权下调用工具。',
|
'你好,我是 GoodBuddy。你可以直接向我提问、添加本地文件或使用知识库。启用 Agent Runtime 后,我也可以在你的授权下调用工具。',
|
||||||
interrupted: '上次运行意外中断,可以重新发送问题',
|
interrupted: '上次运行意外中断,可以重新发送问题',
|
||||||
|
active: '会话正在活动',
|
||||||
unread: '未读',
|
unread: '未读',
|
||||||
unreadRemote: '未读远程消息',
|
unreadRemote: '未读远程消息',
|
||||||
noRemote: '尚无远程会话',
|
noRemote: '尚无远程会话',
|
||||||
|
|||||||
@@ -6962,6 +6962,19 @@ details.settings-section > :not(summary) + :not(summary) {
|
|||||||
padding-right: 58px;
|
padding-right: 58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-activity-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: pulse 1.1s ease-in-out infinite;
|
||||||
|
background: var(--success);
|
||||||
|
box-shadow: 0 0 0 4px
|
||||||
|
color-mix(in srgb, var(--success) 14%, transparent);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-more,
|
.conversation-more,
|
||||||
.conversation-delete {
|
.conversation-delete {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user