mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(ui): plain-text user messages with 8-line auto-collapse
Pasted prompts (test cases, structured asks, JSON dumps) currently render through the same markdown pipeline as assistant output, so '#'/'-'/'**' characters are processed and long prompts dominate the scrollback. - New UserMessageContent.vue: plain-text rendering (white-space: pre-wrap preserves user-typed newlines and indentation), with auto-collapse beyond 8 lines and a "Show more (N more lines) / Show less" toggle. Soft mask gradient at the collapse boundary instead of a hard cut. - MessageBubble.vue: route role==='user' messages through the new component; assistant messages keep the existing markdown pipeline unchanged. - Add chat.expandLines / chat.collapse i18n keys (zh + en). Verified end-to-end in browser preview: 15-line content collapses to 8, toggle expands to full 15 with "Show less" label, raw '#' / '**' / '`' chars shown literally with no <strong>/<h1>/<li> tags emitted.
This commit is contained in:
parent
9c8c393b3c
commit
980b16109d
@ -124,8 +124,16 @@
|
||||
class="msg-content"
|
||||
:class="{ 'with-cursor': showCursor }"
|
||||
>
|
||||
<div class="markdown-body" v-html="renderedContent"></div>
|
||||
<TypingCursor v-if="showCursor" :typing="isGenerating" />
|
||||
<!--
|
||||
User-authored messages render as plain text (no markdown) and
|
||||
auto-collapse beyond 8 lines. Assistant content goes through the
|
||||
normal markdown pipeline.
|
||||
-->
|
||||
<UserMessageContent v-if="role === 'user'" :content="displayContent" />
|
||||
<template v-else>
|
||||
<div class="markdown-body" v-html="renderedContent"></div>
|
||||
<TypingCursor v-if="showCursor" :typing="isGenerating" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
@ -272,6 +280,7 @@ import ToolCallSegment from './ToolCallSegment.vue'
|
||||
import ThinkingSegment from './ThinkingSegment.vue'
|
||||
import ContentSegment from './ContentSegment.vue'
|
||||
import PlanStepsPanel from './PlanStepsPanel.vue'
|
||||
import UserMessageContent from './UserMessageContent.vue'
|
||||
import type { BrowserAction } from './BrowserTimeline.vue'
|
||||
import type { Message, MessageSegment, ChatAttachment, ToolCallMeta, PlanMeta } from '@/types'
|
||||
import type { ChatErrorInfo } from '@/types/chatError'
|
||||
|
||||
118
mateclaw-ui/src/components/chat/UserMessageContent.vue
Normal file
118
mateclaw-ui/src/components/chat/UserMessageContent.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue'
|
||||
|
||||
/**
|
||||
* Renders a user-authored message as plain text (no markdown processing) and
|
||||
* auto-collapses content longer than {@link COLLAPSE_LINE_THRESHOLD} lines.
|
||||
*
|
||||
* Why no markdown: user input is what the user typed, not a render directive.
|
||||
* Markdown rendering surprises people who paste prompts containing #/`/-, and
|
||||
* makes the bubble visually compete with assistant output (which IS markdown).
|
||||
*
|
||||
* Why 8 lines: long pasted prompts (test cases, structured asks, JSON dumps)
|
||||
* dominate the scrollback otherwise. 8 lines fits the typical 1-2 paragraph
|
||||
* ask without truncating.
|
||||
*/
|
||||
|
||||
const props = defineProps<{
|
||||
content: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const COLLAPSE_LINE_THRESHOLD = 8
|
||||
|
||||
const lines = computed(() => (props.content ?? '').split('\n'))
|
||||
const lineCount = computed(() => lines.value.length)
|
||||
const needsCollapse = computed(() => lineCount.value > COLLAPSE_LINE_THRESHOLD)
|
||||
|
||||
const expanded = ref(false)
|
||||
|
||||
const displayText = computed(() => {
|
||||
if (!needsCollapse.value || expanded.value) return props.content ?? ''
|
||||
return lines.value.slice(0, COLLAPSE_LINE_THRESHOLD).join('\n')
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-msg-plain">
|
||||
<div class="user-msg-plain__text" :class="{ 'is-collapsed': needsCollapse && !expanded }">{{ displayText }}</div>
|
||||
|
||||
<button
|
||||
v-if="needsCollapse"
|
||||
type="button"
|
||||
class="user-msg-plain__toggle"
|
||||
@click="toggle"
|
||||
>
|
||||
<span class="user-msg-plain__toggle-label">
|
||||
{{ expanded ? t('chat.collapse') : t('chat.expandLines', { hidden: lineCount - COLLAPSE_LINE_THRESHOLD }) }}
|
||||
</span>
|
||||
<el-icon class="user-msg-plain__toggle-icon">
|
||||
<component :is="expanded ? ArrowUp : ArrowDown" />
|
||||
</el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-msg-plain {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/*
|
||||
* white-space: pre-wrap preserves the user's newlines and indentation
|
||||
* (important for pasted prompts with structure) while still wrapping at the
|
||||
* bubble's right edge. word-break:break-word keeps long URLs / paths from
|
||||
* overflowing.
|
||||
*/
|
||||
.user-msg-plain__text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
color: inherit;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.user-msg-plain__text.is-collapsed {
|
||||
/*
|
||||
* Soft bottom fade so the collapse boundary doesn't look like a hard cut.
|
||||
* Uses currentColor so the gradient inherits whatever bubble color is in
|
||||
* effect (works on the orange user-bubble and on dark mode alike).
|
||||
*/
|
||||
position: relative;
|
||||
-webkit-mask-image: linear-gradient(to bottom, currentColor 70%, rgba(0, 0, 0, 0.35) 100%);
|
||||
mask-image: linear-gradient(to bottom, currentColor 70%, rgba(0, 0, 0, 0.35) 100%);
|
||||
}
|
||||
|
||||
.user-msg-plain__toggle {
|
||||
align-self: flex-end;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
color: inherit;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.user-msg-plain__toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
.user-msg-plain__toggle-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -53,6 +53,8 @@ export default {
|
||||
thinkingInProgress: 'Thinking...',
|
||||
stopped: 'Generation stopped',
|
||||
interrupted: 'Interrupted',
|
||||
expandLines: 'Show more ({hidden} more lines)',
|
||||
collapse: 'Show less',
|
||||
failed: 'Generation failed',
|
||||
compressionSummary: 'Previous conversations summarized',
|
||||
compressionWithCount: '{count} previous messages summarized',
|
||||
|
||||
@ -53,6 +53,8 @@ export default {
|
||||
thinkingInProgress: '思考中...',
|
||||
stopped: '已停止生成',
|
||||
interrupted: '已中断',
|
||||
expandLines: '展开(还有 {hidden} 行)',
|
||||
collapse: '收起',
|
||||
failed: '生成失败',
|
||||
compressionSummary: '之前的对话已整理为摘要',
|
||||
compressionWithCount: '之前的 {count} 条对话已整理为摘要',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user