feat: 增加流式动态渲染
This commit is contained in:
parent
8d259e2d50
commit
d7fe954098
|
|
@ -49,10 +49,7 @@
|
||||||
<div v-if="streamingThinking" class="thinking-content streaming-thinking">
|
<div v-if="streamingThinking" class="thinking-content streaming-thinking">
|
||||||
{{ streamingThinking }}
|
{{ streamingThinking }}
|
||||||
</div>
|
</div>
|
||||||
<div class="message-content streaming-content">
|
<div class="message-content streaming-content" v-html="renderedStreamContent || '<span class=\'placeholder\'>...</span>'"></div>
|
||||||
{{ streamingContent || '...' }}
|
|
||||||
<span class="cursor-blink">|</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -68,9 +65,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import MessageBubble from './MessageBubble.vue'
|
import MessageBubble from './MessageBubble.vue'
|
||||||
import MessageInput from './MessageInput.vue'
|
import MessageInput from './MessageInput.vue'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
|
||||||
|
marked.setOptions({
|
||||||
|
highlight(code, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
return hljs.highlight(code, { language: lang }).value
|
||||||
|
}
|
||||||
|
return hljs.highlightAuto(code).value
|
||||||
|
},
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
conversation: { type: Object, default: null },
|
conversation: { type: Object, default: null },
|
||||||
|
|
@ -87,6 +97,11 @@ defineEmits(['sendMessage', 'deleteMessage', 'toggleSettings', 'loadMoreMessages
|
||||||
const scrollContainer = ref(null)
|
const scrollContainer = ref(null)
|
||||||
const inputRef = ref(null)
|
const inputRef = ref(null)
|
||||||
|
|
||||||
|
const renderedStreamContent = computed(() => {
|
||||||
|
if (!props.streamingContent) return ''
|
||||||
|
return marked.parse(props.streamingContent)
|
||||||
|
})
|
||||||
|
|
||||||
function scrollToBottom(smooth = true) {
|
function scrollToBottom(smooth = true) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const el = scrollContainer.value
|
const el = scrollContainer.value
|
||||||
|
|
@ -142,7 +157,7 @@ defineExpose({ scrollToBottom })
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
background: linear-gradient(135deg, #2563eb, #0ea5e9);
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -193,8 +208,8 @@ defineExpose({ scrollToBottom })
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: rgba(79, 70, 229, 0.15);
|
background: rgba(37, 99, 235, 0.15);
|
||||||
color: #a5b4fc;
|
color: #60a5fa;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,16 +303,75 @@ defineExpose({ scrollToBottom })
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
white-space: pre-wrap;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-blink {
|
.streaming-content :deep(p) {
|
||||||
animation: blink 0.8s infinite;
|
margin: 0 0 8px;
|
||||||
color: #4f46e5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
.streaming-content :deep(p:last-child) {
|
||||||
0%, 50% { opacity: 1; }
|
margin-bottom: 0;
|
||||||
51%, 100% { opacity: 0; }
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(pre) {
|
||||||
|
background: #0d1117;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(pre code) {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(code) {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(pre code) {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(ul),
|
||||||
|
.streaming-content :deep(ol) {
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(blockquote) {
|
||||||
|
border-left: 3px solid rgba(59, 130, 246, 0.5);
|
||||||
|
padding-left: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(table) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 8px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(th),
|
||||||
|
.streaming-content :deep(td) {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(th) {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-content :deep(.placeholder) {
|
||||||
|
color: #475569;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -103,12 +103,12 @@ function copyContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.user .avatar {
|
.user .avatar {
|
||||||
background: linear-gradient(135deg, #4f46e5, #7c3aed);
|
background: linear-gradient(135deg, #2563eb, #0ea5e9);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant .avatar {
|
.assistant .avatar {
|
||||||
background: linear-gradient(135deg, #059669, #10b981);
|
background: linear-gradient(135deg, #0ea5e9, #06b6d4);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,7 +212,7 @@ function copyContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content :deep(blockquote) {
|
.message-content :deep(blockquote) {
|
||||||
border-left: 3px solid rgba(79, 70, 229, 0.5);
|
border-left: 3px solid rgba(59, 130, 246, 0.5);
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
|
|
@ -268,8 +268,8 @@ function copyContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-copy:hover {
|
.btn-copy:hover {
|
||||||
color: #a5b4fc;
|
color: #60a5fa;
|
||||||
background: rgba(165, 180, 252, 0.1);
|
background: rgba(96, 165, 250, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-delete-msg:hover {
|
.btn-delete-msg:hover {
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ defineExpose({ focus })
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-wrapper:focus-within {
|
.input-wrapper:focus-within {
|
||||||
border-color: rgba(79, 70, 229, 0.5);
|
border-color: rgba(37, 99, 235, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
|
|
@ -134,13 +134,13 @@ textarea:disabled {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-send.active {
|
.btn-send.active {
|
||||||
background: #4f46e5;
|
background: #2563eb;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-send.active:hover {
|
.btn-send.active:hover {
|
||||||
background: #6366f1;
|
background: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-hint {
|
.input-hint {
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ function save() {
|
||||||
|
|
||||||
.value-display {
|
.value-display {
|
||||||
float: right;
|
float: right;
|
||||||
color: #a5b4fc;
|
color: #60a5fa;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +223,7 @@ function save() {
|
||||||
.form-group input[type="text"]:focus,
|
.form-group input[type="text"]:focus,
|
||||||
.form-group textarea:focus,
|
.form-group textarea:focus,
|
||||||
.form-group select:focus {
|
.form-group select:focus {
|
||||||
border-color: rgba(79, 70, 229, 0.5);
|
border-color: rgba(37, 99, 235, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group textarea {
|
.form-group textarea {
|
||||||
|
|
@ -264,7 +264,7 @@ function save() {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #4f46e5;
|
background: #2563eb;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 2px solid #0f172a;
|
border: 2px solid #0f172a;
|
||||||
}
|
}
|
||||||
|
|
@ -346,7 +346,7 @@ function save() {
|
||||||
padding: 8px 20px;
|
padding: 8px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: none;
|
border: none;
|
||||||
background: #4f46e5;
|
background: #2563eb;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -354,7 +354,7 @@ function save() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-save:hover {
|
.btn-save:hover {
|
||||||
background: #6366f1;
|
background: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-enter-active,
|
.slide-enter-active,
|
||||||
|
|
|
||||||
|
|
@ -89,10 +89,10 @@ function onContextMenu(e, conv) {
|
||||||
.btn-new {
|
.btn-new {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
background: rgba(79, 70, 229, 0.15);
|
background: rgba(37, 99, 235, 0.15);
|
||||||
border: 1px dashed rgba(79, 70, 229, 0.4);
|
border: 1px dashed rgba(37, 99, 235, 0.4);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
color: #a5b4fc;
|
color: #60a5fa;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -102,8 +102,8 @@ function onContextMenu(e, conv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-new:hover {
|
.btn-new:hover {
|
||||||
background: rgba(79, 70, 229, 0.25);
|
background: rgba(37, 99, 235, 0.25);
|
||||||
border-color: rgba(79, 70, 229, 0.6);
|
border-color: rgba(37, 99, 235, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-new .icon {
|
.btn-new .icon {
|
||||||
|
|
@ -141,7 +141,7 @@ function onContextMenu(e, conv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-item.active {
|
.conversation-item.active {
|
||||||
background: rgba(79, 70, 229, 0.2);
|
background: rgba(37, 99, 235, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.conv-info {
|
.conv-info {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue