diff --git a/src/models/chat_model.py b/src/models/chat_model.py
index e3a83d1e..43b00aa5 100644
--- a/src/models/chat_model.py
+++ b/src/models/chat_model.py
@@ -31,7 +31,8 @@ class OpenAIBase():
stream=True,
)
for chunk in response:
- yield chunk.choices[0].delta
+ if len(chunk.choices) > 0:
+ yield chunk.choices[0].delta
def _get_response(self, messages):
response = self.client.chat.completions.create(
diff --git a/web/src/components/ChatComponent.vue b/web/src/components/ChatComponent.vue
index efd98363..60e61f5d 100644
--- a/web/src/components/ChatComponent.vue
+++ b/web/src/components/ChatComponent.vue
@@ -77,6 +77,7 @@
-
+
@@ -100,7 +102,7 @@
:is-loading="isStreaming"
:send-button-disabled="!conv.inputText && !isStreaming"
:auto-size="{ minRows: 2, maxRows: 10 }"
- @send="sendMessage"
+ @send="handleSendOrStop"
@keydown="handleKeyDown"
>
@@ -171,6 +173,9 @@ import {
BulbOutlined,
CaretRightOutlined,
DeploymentUnitOutlined,
+ PauseOutlined,
+ ReloadOutlined,
+ CopyOutlined
} from '@ant-design/icons-vue'
import { onClickOutside } from '@vueuse/core'
import { Marked } from 'marked';
@@ -221,7 +226,7 @@ const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
selectedKB: null,
stream: true,
summary_title: false,
- history_round: 5,
+ history_round: 20,
db_id: null,
fontSize: 'default',
wideScreen: false,
@@ -253,6 +258,24 @@ const renderMarkdown = (msg) => {
}
}
+// 从 message 中获取 history 信息,每个消息都是 {role, content} 的格式
+const getHistory = () => {
+ const history = conv.value.messages.map((msg) => {
+ if (msg.text) {
+ return {
+ role: msg.role === 'sent' ? 'user' : 'assistant',
+ content: msg.text
+ }
+ }
+ }).reduce((acc, cur) => {
+ if (cur) {
+ acc.push(cur)
+ }
+ return acc
+ }, [])
+ return history.slice(-meta.history_round)
+}
+
const useDatabase = (index) => {
const selected = opts.databases[index]
console.log(selected)
@@ -441,18 +464,24 @@ const loadDatabases = () => {
// 新函数用于处理 fetch 请求
const fetchChatResponse = (user_input, cur_res_id) => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ const params = {
+ query: user_input,
+ history: getHistory(),
+ meta: meta,
+ cur_res_id: cur_res_id,
+ }
+ console.log(params)
+
fetch('/api/chat/', {
method: 'POST',
- body: JSON.stringify({
- query: user_input,
- history: conv.value.history,
- meta: meta,
- cur_res_id: cur_res_id,
- thread_id: conv.value.id.toString(),
- }),
+ body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json'
- }
+ },
+ signal // 添加 signal 用于中断请求
})
.then((response) => {
if (!response.body) throw new Error("ReadableStream not supported.");
@@ -510,13 +539,24 @@ const fetchChatResponse = (user_input, cur_res_id) => {
readChunk();
})
.catch((error) => {
- console.error(error);
- updateMessage({
- id: cur_res_id,
- status: "error",
- });
+ if (error.name === 'AbortError') {
+ console.log('Fetch aborted');
+ } else {
+ console.error(error);
+ updateMessage({
+ id: cur_res_id,
+ status: "error",
+ });
+ }
isStreaming.value = false;
});
+
+ // 监听 isStreaming 变化,当为 false 时中断请求
+ watch(isStreaming, (newValue) => {
+ if (!newValue) {
+ controller.abort();
+ }
+ });
}
@@ -606,6 +646,37 @@ watch(
{ deep: true }
);
+// 处理发送或停止
+const handleSendOrStop = () => {
+ if (isStreaming.value) {
+ // 停止生成
+ isStreaming.value = false;
+ const lastMessage = conv.value.messages[conv.value.messages.length - 1];
+ if (lastMessage) {
+ lastMessage.isStoppedByUser = true;
+ lastMessage.status = 'stopped';
+ }
+ } else {
+ // 发送消息
+ sendMessage();
+ }
+}
+
+// 重试被停止的消息
+const retryStoppedMessage = (id) => {
+ // 找到用户的原始问题
+ const messageIndex = conv.value.messages.findIndex(msg => msg.id === id);
+ if (messageIndex > 0) {
+ const userMessage = conv.value.messages[messageIndex - 1];
+ if (userMessage && userMessage.role === 'sent') {
+ conv.value.inputText = userMessage.text;
+ // 删除被停止的消息,以及所有后面的消息
+ conv.value.messages = conv.value.messages.slice(0, messageIndex-1);
+ // sendMessage();
+ }
+ }
+}
+
const modelNames = computed(() => configStore.config?.model_names)
const modelStatus = computed(() => configStore.config?.model_provider_status)
const customModels = computed(() => configStore.config?.custom_models || [])
@@ -620,6 +691,7 @@ const selectModel = (provider, name) => {
configStore.setConfigValue('model_provider', provider)
configStore.setConfigValue('model_name', name)
message.success(`已切换到模型: ${provider}/${name}`)
+
}
diff --git a/web/src/components/MessageComponent.vue b/web/src/components/MessageComponent.vue
index 70c52028..e5b0eca9 100644
--- a/web/src/components/MessageComponent.vue
+++ b/web/src/components/MessageComponent.vue
@@ -41,6 +41,11 @@
+
+ 你停止生成了本次回答
+ 重新编辑问题
+
+
@@ -60,6 +65,10 @@ import { CaretRightOutlined } from '@ant-design/icons-vue';
const props = defineProps({
// 消息角色:'user'|'assistant'|'sent'|'received'
+ message: {
+ type: Object,
+ required: true
+ },
role: {
type: String,
required: true
@@ -114,7 +123,7 @@ const statusDefination = {
error: '错误'
}
-const emit = defineEmits(['retry']);
+const emit = defineEmits(['retry', 'retryStoppedMessage']);
// 推理面板展开状态
const reasoningActiveKey = ref(['show']);
@@ -319,6 +328,35 @@ const isEmptyAndLoading = computed(() => {
}
}
+.retry-hint {
+ margin-top: 8px;
+ padding: 8px 16px;
+ color: #666;
+ font-size: 14px;
+ text-align: left;
+}
+
+.retry-link {
+ color: #1890ff;
+ cursor: pointer;
+ margin-left: 4px;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.ant-btn-icon-only {
+ &:has(.anticon-stop) {
+ background-color: #ff4d4f !important;
+
+ &:hover {
+ background-color: #ff7875 !important;
+ }
+ }
+}
+
+
.loading-dots {
display: inline-flex;
align-items: center;
diff --git a/web/src/components/MessageInputComponent.vue b/web/src/components/MessageInputComponent.vue
index 39744cd0..108ddccc 100644
--- a/web/src/components/MessageInputComponent.vue
+++ b/web/src/components/MessageInputComponent.vue
@@ -15,11 +15,17 @@
@@ -30,7 +36,8 @@ import { ref, computed, toRefs } from 'vue';
import {
SendOutlined,
ArrowUpOutlined,
- LoadingOutlined
+ LoadingOutlined,
+ PauseOutlined
} from '@ant-design/icons-vue';
const props = defineProps({
@@ -74,13 +81,13 @@ const emit = defineEmits(['update:modelValue', 'send', 'keydown']);
const iconComponents = {
'SendOutlined': SendOutlined,
'ArrowUpOutlined': ArrowUpOutlined,
- 'LoadingOutlined': LoadingOutlined
+ 'PauseOutlined': PauseOutlined
};
// 根据传入的图标名动态获取组件
const getIcon = computed(() => {
if (props.isLoading) {
- return LoadingOutlined;
+ return PauseOutlined;
}
return iconComponents[props.sendIcon] || ArrowUpOutlined;
});
@@ -97,7 +104,7 @@ const handleKeyPress = (e) => {
};
// 处理发送按钮点击
-const handleSend = () => {
+const handleSendOrStop = () => {
emit('send');
};
@@ -218,6 +225,7 @@ button.ant-btn-icon-only {
&:hover {
background-color: var(--main-600);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+ color: white;
}
&:active {
diff --git a/web/src/components/RefsComponent.vue b/web/src/components/RefsComponent.vue
index e79504cc..ad6be515 100644
--- a/web/src/components/RefsComponent.vue
+++ b/web/src/components/RefsComponent.vue
@@ -4,7 +4,8 @@
{{ msg.meta.server_model_name }}
-
+
+
{
const getPercent = (value) => {
return parseFloat((value * 100).toFixed(2))
}
+
+// 添加重新生成方法
+const regenerateMessage = () => {
+ emit('retry')
+}