Merge branch 'main' into agent-demo

This commit is contained in:
Wenjie Zhang 2025-03-30 20:46:26 +08:00
commit cde6b98336
5 changed files with 157 additions and 29 deletions

View File

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

View File

@ -77,6 +77,7 @@
<div class="chat-box" :class="{ 'wide-screen': meta.wideScreen, 'font-smaller': meta.fontSize === 'smaller', 'font-larger': meta.fontSize === 'larger' }"> <div class="chat-box" :class="{ 'wide-screen': meta.wideScreen, 'font-smaller': meta.fontSize === 'smaller', 'font-larger': meta.fontSize === 'larger' }">
<MessageComponent <MessageComponent
v-for="message in conv.messages" v-for="message in conv.messages"
:message="message"
:key="message.id" :key="message.id"
:role="message.role" :role="message.role"
:content="message.text" :content="message.text"
@ -87,9 +88,10 @@
:is-processing="isStreaming" :is-processing="isStreaming"
:error-message="message.message" :error-message="message.message"
@retry="retryMessage(message.id)" @retry="retryMessage(message.id)"
@retryStoppedMessage="retryStoppedMessage(message.id)"
> >
<template #refs v-if="message.role=='received' && message.status=='finished'"> <template #refs v-if="message.role=='received' && message.status=='finished'">
<RefsComponent :message="message" /> <RefsComponent :message="message" :conv="conv" @retry="retryMessage(message.id)" />
</template> </template>
</MessageComponent> </MessageComponent>
</div> </div>
@ -100,7 +102,7 @@
:is-loading="isStreaming" :is-loading="isStreaming"
:send-button-disabled="!conv.inputText && !isStreaming" :send-button-disabled="!conv.inputText && !isStreaming"
:auto-size="{ minRows: 2, maxRows: 10 }" :auto-size="{ minRows: 2, maxRows: 10 }"
@send="sendMessage" @send="handleSendOrStop"
@keydown="handleKeyDown" @keydown="handleKeyDown"
> >
<template #options-left> <template #options-left>
@ -171,6 +173,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';
@ -221,7 +226,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,
@ -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 useDatabase = (index) => {
const selected = opts.databases[index] const selected = opts.databases[index]
console.log(selected) console.log(selected)
@ -441,18 +464,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,
thread_id: conv.value.id.toString(),
}),
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.");
@ -510,13 +539,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();
}
});
} }
@ -606,6 +646,37 @@ 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 = (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 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 || [])
@ -620,6 +691,7 @@ 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}`)
} }
</script> </script>

View File

@ -41,6 +41,11 @@
<!-- 消息内容 --> <!-- 消息内容 -->
<div v-else-if="contentHtml" v-html="contentHtml" class="message-md"></div> <div v-else-if="contentHtml" v-html="contentHtml" class="message-md"></div>
<div v-if="message.isStoppedByUser" class="retry-hint">
你停止生成了本次回答
<span class="retry-link" @click="emit('retryStoppedMessage', message.id)">重新编辑问题</span>
</div>
<!-- 工具调用 (AgentView特有) --> <!-- 工具调用 (AgentView特有) -->
<slot name="tool-calls"></slot> <slot name="tool-calls"></slot>
@ -60,6 +65,10 @@ import { CaretRightOutlined } from '@ant-design/icons-vue';
const props = defineProps({ const props = defineProps({
// 'user'|'assistant'|'sent'|'received' // 'user'|'assistant'|'sent'|'received'
message: {
type: Object,
required: true
},
role: { role: {
type: String, type: String,
required: true required: true
@ -114,7 +123,7 @@ const statusDefination = {
error: '错误' error: '错误'
} }
const emit = defineEmits(['retry']); const emit = defineEmits(['retry', 'retryStoppedMessage']);
// //
const reasoningActiveKey = ref(['show']); 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 { .loading-dots {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@ -15,11 +15,17 @@
<slot name="options-left"></slot> <slot name="options-left"></slot>
</div> </div>
<div class="options__right"> <div class="options__right">
<a-button @click="handleSend" :disabled="sendButtonDisabled" type="link"> <a-tooltip :title="isLoading ? '停止回答' : ''">
<a-button
@click="handleSendOrStop"
:disabled="sendButtonDisabled"
type="link"
>
<template #icon> <template #icon>
<component :is="getIcon" /> <component :is="getIcon" class="send-btn"/>
</template> </template>
</a-button> </a-button>
</a-tooltip>
</div> </div>
</div> </div>
</div> </div>
@ -30,7 +36,8 @@ import { ref, computed, toRefs } from 'vue';
import { import {
SendOutlined, SendOutlined,
ArrowUpOutlined, ArrowUpOutlined,
LoadingOutlined LoadingOutlined,
PauseOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
const props = defineProps({ const props = defineProps({
@ -74,13 +81,13 @@ const emit = defineEmits(['update:modelValue', 'send', 'keydown']);
const iconComponents = { const iconComponents = {
'SendOutlined': SendOutlined, 'SendOutlined': SendOutlined,
'ArrowUpOutlined': ArrowUpOutlined, 'ArrowUpOutlined': ArrowUpOutlined,
'LoadingOutlined': LoadingOutlined 'PauseOutlined': PauseOutlined
}; };
// //
const getIcon = computed(() => { const getIcon = computed(() => {
if (props.isLoading) { if (props.isLoading) {
return LoadingOutlined; return PauseOutlined;
} }
return iconComponents[props.sendIcon] || ArrowUpOutlined; return iconComponents[props.sendIcon] || ArrowUpOutlined;
}); });
@ -97,7 +104,7 @@ const handleKeyPress = (e) => {
}; };
// //
const handleSend = () => { const handleSendOrStop = () => {
emit('send'); emit('send');
}; };
</script> </script>
@ -218,6 +225,7 @@ button.ant-btn-icon-only {
&:hover { &:hover {
background-color: var(--main-600); background-color: var(--main-600);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
color: white;
} }
&:active { &:active {

View File

@ -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()" title="重新生成"><ReloadOutlined /></span>
<span <span
class="item btn" class="item btn"
@click="openSubGraph(msg)" @click="openSubGraph(msg)"
@ -107,20 +108,23 @@ import {
GlobalOutlined, GlobalOutlined,
FileTextOutlined, FileTextOutlined,
CopyOutlined, CopyOutlined,
LikeOutlined,
DislikeOutlined,
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(['retry']);
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 +214,11 @@ const formatDate = (timestamp) => {
const getPercent = (value) => { const getPercent = (value) => {
return parseFloat((value * 100).toFixed(2)) return parseFloat((value * 100).toFixed(2))
} }
//
const regenerateMessage = () => {
emit('retry')
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>