|
@@ -9,6 +9,7 @@ import ToolTrace from './ToolTrace.vue'
|
|
|
import TurnTrace from './TurnTrace.vue'
|
|
import TurnTrace from './TurnTrace.vue'
|
|
|
import type { UiInteraction, UiLocalFileLink, UiMessage } from './types'
|
|
import type { UiInteraction, UiLocalFileLink, UiMessage } from './types'
|
|
|
import { buildMessageTimelineRows } from '@/utils/turnTrace'
|
|
import { buildMessageTimelineRows } from '@/utils/turnTrace'
|
|
|
|
|
+import { copyTextToClipboard } from '@/utils/clipboard'
|
|
|
import { displayContentForMessage } from '@/utils/messageDisplay'
|
|
import { displayContentForMessage } from '@/utils/messageDisplay'
|
|
|
import { formatMessageTime } from '@/utils/messageTime'
|
|
import { formatMessageTime } from '@/utils/messageTime'
|
|
|
|
|
|
|
@@ -67,6 +68,7 @@ const scroller = ref<HTMLElement | null>(null)
|
|
|
const pinnedToBottom = ref(true)
|
|
const pinnedToBottom = ref(true)
|
|
|
const showJump = ref(false)
|
|
const showJump = ref(false)
|
|
|
const copiedMessageId = ref('')
|
|
const copiedMessageId = ref('')
|
|
|
|
|
+const copyFailedMessageId = ref('')
|
|
|
const thinkingElapsedMs = ref(0)
|
|
const thinkingElapsedMs = ref(0)
|
|
|
const timelineRows = computed(() => buildMessageTimelineRows(props.messages))
|
|
const timelineRows = computed(() => buildMessageTimelineRows(props.messages))
|
|
|
const showThinkingIndicator = computed(() => {
|
|
const showThinkingIndicator = computed(() => {
|
|
@@ -151,28 +153,13 @@ function scrollToAnchor(messageId: string, anchorId: string): boolean {
|
|
|
|
|
|
|
|
async function copyMessage(message: UiMessage) {
|
|
async function copyMessage(message: UiMessage) {
|
|
|
const text = displayContentForMessage(message.role, message.content)
|
|
const text = displayContentForMessage(message.role, message.content)
|
|
|
- let copied = false
|
|
|
|
|
- try {
|
|
|
|
|
- if (navigator.clipboard?.writeText && window.isSecureContext) {
|
|
|
|
|
- await navigator.clipboard.writeText(text)
|
|
|
|
|
- copied = true
|
|
|
|
|
- }
|
|
|
|
|
- } catch { /* Fall through to the HTTP-compatible browser fallback. */ }
|
|
|
|
|
- if (!copied) {
|
|
|
|
|
- const textarea = document.createElement('textarea')
|
|
|
|
|
- textarea.value = text
|
|
|
|
|
- textarea.setAttribute('readonly', '')
|
|
|
|
|
- textarea.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0'
|
|
|
|
|
- document.body.appendChild(textarea)
|
|
|
|
|
- textarea.select()
|
|
|
|
|
- copied = document.execCommand('copy')
|
|
|
|
|
- textarea.remove()
|
|
|
|
|
- }
|
|
|
|
|
- if (!copied) throw new Error('浏览器拒绝访问剪贴板')
|
|
|
|
|
- copiedMessageId.value = message.id
|
|
|
|
|
|
|
+ const copied = await copyTextToClipboard(text)
|
|
|
|
|
+ copiedMessageId.value = copied ? message.id : ''
|
|
|
|
|
+ copyFailedMessageId.value = copied ? '' : message.id
|
|
|
if (copyResetTimer !== undefined) window.clearTimeout(copyResetTimer)
|
|
if (copyResetTimer !== undefined) window.clearTimeout(copyResetTimer)
|
|
|
copyResetTimer = window.setTimeout(() => {
|
|
copyResetTimer = window.setTimeout(() => {
|
|
|
copiedMessageId.value = ''
|
|
copiedMessageId.value = ''
|
|
|
|
|
+ copyFailedMessageId.value = ''
|
|
|
copyResetTimer = undefined
|
|
copyResetTimer = undefined
|
|
|
}, 1400)
|
|
}, 1400)
|
|
|
}
|
|
}
|
|
@@ -221,7 +208,10 @@ watch(showThinkingIndicator, visible => {
|
|
|
}, { immediate: true })
|
|
}, { immediate: true })
|
|
|
|
|
|
|
|
onMounted(() => nextTick(() => scrollToBottom('auto')))
|
|
onMounted(() => nextTick(() => scrollToBottom('auto')))
|
|
|
-onBeforeUnmount(() => { if (thinkingTimer !== undefined) window.clearInterval(thinkingTimer) })
|
|
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ if (copyResetTimer !== undefined) window.clearTimeout(copyResetTimer)
|
|
|
|
|
+ if (thinkingTimer !== undefined) window.clearInterval(thinkingTimer)
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
|
|
defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
|
|
|
</script>
|
|
</script>
|
|
@@ -358,11 +348,14 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
|
|
|
<time v-if="message.role === 'assistant' && !showAssistantIdentity && message.createdAt" class="message__action-time">{{ formatTime(message.createdAt) }}</time>
|
|
<time v-if="message.role === 'assistant' && !showAssistantIdentity && message.createdAt" class="message__action-time">{{ formatTime(message.createdAt) }}</time>
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
- :class="{ 'message-action--copied': copiedMessageId === message.id }"
|
|
|
|
|
- :title="copiedMessageId === message.id ? '已复制' : '复制'"
|
|
|
|
|
- :aria-label="copiedMessageId === message.id ? '已复制' : '复制消息'"
|
|
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'message-action--copied': copiedMessageId === message.id,
|
|
|
|
|
+ 'message-action--copy-failed': copyFailedMessageId === message.id,
|
|
|
|
|
+ }"
|
|
|
|
|
+ :title="copiedMessageId === message.id ? '已复制' : copyFailedMessageId === message.id ? '复制失败' : '复制'"
|
|
|
|
|
+ :aria-label="copiedMessageId === message.id ? '已复制' : copyFailedMessageId === message.id ? '复制失败' : '复制消息'"
|
|
|
@click="copyMessage(message)"
|
|
@click="copyMessage(message)"
|
|
|
- ><AppIcon :name="copiedMessageId === message.id ? 'check' : 'copy'" :size="13" /></button>
|
|
|
|
|
|
|
+ ><AppIcon :name="copiedMessageId === message.id ? 'check' : copyFailedMessageId === message.id ? 'alert' : 'copy'" :size="13" /></button>
|
|
|
<button type="button" title="引用" aria-label="引用消息" @click="emit('quote', message)"><AppIcon name="quote" :size="13" /></button>
|
|
<button type="button" title="引用" aria-label="引用消息" @click="emit('quote', message)"><AppIcon name="quote" :size="13" /></button>
|
|
|
<button v-if="message.role === 'assistant'" type="button" title="从这里分支" aria-label="从这里分支" @click="emit('branch', message)"><AppIcon name="branch" :size="13" /></button>
|
|
<button v-if="message.role === 'assistant'" type="button" title="从这里分支" aria-label="从这里分支" @click="emit('branch', message)"><AppIcon name="branch" :size="13" /></button>
|
|
|
</div>
|
|
</div>
|
|
@@ -427,7 +420,7 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
|
|
|
.message__attachments { display: flex; margin-top: 8px; flex-wrap: wrap; gap: 7px; }.message__attachments button { display: flex; min-width: 90px; max-width: 210px; min-height: 42px; align-items: center; gap: 7px; padding: 5px 8px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); color: var(--text-secondary); cursor: pointer; }.message__attachments button:hover { border-color: var(--line-strong); }.message__attachments img { width: 48px; height: 48px; margin: -5px 0 -5px -8px; object-fit: cover; }.message__attachments button > span { display: grid; place-items: center; width: 25px; height: 25px; border-radius: 7px; background: var(--surface-soft); }.message__attachments strong { min-width: 0; overflow: hidden; font-size: 10px; font-weight: 520; text-overflow: ellipsis; white-space: nowrap; }
|
|
.message__attachments { display: flex; margin-top: 8px; flex-wrap: wrap; gap: 7px; }.message__attachments button { display: flex; min-width: 90px; max-width: 210px; min-height: 42px; align-items: center; gap: 7px; padding: 5px 8px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); color: var(--text-secondary); cursor: pointer; }.message__attachments button:hover { border-color: var(--line-strong); }.message__attachments img { width: 48px; height: 48px; margin: -5px 0 -5px -8px; object-fit: cover; }.message__attachments button > span { display: grid; place-items: center; width: 25px; height: 25px; border-radius: 7px; background: var(--surface-soft); }.message__attachments strong { min-width: 0; overflow: hidden; font-size: 10px; font-weight: 520; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
.message--user .message__attachments { margin: 0 0 9px; }.message--user .message__attachments button { width: 100%; max-width: none; min-height: 50px; padding: 7px 9px; border: 0; border-radius: 11px; background: rgba(255,255,255,.48); color: var(--text-primary); }.message--user .message__attachments button > span { width: 28px; height: 28px; background: rgba(255,255,255,.65); }.message--user .message__attachments strong { font-size: 11px; font-weight: 560; }.dark .message--user .message__attachments button { background: rgba(255,255,255,.08); }.message--user .message__attachments button.message__attachment--image { display: block; width: auto; min-width: 0; max-width: min(360px, 100%); min-height: 0; padding: 0; border: 0; border-radius: 8px; background: transparent; }.message--user .message__attachments button.message__attachment--image img { display: block; width: auto; height: auto; max-width: min(360px, 100%); max-height: 420px; margin: 0; border-radius: inherit; object-fit: contain; }
|
|
.message--user .message__attachments { margin: 0 0 9px; }.message--user .message__attachments button { width: 100%; max-width: none; min-height: 50px; padding: 7px 9px; border: 0; border-radius: 11px; background: rgba(255,255,255,.48); color: var(--text-primary); }.message--user .message__attachments button > span { width: 28px; height: 28px; background: rgba(255,255,255,.65); }.message--user .message__attachments strong { font-size: 11px; font-weight: 560; }.dark .message--user .message__attachments button { background: rgba(255,255,255,.08); }.message--user .message__attachments button.message__attachment--image { display: block; width: auto; min-width: 0; max-width: min(360px, 100%); min-height: 0; padding: 0; border: 0; border-radius: 8px; background: transparent; }.message--user .message__attachments button.message__attachment--image img { display: block; width: auto; height: auto; max-width: min(360px, 100%); max-height: 420px; margin: 0; border-radius: inherit; object-fit: contain; }
|
|
|
.message__tools { margin-top: 9px; }.message__error { display: flex; align-items: center; gap: 5px; margin: 7px 0 0; color: var(--danger); font-size: 9px; }
|
|
.message__tools { margin-top: 9px; }.message__error { display: flex; align-items: center; gap: 5px; margin: 7px 0 0; color: var(--danger); font-size: 9px; }
|
|
|
-.message__actions { display: flex; position: absolute; left: -3px; top: 100%; gap: 2px; padding-top: 2px; opacity: 0; transition: opacity 120ms ease; }.message:hover .message__actions, .message:focus-within .message__actions { opacity: 1; }.message--user .message__actions { right: 0; left: auto; }.message__actions button { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 7px; background: transparent; color: var(--text-muted); cursor: pointer; }.message__actions button:hover { background: var(--surface-soft); color: var(--text-primary); }.message__actions .message-action--copied { color: var(--success); }
|
|
|
|
|
|
|
+.message__actions { display: flex; position: absolute; left: -3px; top: 100%; gap: 2px; padding-top: 2px; opacity: 0; transition: opacity 120ms ease; }.message:hover .message__actions, .message:focus-within .message__actions { opacity: 1; }.message--user .message__actions { right: 0; left: auto; }.message__actions button { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 7px; background: transparent; color: var(--text-muted); cursor: pointer; }.message__actions button:hover { background: var(--surface-soft); color: var(--text-primary); }.message__actions .message-action--copied { color: var(--success); }.message__actions .message-action--copy-failed { color: var(--danger); }
|
|
|
.message--assistant-anonymous .message__actions { align-items: center; }.message__action-time { min-width: 36px; padding: 0 4px 0 3px; color: var(--text-muted); font-size: 9px; font-variant-numeric: tabular-nums; }
|
|
.message--assistant-anonymous .message__actions { align-items: center; }.message__action-time { min-width: 36px; padding: 0 4px 0 3px; color: var(--text-muted); font-size: 9px; font-variant-numeric: tabular-nums; }
|
|
|
.message--failed .message__body { border-color: color-mix(in srgb, var(--danger) 35%, var(--line)); }
|
|
.message--failed .message__body { border-color: color-mix(in srgb, var(--danger) 35%, var(--line)); }
|
|
|
.message--revealed .message__body { animation: reveal-message 1.8s ease; }
|
|
.message--revealed .message__body { animation: reveal-message 1.8s ease; }
|