This commit is contained in:
aaron 2025-06-02 09:48:46 +08:00
parent 4faacb2a69
commit 2b7e900cd5
2 changed files with 134 additions and 202 deletions

View File

@ -3,7 +3,7 @@ services:
build:
context: .
dockerfile: Dockerfile
image: tradus-web:1.3.35
image: tradus-web:1.3.36
container_name: tradus-web
ports:
- '6000:80'

View File

@ -59,6 +59,10 @@ const conversationToRename = ref<Conversation | null>(null)
const newConversationName = ref('')
const renameInputRef = ref<HTMLInputElement | null>(null)
//
const currentTaskId = ref<string>('')
const streamingAbortController = ref<AbortController | null>(null)
// APIURL
const apiBaseUrl =
import.meta.env.MODE === 'development' ? 'http://127.0.0.1:8000' : 'https://api.ibtc.work'
@ -201,6 +205,9 @@ const sendMessage = async () => {
isLoading.value = true
await scrollToBottom()
// AbortController
streamingAbortController.value = new AbortController()
try {
const response = await http.post(`${apiBaseUrl}/analysis/chat-messages`, {
message: message,
@ -233,6 +240,11 @@ const sendMessage = async () => {
let buffer = ''
while (true) {
//
if (streamingAbortController.value?.signal.aborted) {
break
}
const { done, value } = await reader.read()
if (done) break
@ -253,6 +265,11 @@ const sendMessage = async () => {
switch (data.event) {
case 'workflow_started':
// task_id
if (data.task_id) {
currentTaskId.value = data.task_id
console.log('获取到task_id:', data.task_id)
}
break
case 'node_started':
break
@ -292,12 +309,16 @@ const sendMessage = async () => {
}
}
assistantMessage.isStreaming = false
// task_id
currentTaskId.value = ''
break
case 'error':
const errorMessage = data.error || '未知错误'
assistantMessage.content = `错误: ${errorMessage}`
assistantMessage.isStreaming = false
// task_id
currentTaskId.value = ''
await scrollToBottom()
break
@ -309,6 +330,7 @@ const sendMessage = async () => {
console.error('解析响应数据出错:', error, '原始数据:', line)
assistantMessage.content = '错误: 解析响应数据时出错,请稍后重试'
assistantMessage.isStreaming = false
currentTaskId.value = ''
}
}
}
@ -323,6 +345,8 @@ const sendMessage = async () => {
assistantMessage.isStreaming = false
} finally {
isLoading.value = false
currentTaskId.value = ''
streamingAbortController.value = null
await scrollToBottom()
}
}
@ -504,6 +528,40 @@ const confirmDelete = async () => {
cancelDelete()
}
//
const stopStreaming = async () => {
if (!currentTaskId.value) return
try {
const response = await http.post(`${apiBaseUrl}/analysis/stop_streaming`, {
task_id: currentTaskId.value,
})
if (response.ok) {
console.log('停止流式输出成功')
} else {
console.error('停止流式输出失败')
}
} catch (error) {
console.error('停止流式输出失败:', error)
}
//
currentTaskId.value = ''
if (streamingAbortController.value) {
streamingAbortController.value.abort()
streamingAbortController.value = null
}
//
const lastMessage = messages.value[messages.value.length - 1]
if (lastMessage && lastMessage.isStreaming) {
lastMessage.isStreaming = false
}
isLoading.value = false
}
</script>
<template>
@ -730,21 +788,20 @@ const confirmDelete = async () => {
rows="1"
></textarea>
<button
v-if="!isLoading"
class="send-button"
@click="sendMessage"
:disabled="isLoading || !messageInput.trim()"
:disabled="!messageInput.trim()"
>
<svg
v-if="!isLoading"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22,2 15,22 11,13 2,9"></polygon>
</svg>
<div v-else class="button-loader"></div>
</button>
<button v-else class="stop-button" @click="stopStreaming" title="停止生成">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none">
<rect x="6" y="6" width="12" height="12" rx="2" ry="2"></rect>
</svg>
</button>
</div>
</div>
@ -1414,6 +1471,36 @@ const confirmDelete = async () => {
height: 20px;
}
.stop-button {
width: 44px;
height: 44px;
border: none;
border-radius: var(--border-radius);
background-color: rgba(239, 68, 68, 0.1);
color: #ef4444;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.stop-button:hover:not(:disabled) {
background-color: rgba(239, 68, 68, 0.2);
color: #dc2626;
}
.stop-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.stop-button svg {
width: 20px;
height: 20px;
}
.button-loader {
width: 16px;
height: 16px;
@ -1470,39 +1557,6 @@ const confirmDelete = async () => {
.welcome-content {
padding: 0 1rem;
}
}
@media (max-width: 480px) {
.chat-agent-view {
height: 100vh;
height: 100dvh;
min-height: -webkit-fill-available;
}
.chat-messages {
padding: 1.5rem 0.75rem 80px 0.75rem; /* 为固定输入框留出空间 */
}
.conversation-header {
padding: 0.6rem 0.75rem;
}
.conversation-title-container {
gap: 0.25rem;
}
.conversation-title {
font-size: 1rem;
}
.dropdown-icon {
width: 16px;
height: 16px;
}
.welcome-content {
padding: 0 1rem;
}
.example-grid {
grid-template-columns: 1fr;
@ -1568,6 +1622,18 @@ const confirmDelete = async () => {
height: 16px;
}
.stop-button {
width: 36px;
height: 36px;
border-radius: 18px;
flex-shrink: 0;
}
.stop-button svg {
width: 16px;
height: 16px;
}
.message {
gap: 0.75rem;
max-width: 90%;
@ -1613,171 +1679,37 @@ const confirmDelete = async () => {
}
}
.sidebar {
position: fixed;
top: 0;
right: -350px;
width: 350px;
height: 100vh;
background-color: var(--color-bg-secondary);
border-left: 1px solid var(--color-border);
z-index: 120;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
}
@media (max-width: 480px) {
.chat-agent-view {
height: 100vh;
height: 100dvh;
min-height: -webkit-fill-available;
}
.sidebar.open {
right: 0;
}
.chat-messages {
padding: 1.5rem 0.75rem 80px 0.75rem; /* 为固定输入框留出空间 */
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--color-border);
background-color: var(--color-bg-primary);
}
.conversation-header {
padding: 0.6rem 0.75rem;
}
.sidebar-header h3 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text-primary);
}
.conversation-title-container {
gap: 0.25rem;
}
.close-sidebar-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background-color: transparent;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.conversation-title {
font-size: 1rem;
}
.close-sidebar-btn:hover {
background-color: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.dropdown-icon {
width: 16px;
height: 16px;
}
.close-sidebar-btn svg {
width: 16px;
height: 16px;
}
.sidebar-content {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.new-conversation-item {
width: 100%;
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
margin-bottom: 1rem;
border: 1px dashed var(--color-border);
border-radius: var(--border-radius);
background-color: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
}
.new-conversation-item:hover:not(:disabled) {
border-color: var(--color-accent);
background-color: rgba(51, 85, 255, 0.05);
color: var(--color-accent);
}
.new-conversation-item:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.new-conversation-item svg {
width: 16px;
height: 16px;
}
.conversation-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.conversation-item {
width: 100%;
display: flex;
align-items: center;
padding: 0.75rem 1rem;
border: 1px solid transparent;
border-radius: var(--border-radius);
background-color: var(--color-bg-primary);
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
}
.conversation-item:hover:not(:disabled) {
border-color: var(--color-border);
background-color: var(--color-bg-secondary);
}
.conversation-item.active {
border-color: var(--color-accent);
background-color: rgba(51, 85, 255, 0.1);
}
.conversation-item:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.conversation-info {
flex: 1;
}
.conversation-name {
font-size: 0.9rem;
font-weight: 500;
color: var(--color-text-primary);
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-item.active .conversation-name {
color: var(--color-accent);
}
.loading-conversations,
.loading-history {
text-align: center;
padding: 1rem;
color: var(--color-text-secondary);
font-size: 0.85rem;
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
z-index: 110;
backdrop-filter: blur(2px);
.welcome-content {
padding: 0 1rem;
}
}
</style>