Merge pull request #110 from zhaoshibao/feat-104

新增停止大模型回答功能(关联 Issue #104)
This commit is contained in:
Wenjie Zhang 2025-03-30 19:27:05 +08:00 committed by GitHub
commit e553e4498d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 153 additions and 20 deletions

View File

@ -28,7 +28,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(

View File

@ -118,7 +118,11 @@
>
请求错误请重试{{ message.message }}
</div>
<RefsComponent v-if="message.role=='received' && message.status=='finished'" :message="message" />
<div v-if="message.isStoppedByUser" class="retry-hint">
你停止生成了本次回答
<span class="retry-link" @click="retryStoppedMessage(message)">重新编辑问题</span>
</div>
<RefsComponent v-if="message.role=='received' && message.status=='finished'" :message="message" :conv="conv" @regenerateMessage="regenerateMessage" />
</div>
</div>
<div class="bottom">
@ -171,9 +175,19 @@
</a-dropdown>
</div>
<div class="options__right">
<a-button size="large" @click="sendMessage" :disabled="(!conv.inputText && !isStreaming)" type="link">
<template #icon> <ArrowUpOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
</a-button>
<a-tooltip :title="isStreaming ? '停止回答' : ''">
<a-button
size="large"
@click="handleSendOrStop"
:disabled="(!conv.inputText && !isStreaming)"
type="link"
>
<template #icon>
<PauseOutlined v-if="isStreaming" />
<ArrowUpOutlined v-else />
</template>
</a-button>
</a-tooltip>
</div>
</div>
</div>
@ -206,6 +220,9 @@ import {
BulbOutlined,
CaretRightOutlined,
DeploymentUnitOutlined,
PauseOutlined,
ReloadOutlined,
CopyOutlined
} from '@ant-design/icons-vue'
import { onClickOutside } from '@vueuse/core'
import { Marked } from 'marked';
@ -254,7 +271,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,
@ -286,6 +303,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)
@ -474,17 +509,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,
}),
body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json'
}
},
signal // signal
})
.then((response) => {
if (!response.body) throw new Error("ReadableStream not supported.");
@ -542,13 +584,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();
}
});
}
@ -626,6 +679,36 @@ 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 = (message) => {
//
const messageIndex = conv.value.messages.findIndex(msg => msg.id === message.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);
}
}
}
const modelNames = computed(() => configStore.config?.model_names)
const modelStatus = computed(() => configStore.config?.model_provider_status)
const customModels = computed(() => configStore.config?.custom_models || [])
@ -640,6 +723,13 @@ const selectModel = (provider, name) => {
configStore.setConfigValue('model_provider', provider)
configStore.setConfigValue('model_name', name)
message.success(`已切换到模型: ${provider}/${name}`)
}
//
const regenerateMessage = (message) => {
//
retryMessage(message.id)
}
</script>
@ -889,6 +979,9 @@ const selectModel = (provider, name) => {
padding-left: 0;
padding-right: 0;
text-align: justify;
position: relative;
}
p.message-text {
@ -1156,6 +1249,34 @@ const selectModel = (provider, name) => {
}
}
.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;
}
}
}
.scrollable-menu {
max-height: 300px;
overflow-y: auto;

View File

@ -4,7 +4,8 @@
<!-- <span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span> -->
<!-- <span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span> -->
<span class="item"><BulbOutlined /> {{ msg.meta.server_model_name }}</span>
<span class="item btn" @click="copyText(msg.text)"><CopyOutlined /></span>
<span class="item btn" @click="copyText(msg.text)" title="复制"><CopyOutlined /></span>
<span class="item btn" @click="regenerateMessage(msg)" title="重新生成"><ReloadOutlined /></span>
<span
class="item btn"
@click="openSubGraph(msg)"
@ -112,15 +113,20 @@ import {
DeploymentUnitOutlined,
BulbOutlined,
FileOutlined,
ClockCircleOutlined
ClockCircleOutlined,
ReloadOutlined,
} from '@ant-design/icons-vue'
import GraphContainer from './GraphContainer.vue' // GraphContainer
const emit = defineEmits(['regenerateMessage']);
const props = defineProps({
message: Object,
conv: Object,
})
const msg = ref(props.message)
const conv = ref(props.conv)
// 使 useClipboard
const { copy, isSupported } = useClipboard()
@ -210,6 +216,11 @@ const formatDate = (timestamp) => {
const getPercent = (value) => {
return parseFloat((value * 100).toFixed(2))
}
//
const regenerateMessage = (message) => {
emit('regenerateMessage', message)
}
</script>
<style lang="less" scoped>