Merge pull request #110 from zhaoshibao/feat-104
新增停止大模型回答功能(关联 Issue #104)
This commit is contained in:
commit
e553e4498d
@ -28,7 +28,8 @@ class OpenAIBase():
|
|||||||
stream=True,
|
stream=True,
|
||||||
)
|
)
|
||||||
for chunk in response:
|
for chunk in response:
|
||||||
yield chunk.choices[0].delta
|
if len(chunk.choices) > 0:
|
||||||
|
yield chunk.choices[0].delta
|
||||||
|
|
||||||
def _get_response(self, messages):
|
def _get_response(self, messages):
|
||||||
response = self.client.chat.completions.create(
|
response = self.client.chat.completions.create(
|
||||||
|
|||||||
@ -118,7 +118,11 @@
|
|||||||
>
|
>
|
||||||
请求错误,请重试。{{ message.message }}
|
请求错误,请重试。{{ message.message }}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
@ -171,9 +175,19 @@
|
|||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div class="options__right">
|
<div class="options__right">
|
||||||
<a-button size="large" @click="sendMessage" :disabled="(!conv.inputText && !isStreaming)" type="link">
|
<a-tooltip :title="isStreaming ? '停止回答' : ''">
|
||||||
<template #icon> <ArrowUpOutlined v-if="!isStreaming" /> <LoadingOutlined v-else/> </template>
|
<a-button
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -206,6 +220,9 @@ import {
|
|||||||
BulbOutlined,
|
BulbOutlined,
|
||||||
CaretRightOutlined,
|
CaretRightOutlined,
|
||||||
DeploymentUnitOutlined,
|
DeploymentUnitOutlined,
|
||||||
|
PauseOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
CopyOutlined
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
import { Marked } from 'marked';
|
import { Marked } from 'marked';
|
||||||
@ -254,7 +271,7 @@ const meta = reactive(JSON.parse(localStorage.getItem('meta')) || {
|
|||||||
selectedKB: null,
|
selectedKB: null,
|
||||||
stream: true,
|
stream: true,
|
||||||
summary_title: false,
|
summary_title: false,
|
||||||
history_round: 5,
|
history_round: 20,
|
||||||
db_id: null,
|
db_id: null,
|
||||||
fontSize: 'default',
|
fontSize: 'default',
|
||||||
wideScreen: false,
|
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 useDatabase = (index) => {
|
||||||
const selected = opts.databases[index]
|
const selected = opts.databases[index]
|
||||||
console.log(selected)
|
console.log(selected)
|
||||||
@ -474,17 +509,24 @@ const loadDatabases = () => {
|
|||||||
|
|
||||||
// 新函数用于处理 fetch 请求
|
// 新函数用于处理 fetch 请求
|
||||||
const fetchChatResponse = (user_input, cur_res_id) => {
|
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/', {
|
fetch('/api/chat/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(params),
|
||||||
query: user_input,
|
|
||||||
history: conv.value.history,
|
|
||||||
meta: meta,
|
|
||||||
cur_res_id: cur_res_id,
|
|
||||||
}),
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
},
|
||||||
|
signal // 添加 signal 用于中断请求
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.body) throw new Error("ReadableStream not supported.");
|
if (!response.body) throw new Error("ReadableStream not supported.");
|
||||||
@ -542,13 +584,24 @@ const fetchChatResponse = (user_input, cur_res_id) => {
|
|||||||
readChunk();
|
readChunk();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
if (error.name === 'AbortError') {
|
||||||
updateMessage({
|
console.log('Fetch aborted');
|
||||||
id: cur_res_id,
|
} else {
|
||||||
status: "error",
|
console.error(error);
|
||||||
});
|
updateMessage({
|
||||||
|
id: cur_res_id,
|
||||||
|
status: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
isStreaming.value = false;
|
isStreaming.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听 isStreaming 变化,当为 false 时中断请求
|
||||||
|
watch(isStreaming, (newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
controller.abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -626,6 +679,36 @@ watch(
|
|||||||
{ deep: true }
|
{ 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 modelNames = computed(() => configStore.config?.model_names)
|
||||||
const modelStatus = computed(() => configStore.config?.model_provider_status)
|
const modelStatus = computed(() => configStore.config?.model_provider_status)
|
||||||
const customModels = computed(() => configStore.config?.custom_models || [])
|
const customModels = computed(() => configStore.config?.custom_models || [])
|
||||||
@ -640,6 +723,13 @@ const selectModel = (provider, name) => {
|
|||||||
configStore.setConfigValue('model_provider', provider)
|
configStore.setConfigValue('model_provider', provider)
|
||||||
configStore.setConfigValue('model_name', name)
|
configStore.setConfigValue('model_name', name)
|
||||||
message.success(`已切换到模型: ${provider}/${name}`)
|
message.success(`已切换到模型: ${provider}/${name}`)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加重新生成方法
|
||||||
|
const regenerateMessage = (message) => {
|
||||||
|
// 找到用户的原始问题
|
||||||
|
retryMessage(message.id)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -889,6 +979,9 @@ const selectModel = (provider, name) => {
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.message-text {
|
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 {
|
.scrollable-menu {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@ -4,7 +4,8 @@
|
|||||||
<!-- <span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span> -->
|
<!-- <span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span> -->
|
||||||
<!-- <span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span> -->
|
<!-- <span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span> -->
|
||||||
<span class="item"><BulbOutlined /> {{ msg.meta.server_model_name }}</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
|
<span
|
||||||
class="item btn"
|
class="item btn"
|
||||||
@click="openSubGraph(msg)"
|
@click="openSubGraph(msg)"
|
||||||
@ -112,15 +113,20 @@ import {
|
|||||||
DeploymentUnitOutlined,
|
DeploymentUnitOutlined,
|
||||||
BulbOutlined,
|
BulbOutlined,
|
||||||
FileOutlined,
|
FileOutlined,
|
||||||
ClockCircleOutlined
|
ClockCircleOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import GraphContainer from './GraphContainer.vue' // 导入 GraphContainer 组件
|
import GraphContainer from './GraphContainer.vue' // 导入 GraphContainer 组件
|
||||||
|
|
||||||
|
|
||||||
|
const emit = defineEmits(['regenerateMessage']);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
message: Object,
|
message: Object,
|
||||||
|
conv: Object,
|
||||||
})
|
})
|
||||||
|
|
||||||
const msg = ref(props.message)
|
const msg = ref(props.message)
|
||||||
|
const conv = ref(props.conv)
|
||||||
|
|
||||||
// 使用 useClipboard 实现复制功能
|
// 使用 useClipboard 实现复制功能
|
||||||
const { copy, isSupported } = useClipboard()
|
const { copy, isSupported } = useClipboard()
|
||||||
@ -210,6 +216,11 @@ const formatDate = (timestamp) => {
|
|||||||
const getPercent = (value) => {
|
const getPercent = (value) => {
|
||||||
return parseFloat((value * 100).toFixed(2))
|
return parseFloat((value * 100).toFixed(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加重新生成方法
|
||||||
|
const regenerateMessage = (message) => {
|
||||||
|
emit('regenerateMessage', message)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user