mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +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="msg-content"
|
||||||
:class="{ 'with-cursor': showCursor }"
|
: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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -272,6 +280,7 @@ import ToolCallSegment from './ToolCallSegment.vue'
|
|||||||
import ThinkingSegment from './ThinkingSegment.vue'
|
import ThinkingSegment from './ThinkingSegment.vue'
|
||||||
import ContentSegment from './ContentSegment.vue'
|
import ContentSegment from './ContentSegment.vue'
|
||||||
import PlanStepsPanel from './PlanStepsPanel.vue'
|
import PlanStepsPanel from './PlanStepsPanel.vue'
|
||||||
|
import UserMessageContent from './UserMessageContent.vue'
|
||||||
import type { BrowserAction } from './BrowserTimeline.vue'
|
import type { BrowserAction } from './BrowserTimeline.vue'
|
||||||
import type { Message, MessageSegment, ChatAttachment, ToolCallMeta, PlanMeta } from '@/types'
|
import type { Message, MessageSegment, ChatAttachment, ToolCallMeta, PlanMeta } from '@/types'
|
||||||
import type { ChatErrorInfo } from '@/types/chatError'
|
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...',
|
thinkingInProgress: 'Thinking...',
|
||||||
stopped: 'Generation stopped',
|
stopped: 'Generation stopped',
|
||||||
interrupted: 'Interrupted',
|
interrupted: 'Interrupted',
|
||||||
|
expandLines: 'Show more ({hidden} more lines)',
|
||||||
|
collapse: 'Show less',
|
||||||
failed: 'Generation failed',
|
failed: 'Generation failed',
|
||||||
compressionSummary: 'Previous conversations summarized',
|
compressionSummary: 'Previous conversations summarized',
|
||||||
compressionWithCount: '{count} previous messages summarized',
|
compressionWithCount: '{count} previous messages summarized',
|
||||||
|
|||||||
@ -53,6 +53,8 @@ export default {
|
|||||||
thinkingInProgress: '思考中...',
|
thinkingInProgress: '思考中...',
|
||||||
stopped: '已停止生成',
|
stopped: '已停止生成',
|
||||||
interrupted: '已中断',
|
interrupted: '已中断',
|
||||||
|
expandLines: '展开(还有 {hidden} 行)',
|
||||||
|
collapse: '收起',
|
||||||
failed: '生成失败',
|
failed: '生成失败',
|
||||||
compressionSummary: '之前的对话已整理为摘要',
|
compressionSummary: '之前的对话已整理为摘要',
|
||||||
compressionWithCount: '之前的 {count} 条对话已整理为摘要',
|
compressionWithCount: '之前的 {count} 条对话已整理为摘要',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user