2026-09-27 21:10:00 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
Copyright 2024-2026 ReplyPier
|
|
|
|
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
|
limitations under the License.
|
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
import { onMounted, watch, ref, computed } from 'vue'
|
|
|
|
|
|
import type { ChatDetail, Message } from '@/types/chat'
|
2026-09-27 21:10:00 +08:00
|
|
|
|
import { useConversationChat } from '@/composables/useConversationChat'
|
|
|
|
|
|
import { useConversationFiles } from '@/composables/useConversationFiles'
|
|
|
|
|
|
import { useVisualViewport } from '@/composables/useVisualViewport'
|
|
|
|
|
|
import { useJiraTicket } from '@/composables/useJiraTicket'
|
|
|
|
|
|
import JiraTicketModal from '@/components/jira/JiraTicketModal.vue'
|
|
|
|
|
|
import TicketCreateModal from '@/components/tickets/TicketCreateModal.vue'
|
|
|
|
|
|
import { ticketService } from '@/services/tickets'
|
|
|
|
|
|
import { permissionChecks } from '@/utils/permissions'
|
|
|
|
|
|
import FileUpload from '@/components/common/FileUpload.vue'
|
|
|
|
|
|
import { userService } from '@/services/user'
|
|
|
|
|
|
import WhatsAppTemplatePicker from '@/components/conversations/WhatsAppTemplatePicker.vue'
|
|
|
|
|
|
import MessageStatusTicks from '@/components/conversations/MessageStatusTicks.vue'
|
|
|
|
|
|
import QuickReplyChips from '@/components/conversations/QuickReplyChips.vue'
|
|
|
|
|
|
import MessageTag from '@/components/conversations/MessageTag.vue'
|
2026-10-22 17:44:36 +08:00
|
|
|
|
import { navIconSvg } from '@/components/layout/navIcons'
|
|
|
|
|
|
import { getInitials } from '@/utils/text'
|
2026-09-27 21:10:00 +08:00
|
|
|
|
import { marked } from 'marked'
|
|
|
|
|
|
import { sanitizeHtml } from '@/utils/sanitize'
|
|
|
|
|
|
import type { Renderer } from 'marked'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
chat: ChatDetail
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
(e: 'refresh'): void
|
|
|
|
|
|
(e: 'chatUpdated', data: ChatDetail): void
|
|
|
|
|
|
(e: 'clearUnread', sessionId: string): void
|
|
|
|
|
|
(e: 'view-product', productId: string): void
|
|
|
|
|
|
(e: 'back'): void
|
|
|
|
|
|
(e: 'info'): void
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
// Create a local ref to track the current chat state
|
|
|
|
|
|
const currentChat = ref(props.chat)
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
newMessage,
|
|
|
|
|
|
messagesContainer,
|
|
|
|
|
|
formattedMessages,
|
|
|
|
|
|
isLoading,
|
|
|
|
|
|
showTakeoverButton,
|
|
|
|
|
|
showTakenOverStatus,
|
|
|
|
|
|
isChatClosed,
|
|
|
|
|
|
canSendMessage,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
handleTakeover,
|
|
|
|
|
|
handleRouteToHuman,
|
|
|
|
|
|
isWaitingForHuman,
|
|
|
|
|
|
handlerCaption,
|
|
|
|
|
|
handledByAI,
|
2026-10-22 17:44:36 +08:00
|
|
|
|
replaceChatFromProps,
|
2026-09-27 21:10:00 +08:00
|
|
|
|
templateCanReopen,
|
|
|
|
|
|
clearTemplateSuggestion
|
|
|
|
|
|
} = useConversationChat(props.chat, emit)
|
|
|
|
|
|
|
|
|
|
|
|
// Templates are a WhatsApp-only way back into a conversation whose 24h window
|
|
|
|
|
|
// closed, and they need the account the conversation arrived on.
|
|
|
|
|
|
const showTemplatePicker = ref(false)
|
|
|
|
|
|
const canUseTemplates = computed(
|
|
|
|
|
|
() => currentChat.value.channel === 'whatsapp' && !!currentChat.value.channel_account_id
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const handleTemplateSent = () => {
|
|
|
|
|
|
showTemplatePicker.value = false
|
|
|
|
|
|
clearTemplateSuggestion()
|
|
|
|
|
|
emit('refresh')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add file handling functionality
|
|
|
|
|
|
const {
|
|
|
|
|
|
fileUploadRef,
|
|
|
|
|
|
uploadedFiles,
|
|
|
|
|
|
handleFilesUploaded,
|
|
|
|
|
|
handleFileUploadError,
|
|
|
|
|
|
handleChatPaste,
|
|
|
|
|
|
handleSendMessageWithAttachments,
|
|
|
|
|
|
formatFileSize,
|
|
|
|
|
|
isImageAttachment,
|
|
|
|
|
|
getDownloadUrl,
|
|
|
|
|
|
getImageUrl
|
|
|
|
|
|
} = useConversationFiles(currentChat, newMessage, canSendMessage, scrollToBottom)
|
|
|
|
|
|
|
|
|
|
|
|
// Add Jira ticket functionality
|
|
|
|
|
|
const {
|
|
|
|
|
|
jiraConnected,
|
|
|
|
|
|
checkJiraStatus
|
|
|
|
|
|
} = useJiraTicket()
|
|
|
|
|
|
|
|
|
|
|
|
// State for Jira ticket modal
|
|
|
|
|
|
const showJiraTicketModal = ref(false)
|
|
|
|
|
|
const ticketSummary = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
// Native ticketing: preferred over Jira when the user can manage tickets.
|
|
|
|
|
|
// One native ticket per session — the button flips to "View ticket".
|
|
|
|
|
|
const showTicketModal = ref(false)
|
|
|
|
|
|
const canUseNativeTickets = permissionChecks.canManageTickets()
|
|
|
|
|
|
const linkedTicketId = ref<string | null>(null)
|
|
|
|
|
|
const showTicketMenu = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshLinkedTicket() {
|
|
|
|
|
|
if (!canUseNativeTickets || !currentChat.value?.session_id) return
|
|
|
|
|
|
const ticket = await ticketService.getTicketBySession(currentChat.value.session_id)
|
|
|
|
|
|
linkedTicketId.value = ticket?.id || null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add marked configuration
|
|
|
|
|
|
marked.setOptions({
|
|
|
|
|
|
renderer: new marked.Renderer(),
|
|
|
|
|
|
gfm: true,
|
|
|
|
|
|
breaks: true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Configure marked renderer to add target="_blank" to links
|
|
|
|
|
|
const renderer = new marked.Renderer() as Renderer
|
|
|
|
|
|
renderer.link = function({ href, title, text }) {
|
|
|
|
|
|
if (!href) return text || ''
|
|
|
|
|
|
const link = `<a href="${href}"${title ? ` title="${title}"` : ''}>${text}</a>`
|
|
|
|
|
|
return link.replace(/^<a /, '<a target="_blank" rel="nofollow" ')
|
|
|
|
|
|
}
|
|
|
|
|
|
marked.use({ renderer })
|
|
|
|
|
|
|
|
|
|
|
|
// Create helper function to render and sanitize markdown
|
|
|
|
|
|
const renderMarkdown = (text: string) => {
|
|
|
|
|
|
return sanitizeHtml(marked.parse(text, { async: false }) as string)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Computed property to determine if the current user can create a ticket
|
|
|
|
|
|
const canCreateTicket = computed(() => {
|
|
|
|
|
|
// Can only create ticket if:
|
|
|
|
|
|
// 1. User can send messages (already handled by canSendMessage)
|
|
|
|
|
|
// 2. Chat is not closed
|
|
|
|
|
|
// 3. Current user is the one who took over the chat
|
|
|
|
|
|
return canSendMessage.value &&
|
|
|
|
|
|
!isChatClosed.value &&
|
|
|
|
|
|
currentChat.value.user_id === userService.getUserId();
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Function to handle create ticket
|
|
|
|
|
|
const handleCreateTicket = async () => {
|
|
|
|
|
|
// Get a summary from the last few messages
|
|
|
|
|
|
const lastMessages = formattedMessages.value.slice(-3)
|
|
|
|
|
|
const summary = lastMessages.map(m => m.message).join(' ').substring(0, 100) + '...'
|
|
|
|
|
|
|
|
|
|
|
|
ticketSummary.value = summary
|
|
|
|
|
|
showJiraTicketModal.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Function to handle ticket created
|
|
|
|
|
|
const handleTicketCreated = (ticketKey: string) => {
|
|
|
|
|
|
// Add ticket key to message input
|
|
|
|
|
|
newMessage.value = `Jira 工单已创建:${ticketKey}`
|
|
|
|
|
|
// Close the modal
|
|
|
|
|
|
showJiraTicketModal.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
// ── Thread presentation ─────────────────────────────────────────────────
|
|
|
|
|
|
const currentUserName = userService.getUserName() || '人工客服'
|
|
|
|
|
|
|
|
|
|
|
|
const customerName = computed(
|
|
|
|
|
|
() => props.chat.customer.full_name || props.chat.customer.email || '访客'
|
|
|
|
|
|
)
|
|
|
|
|
|
const customerSubline = computed(() => {
|
|
|
|
|
|
const email = props.chat.customer.email
|
|
|
|
|
|
if (props.chat.customer.full_name) return email || '未留下邮箱'
|
|
|
|
|
|
return email || '匿名访客 · 未留下姓名与邮箱'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const CHANNEL_LABELS: Record<string, string> = {
|
|
|
|
|
|
web: '网页组件',
|
|
|
|
|
|
shopify: 'Shopify',
|
|
|
|
|
|
whatsapp: 'WhatsApp',
|
|
|
|
|
|
messenger: 'Messenger',
|
|
|
|
|
|
instagram: 'Instagram',
|
|
|
|
|
|
telegram: 'Telegram',
|
|
|
|
|
|
slack: 'Slack',
|
|
|
|
|
|
email: '邮件',
|
|
|
|
|
|
sms: '短信',
|
|
|
|
|
|
line: 'LINE',
|
|
|
|
|
|
api: 'API'
|
|
|
|
|
|
}
|
|
|
|
|
|
const channelLabel = (channel?: string | null) =>
|
|
|
|
|
|
(channel && CHANNEL_LABELS[channel]) || channel || '网页组件'
|
|
|
|
|
|
|
|
|
|
|
|
const formatClock = (iso: string) => {
|
|
|
|
|
|
const date = new Date(iso)
|
|
|
|
|
|
if (Number.isNaN(date.getTime())) return ''
|
|
|
|
|
|
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dayLabel = (iso: string) => {
|
|
|
|
|
|
const date = new Date(iso)
|
|
|
|
|
|
const now = new Date()
|
|
|
|
|
|
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
|
|
|
|
const startOfMessageDay = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
|
|
|
|
|
const dayDiff = Math.round(
|
|
|
|
|
|
(startOfToday.getTime() - startOfMessageDay.getTime()) / 86400000
|
|
|
|
|
|
)
|
|
|
|
|
|
const formatted = new Intl.DateTimeFormat('zh-CN', {
|
|
|
|
|
|
month: 'long',
|
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
|
}).format(date)
|
|
|
|
|
|
|
|
|
|
|
|
if (dayDiff === 0) return `今天 · ${formatted}`
|
|
|
|
|
|
if (dayDiff === 1) return `昨天 · ${formatted}`
|
|
|
|
|
|
return formatted
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Who authored a bubble. The backend keeps the visitor / AI / human split in
|
|
|
|
|
|
// message_type, which is what the three designs key off.
|
|
|
|
|
|
type MessageKind = 'visitor' | 'ai' | 'human'
|
|
|
|
|
|
const messageKind = (type?: string): MessageKind => {
|
|
|
|
|
|
if (type === 'user') return 'visitor'
|
|
|
|
|
|
if (type === 'agent') return 'human'
|
|
|
|
|
|
return 'ai'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isOwnMessage = (message: Message) =>
|
|
|
|
|
|
!!message.user_name && message.user_name === currentUserName
|
|
|
|
|
|
|
|
|
|
|
|
const whoLabel = (message: Message) => {
|
|
|
|
|
|
if (message.message_type === 'agent') {
|
|
|
|
|
|
return isOwnMessage(message)
|
|
|
|
|
|
? '你 · 人工客服'
|
|
|
|
|
|
: `${message.user_name || '客服'} · 人工客服`
|
|
|
|
|
|
}
|
|
|
|
|
|
return `AI · ${message.agent_name || props.chat.agent.name || '客服'}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const avatarText = (message: Message) => {
|
|
|
|
|
|
if (messageKind(message.message_type) === 'human') {
|
|
|
|
|
|
return isOwnMessage(message) ? '你' : getInitials(message.user_name, '客')
|
|
|
|
|
|
}
|
|
|
|
|
|
return getInitials(customerName.value, '访')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const avatarTone = (message: Message) => {
|
|
|
|
|
|
const kind = messageKind(message.message_type)
|
|
|
|
|
|
return kind === 'human' ? 'ink' : kind === 'ai' ? 'celadon' : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isEndEvent = (message: Message) =>
|
|
|
|
|
|
Boolean(message.end_chat || message.attributes?.end_chat)
|
|
|
|
|
|
|
|
|
|
|
|
// Hand-off notices are stored as ordinary agent/bot rows with this flag, not as
|
|
|
|
|
|
// message_type 'system' — they belong on the rule, not in a bubble.
|
|
|
|
|
|
const isHandoverEvent = (message: Message) =>
|
|
|
|
|
|
Boolean(message.attributes?.handover_notice)
|
|
|
|
|
|
|
|
|
|
|
|
const eventIcon = (message: Message) =>
|
|
|
|
|
|
isEndEvent(message) ? 'lock' : isHandoverEvent(message) ? 'headset' : 'clock'
|
|
|
|
|
|
|
|
|
|
|
|
// One stream that interleaves day dividers, system events and bubbles. System
|
|
|
|
|
|
// messages are the backend's own record of a hand-off or a close, so they get
|
|
|
|
|
|
// the rule-and-pill treatment instead of a bubble.
|
|
|
|
|
|
interface FormattedMessage extends Message {
|
|
|
|
|
|
timeAgo: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface DayItem {
|
|
|
|
|
|
key: string
|
|
|
|
|
|
kind: 'day'
|
|
|
|
|
|
label: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface MessageItem {
|
|
|
|
|
|
key: string
|
|
|
|
|
|
kind: 'event' | MessageKind
|
|
|
|
|
|
message: FormattedMessage
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type StreamItem = DayItem | MessageItem
|
|
|
|
|
|
|
|
|
|
|
|
const streamItems = computed<StreamItem[]>(() => {
|
|
|
|
|
|
const items: StreamItem[] = []
|
|
|
|
|
|
let lastDay = ''
|
|
|
|
|
|
|
|
|
|
|
|
formattedMessages.value.forEach((message, index) => {
|
|
|
|
|
|
const dayKey = new Date(message.created_at).toDateString()
|
|
|
|
|
|
if (dayKey !== lastDay) {
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
key: `day-${dayKey}`,
|
|
|
|
|
|
kind: 'day',
|
|
|
|
|
|
label: dayLabel(message.created_at)
|
|
|
|
|
|
})
|
|
|
|
|
|
lastDay = dayKey
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (message.message_type === 'system' || isHandoverEvent(message)) {
|
|
|
|
|
|
items.push({ key: `message-${index}`, kind: 'event', message })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-09-27 21:10:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
items.push({
|
|
|
|
|
|
key: `message-${index}`,
|
|
|
|
|
|
kind: messageKind(message.message_type),
|
|
|
|
|
|
message
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return items
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const composerPlaceholder = computed(
|
|
|
|
|
|
() => `回复 ${customerName.value}… 支持粘贴截图`
|
|
|
|
|
|
)
|
2026-09-27 21:10:00 +08:00
|
|
|
|
|
|
|
|
|
|
// Keep the composer above the on-screen keyboard on installed iOS PWAs, where
|
|
|
|
|
|
// the keyboard overlays the viewport instead of resizing it
|
|
|
|
|
|
useVisualViewport(() => scrollToBottom())
|
|
|
|
|
|
|
|
|
|
|
|
// Watch for chat changes and update the internal state
|
|
|
|
|
|
watch(() => props.chat, (newChat) => {
|
|
|
|
|
|
if (newChat) {
|
|
|
|
|
|
// Only sync local state to avoid emitting events back to parent
|
|
|
|
|
|
currentChat.value = newChat
|
|
|
|
|
|
replaceChatFromProps(newChat)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, { immediate: true })
|
|
|
|
|
|
|
|
|
|
|
|
// Check Jira status on mount
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
await checkJiraStatus()
|
|
|
|
|
|
await refreshLinkedTicket()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
watch(() => currentChat.value?.session_id, () => refreshLinkedTicket())
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="chat-layout">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<header class="thread-head">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="icon-btn sm back-btn"
|
|
|
|
|
|
aria-label="返回会话列表"
|
|
|
|
|
|
@click="emit('back')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="back-glyph" v-html="navIconSvg('chevronRight', 18)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</button>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="thread-who">
|
|
|
|
|
|
<div class="thread-name">
|
|
|
|
|
|
<h2>{{ customerName }}</h2>
|
|
|
|
|
|
<span v-if="handledByAI" class="chip chip-ai"><i class="d"></i>AI 接待中</span>
|
|
|
|
|
|
<span v-else-if="isWaitingForHuman" class="chip chip-wait"><i class="d"></i>等待人工</span>
|
|
|
|
|
|
<span v-else-if="showTakenOverStatus" class="chip">
|
|
|
|
|
|
<span class="chip-glyph" v-html="navIconSvg('headset', 12)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
由 {{ chat.user_name || '其他客服' }} 接管
|
|
|
|
|
|
</span>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span v-else-if="isChatClosed" class="chip chip-quiet">已结束</span>
|
|
|
|
|
|
<span v-else class="chip chip-human">人工接待中 · 你</span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="thread-meta">
|
|
|
|
|
|
<span>{{ customerSubline }}</span>
|
|
|
|
|
|
<i></i>
|
|
|
|
|
|
<span>{{ channelLabel(chat.channel) }}</span>
|
|
|
|
|
|
<i></i>
|
|
|
|
|
|
<span>{{ chat.agent.name || 'AI 客服' }}</span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="thread-actions">
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<!-- The thread only streams once a human takes the chat over; while the
|
|
|
|
|
|
AI is answering this is how you pull the latest turns. -->
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<button
|
|
|
|
|
|
class="icon-btn sm"
|
|
|
|
|
|
title="刷新会话"
|
|
|
|
|
|
aria-label="刷新会话"
|
|
|
|
|
|
@click="emit('refresh')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span v-html="navIconSvg('refresh', 16)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</button>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<button
|
|
|
|
|
|
v-if="canUseTemplates && !isChatClosed"
|
2026-10-22 17:44:36 +08:00
|
|
|
|
class="btn btn-ghost btn-sm"
|
2026-09-27 21:10:00 +08:00
|
|
|
|
title="发送模板"
|
|
|
|
|
|
aria-label="发送模板"
|
|
|
|
|
|
@click="showTemplatePicker = true"
|
|
|
|
|
|
>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span class="btn-glyph" v-html="navIconSvg('message', 15)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<span class="btn-label">发送模板</span>
|
|
|
|
|
|
</button>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<template v-if="canCreateTicket">
|
|
|
|
|
|
<!-- A native ticket already exists for this conversation -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="linkedTicketId"
|
2026-10-22 17:44:36 +08:00
|
|
|
|
class="btn btn-ghost btn-sm"
|
2026-09-27 21:10:00 +08:00
|
|
|
|
@click="$router.push(`/tickets/${linkedTicketId}`)"
|
|
|
|
|
|
>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span class="btn-glyph" v-html="navIconSvg('ticket', 15)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<span class="btn-label">查看工单</span>
|
|
|
|
|
|
</button>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<!-- Jira connected: keep the familiar Jira flow primary, native in
|
|
|
|
|
|
the dropdown (orgs that configured Jira keep their board). -->
|
|
|
|
|
|
<div v-else-if="jiraConnected" class="ticket-btn-group">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<button class="btn btn-ghost btn-sm" @click="handleCreateTicket">
|
|
|
|
|
|
<span class="btn-glyph" v-html="navIconSvg('ticket', 15)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<span class="btn-label">创建工单</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="canUseNativeTickets"
|
|
|
|
|
|
class="ticket-menu-toggle"
|
|
|
|
|
|
title="更多工单选项"
|
2026-10-22 17:44:36 +08:00
|
|
|
|
aria-label="更多工单选项"
|
2026-09-27 21:10:00 +08:00
|
|
|
|
@click="showTicketMenu = !showTicketMenu"
|
|
|
|
|
|
>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span v-html="navIconSvg('chevronDown', 14)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<div v-if="showTicketMenu" class="ticket-menu">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="ticket-menu-item"
|
|
|
|
|
|
@click="showTicketMenu = false; showTicketModal = true"
|
|
|
|
|
|
>
|
|
|
|
|
|
创建 ReplyPier 工单
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<!-- No Jira: native ticketing is the flow -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-else-if="canUseNativeTickets"
|
2026-10-22 17:44:36 +08:00
|
|
|
|
class="btn btn-ghost btn-sm"
|
2026-09-27 21:10:00 +08:00
|
|
|
|
@click="showTicketModal = true"
|
|
|
|
|
|
>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span class="btn-glyph" v-html="navIconSvg('ticket', 15)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<span class="btn-label">创建工单</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</template>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="icon-btn sm"
|
|
|
|
|
|
aria-label="会话详情"
|
|
|
|
|
|
title="会话详情"
|
|
|
|
|
|
@click="emit('info')"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span v-html="navIconSvg('panel', 16)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="chat-content">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<div class="messages" ref="messagesContainer" aria-label="对话消息">
|
|
|
|
|
|
<div class="stream-inner">
|
|
|
|
|
|
<template v-for="item in streamItems" :key="item.key">
|
|
|
|
|
|
<div v-if="item.kind === 'day'" class="day">{{ item.label }}</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else-if="item.kind === 'event'"
|
|
|
|
|
|
class="event"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
warn: isEndEvent(item.message),
|
|
|
|
|
|
take: isHandoverEvent(item.message),
|
|
|
|
|
|
}"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="event-pill">
|
|
|
|
|
|
<span
|
|
|
|
|
|
class="event-glyph"
|
|
|
|
|
|
v-html="navIconSvg(eventIcon(item.message), 13)"
|
|
|
|
|
|
></span>
|
|
|
|
|
|
<span class="event-text">{{ item.message.message || '会话事件' }}</span>
|
|
|
|
|
|
<time class="num">{{ formatClock(item.message.created_at) }}</time>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-else
|
|
|
|
|
|
class="msg"
|
|
|
|
|
|
:class="[item.kind, { out: item.kind !== 'visitor' }]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="avatar" :class="avatarTone(item.message)">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="item.kind === 'ai'"
|
|
|
|
|
|
class="ai-glyph"
|
|
|
|
|
|
v-html="navIconSvg('sparkles', 14)"
|
|
|
|
|
|
></span>
|
|
|
|
|
|
<template v-else>{{ avatarText(item.message) }}</template>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="msg-body">
|
|
|
|
|
|
<div class="bubble">
|
|
|
|
|
|
<!-- Product message -->
|
|
|
|
|
|
<template v-if="item.message.message_type === 'product' && item.message.attributes?.shopify_output?.products?.length">
|
|
|
|
|
|
<div class="products-carousel">
|
|
|
|
|
|
<div v-html="renderMarkdown(item.message.message || '')" class="product-message-text"></div>
|
|
|
|
|
|
<div class="carousel-items">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="product in item.message.attributes.shopify_output.products"
|
|
|
|
|
|
:key="product.id"
|
|
|
|
|
|
class="product-card-compact"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="product-image-compact" v-if="product.image?.src">
|
|
|
|
|
|
<img :src="product.image.src" :alt="product.title || ''" class="product-thumbnail">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="product-info-compact">
|
|
|
|
|
|
<div class="product-text-area">
|
|
|
|
|
|
<div class="product-title-compact">{{ product.title }}</div>
|
|
|
|
|
|
<div class="product-variant-compact" v-if="product.variant_title">{{ product.variant_title }}</div>
|
|
|
|
|
|
<div class="product-price-compact">{{ product.price }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="product-actions-compact">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="view-details-button-compact"
|
|
|
|
|
|
@click="$emit('view-product', String(product?.id || 'unknown'))"
|
|
|
|
|
|
>
|
|
|
|
|
|
查看商品 <span class="external-link-icon" v-html="navIconSvg('arrowUpRight', 12)"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Regular message with markdown -->
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<MessageTag
|
|
|
|
|
|
v-if="item.message.attributes?.interactive_reply"
|
|
|
|
|
|
icon="fa-solid fa-hand-pointer"
|
|
|
|
|
|
label="按钮回复"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<MessageTag
|
|
|
|
|
|
v-if="item.message.attributes?.source === 'app'"
|
|
|
|
|
|
icon="fa-solid fa-plug"
|
|
|
|
|
|
:label="`由 ${item.message.attributes?.app_name || '应用'} 发送`"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="bubble-text" v-html="renderMarkdown(item.message.message || '')"></div>
|
|
|
|
|
|
<QuickReplyChips :sent="item.message.attributes?.interactive_sent" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Display attachments if present -->
|
|
|
|
|
|
<div v-if="item.message.attachments && item.message.attachments.length > 0" class="message-attachments">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="attachment in item.message.attachments"
|
|
|
|
|
|
:key="attachment.id"
|
|
|
|
|
|
class="attachment-item"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- Image attachment - render as image -->
|
|
|
|
|
|
<template v-if="isImageAttachment(attachment.content_type)">
|
|
|
|
|
|
<div class="attachment-image-container">
|
|
|
|
|
|
<img
|
|
|
|
|
|
:src="getImageUrl(attachment.file_url)"
|
|
|
|
|
|
:alt="attachment.filename"
|
|
|
|
|
|
class="attachment-image"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="attachment-image-info">
|
|
|
|
|
|
<a
|
|
|
|
|
|
:href="getDownloadUrl(attachment.file_url)"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
class="attachment-link"
|
|
|
|
|
|
>
|
|
|
|
|
|
<font-awesome-icon icon="fa-solid fa-download" />
|
|
|
|
|
|
{{ attachment.filename }}
|
|
|
|
|
|
<span class="attachment-size">({{ formatFileSize(attachment.file_size) }})</span>
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<!-- Other file types - render as download link -->
|
|
|
|
|
|
<template v-else>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<a
|
|
|
|
|
|
:href="getDownloadUrl(attachment.file_url)"
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
class="attachment-link"
|
|
|
|
|
|
>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<font-awesome-icon icon="fa-solid fa-paperclip" />
|
2026-09-27 21:10:00 +08:00
|
|
|
|
{{ attachment.filename }}
|
|
|
|
|
|
<span class="attachment-size">({{ formatFileSize(attachment.file_size) }})</span>
|
|
|
|
|
|
</a>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
</template>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="msg-meta">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="(source, sourceIndex) in (item.message.sources || [])"
|
|
|
|
|
|
:key="`${item.key}-source-${sourceIndex}`"
|
|
|
|
|
|
class="cite"
|
|
|
|
|
|
:title="source.type"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="cite-glyph" v-html="navIconSvg('book', 12)"></span>
|
|
|
|
|
|
依据 · {{ source.name }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="item.kind !== 'visitor'"
|
|
|
|
|
|
class="who"
|
|
|
|
|
|
:class="{ 'who-ai': item.kind === 'ai' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ whoLabel(item.message) }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
|
|
<time class="num" :title="item.message.timeAgo">
|
|
|
|
|
|
{{ formatClock(item.message.created_at) }}
|
|
|
|
|
|
</time>
|
|
|
|
|
|
|
|
|
|
|
|
<MessageStatusTicks
|
|
|
|
|
|
:status="item.message.channel_status"
|
|
|
|
|
|
:on-accent="item.kind === 'human'"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<span v-if="item.message.attributes?.delivery_status === 'failed'" class="delivery-failed">
|
|
|
|
|
|
<span class="fail-glyph" v-html="navIconSvg('x', 12)"></span> 未送达
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="formattedMessages.length === 0" class="thread-empty">
|
|
|
|
|
|
<span class="thread-empty-glyph" v-html="navIconSvg('message', 20)"></span>
|
|
|
|
|
|
<p>还没有消息</p>
|
|
|
|
|
|
<span>这段会话开始后,消息会出现在这里。</span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Jira Ticket Modal -->
|
|
|
|
|
|
<JiraTicketModal
|
|
|
|
|
|
v-if="showJiraTicketModal"
|
|
|
|
|
|
:chat-id="currentChat.session_id"
|
|
|
|
|
|
:initial-summary="ticketSummary"
|
|
|
|
|
|
@close="showJiraTicketModal = false"
|
|
|
|
|
|
@ticket-created="handleTicketCreated"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Native ticket modal (AI-drafted from this conversation) -->
|
|
|
|
|
|
<TicketCreateModal
|
|
|
|
|
|
:open="showTicketModal"
|
|
|
|
|
|
:session-id="currentChat.session_id"
|
|
|
|
|
|
:session-label="chat.customer.full_name || chat.customer.email || '此会话'"
|
|
|
|
|
|
@close="showTicketModal = false"
|
|
|
|
|
|
@created="refreshLinkedTicket"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Unclaimed: the composer is replaced by the primary "Take over chat"
|
|
|
|
|
|
action. Covers both an AI-handled chat and one the AI has queued for a
|
|
|
|
|
|
human, so claiming never requires opening the details panel. -->
|
|
|
|
|
|
<footer v-if="showTakeoverButton" class="chat-input takeover-footer">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<p class="takeover-caption">{{ handlerCaption }}</p>
|
|
|
|
|
|
<div class="takeover-actions">
|
|
|
|
|
|
<button class="btn btn-primary takeover-button" :disabled="isLoading" @click="handleTakeover">
|
|
|
|
|
|
<span class="btn-glyph" v-html="navIconSvg('headset', 16)"></span>
|
|
|
|
|
|
接管会话
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<!-- Claiming it yourself was the only way to silence the AI. This queues
|
|
|
|
|
|
it for whoever is free instead. -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="handledByAI"
|
|
|
|
|
|
class="btn btn-ghost hand-over-button"
|
|
|
|
|
|
:disabled="isLoading"
|
|
|
|
|
|
@click="handleRouteToHuman"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="btn-glyph" v-html="navIconSvg('transfer', 15)"></span>
|
|
|
|
|
|
转给团队
|
|
|
|
|
|
</button>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
|
|
|
|
|
|
<footer class="chat-input" v-else-if="!isChatClosed && !handledByAI">
|
|
|
|
|
|
<!-- The agent has just been told a reply couldn't be delivered; offer the
|
|
|
|
|
|
one action that gets the conversation back rather than leaving them
|
|
|
|
|
|
to retype into a composer that will fail again. -->
|
|
|
|
|
|
<div v-if="templateCanReopen && canUseTemplates" class="window-closed-notice">
|
|
|
|
|
|
<div class="window-closed-text">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span class="btn-glyph" v-html="navIconSvg('clock', 14)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<span>该客户的 24 小时窗口已关闭,发送已审核的模板可重新开启。</span>
|
|
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<button class="btn btn-primary btn-sm" @click="showTemplatePicker = true">
|
2026-09-27 21:10:00 +08:00
|
|
|
|
发送模板
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="composer"
|
|
|
|
|
|
:class="{ disabled: !canSendMessage }"
|
|
|
|
|
|
@paste="handleChatPaste"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="composer-row">
|
|
|
|
|
|
<input
|
2026-09-27 21:10:00 +08:00
|
|
|
|
v-model="newMessage"
|
|
|
|
|
|
type="text"
|
2026-10-22 17:44:36 +08:00
|
|
|
|
:placeholder="composerPlaceholder"
|
2026-09-27 21:10:00 +08:00
|
|
|
|
class="message-input"
|
|
|
|
|
|
@keyup.enter="handleSendMessageWithAttachments"
|
|
|
|
|
|
:disabled="!canSendMessage"
|
|
|
|
|
|
>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="composer-bar">
|
|
|
|
|
|
<FileUpload
|
|
|
|
|
|
ref="fileUploadRef"
|
|
|
|
|
|
:max-files="3"
|
|
|
|
|
|
@filesUploaded="handleFilesUploaded"
|
|
|
|
|
|
@error="handleFileUploadError"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="as-role">
|
|
|
|
|
|
以 <b>{{ currentUserName }}</b> 身份回复
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="hint"><kbd>Enter</kbd> 发送</span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<button
|
2026-10-22 17:44:36 +08:00
|
|
|
|
class="btn btn-primary btn-sm send-button"
|
2026-09-27 21:10:00 +08:00
|
|
|
|
@click="handleSendMessageWithAttachments"
|
|
|
|
|
|
:disabled="(!newMessage.trim() && uploadedFiles.length === 0) || !canSendMessage"
|
|
|
|
|
|
aria-label="发送"
|
|
|
|
|
|
>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
发送
|
|
|
|
|
|
<span class="btn-glyph" v-html="navIconSvg('send', 14)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-10-22 17:44:36 +08:00
|
|
|
|
|
2026-09-27 21:10:00 +08:00
|
|
|
|
<div v-if="!canSendMessage" class="input-message">
|
|
|
|
|
|
{{ handlerCaption }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
|
|
|
|
|
|
<footer v-else-if="handledByAI" class="chat-closed-footer">
|
|
|
|
|
|
<div class="chat-closed-message">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span v-html="navIconSvg('lock', 15)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
此会话正由 AI 客服处理
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
<footer v-else class="chat-closed-footer">
|
|
|
|
|
|
<div class="chat-closed-message">
|
2026-10-22 17:44:36 +08:00
|
|
|
|
<span v-html="navIconSvg('lock', 15)"></span>
|
2026-09-27 21:10:00 +08:00
|
|
|
|
此会话已结束
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
|
|
|
|
|
|
<WhatsAppTemplatePicker
|
|
|
|
|
|
v-if="showTemplatePicker && currentChat.channel_account_id"
|
|
|
|
|
|
:account-id="currentChat.channel_account_id"
|
|
|
|
|
|
:session-id="currentChat.session_id"
|
|
|
|
|
|
@close="showTemplatePicker = false"
|
|
|
|
|
|
@sent="handleTemplateSent"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.chat-layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
width: 100%;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
background: var(--paper);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Thread header ─────────────────────────────────── */
|
|
|
|
|
|
.thread-head {
|
|
|
|
|
|
flex: none;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 14px;
|
|
|
|
|
|
padding: 16px 28px;
|
|
|
|
|
|
border-bottom: 1px solid var(--line);
|
|
|
|
|
|
background: color-mix(in srgb, var(--surface) 70%, transparent);
|
|
|
|
|
|
backdrop-filter: blur(8px);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.back-btn {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: none;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
margin-left: -8px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.back-glyph {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
transform: rotate(180deg);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.back-glyph :deep(svg) {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-who {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-name {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
min-width: 0;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.thread-name h2 {
|
|
|
|
|
|
font-family: var(--font-serif);
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
letter-spacing: 0.03em;
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-name .chip {
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chip-glyph {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chip-glyph :deep(svg) {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-meta {
|
|
|
|
|
|
margin-top: 3px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
color: var(--muted);
|
2026-10-22 17:44:36 +08:00
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.thread-meta i {
|
|
|
|
|
|
width: 3px;
|
|
|
|
|
|
height: 3px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--faint);
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-actions {
|
|
|
|
|
|
margin-left: auto;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex: none;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.btn-glyph {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.btn-glyph :deep(svg) {
|
|
|
|
|
|
display: block;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.ticket-btn-group {
|
|
|
|
|
|
position: relative;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ticket-btn-group .btn {
|
|
|
|
|
|
border-top-right-radius: 0;
|
|
|
|
|
|
border-bottom-right-radius: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ticket-menu-toggle {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
justify-content: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: 1px solid var(--line-2);
|
|
|
|
|
|
border-left: 0;
|
|
|
|
|
|
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
color: var(--text2);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
cursor: pointer;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
transition:
|
|
|
|
|
|
border-color var(--t-fast),
|
|
|
|
|
|
color var(--t-fast);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.ticket-menu-toggle:hover {
|
|
|
|
|
|
border-color: var(--line-3);
|
|
|
|
|
|
color: var(--ink);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.ticket-menu {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: calc(100% + 6px);
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
z-index: 30;
|
|
|
|
|
|
min-width: 190px;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ticket-menu-item {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--text2);
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ticket-menu-item:hover {
|
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
color: var(--ink);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Stream ────────────────────────────────────────── */
|
2026-09-27 21:10:00 +08:00
|
|
|
|
.chat-content {
|
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.messages {
|
|
|
|
|
|
position: absolute;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
inset: 0;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
overflow-y: auto;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 28px 28px 12px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
scroll-behavior: smooth;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.stream-inner {
|
|
|
|
|
|
max-width: 780px;
|
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
|
margin: 0 auto;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 18px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.day {
|
|
|
|
|
|
align-self: center;
|
|
|
|
|
|
font-family: var(--font-serif);
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
letter-spacing: 0.3em;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
color: var(--muted);
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 4px 0 6px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
max-width: 80%;
|
|
|
|
|
|
align-items: flex-start;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.out {
|
|
|
|
|
|
align-self: flex-end;
|
|
|
|
|
|
flex-direction: row-reverse;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg .avatar {
|
|
|
|
|
|
--a: 30px;
|
|
|
|
|
|
margin-top: 2px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg-body {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 7px;
|
|
|
|
|
|
min-width: 0;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.out .msg-body {
|
|
|
|
|
|
align-items: flex-end;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.bubble {
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
font-size: 14.5px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
max-width: 100%;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.visitor .bubble {
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-top-left-radius: 5px;
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
color: var(--ink-2);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.ai .bubble {
|
|
|
|
|
|
background: var(--bubble-ai);
|
|
|
|
|
|
border: 1px solid var(--celadon-line);
|
|
|
|
|
|
border-top-right-radius: 5px;
|
|
|
|
|
|
color: var(--ink-2);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.human .bubble {
|
|
|
|
|
|
background: var(--bubble-human);
|
|
|
|
|
|
color: var(--bubble-human-fg);
|
|
|
|
|
|
border-top-right-radius: 5px;
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg-meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
padding: 0 2px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.out .msg-meta {
|
|
|
|
|
|
justify-content: flex-end;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg-meta .who {
|
|
|
|
|
|
color: var(--ink-2);
|
|
|
|
|
|
font-weight: 500;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.ai .msg-meta .who {
|
|
|
|
|
|
color: var(--celadon);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg-meta time {
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* Knowledge-base citation — 青瓷 dashed capsule */
|
|
|
|
|
|
.cite {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
padding: 0 10px 0 8px;
|
|
|
|
|
|
border-radius: var(--radius-pill);
|
|
|
|
|
|
border: 1px dashed var(--celadon-line);
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
color: var(--celadon);
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
transition: border-style var(--t-fast);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.cite:hover {
|
|
|
|
|
|
border-style: solid;
|
|
|
|
|
|
}
|
2026-09-27 21:10:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.cite-glyph {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
flex: none;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.cite-glyph :deep(svg) {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* System events — rule, pill, timestamp */
|
|
|
|
|
|
.event {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 14px;
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
margin: 6px 0;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.event::before,
|
|
|
|
|
|
.event::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
background: linear-gradient(90deg, transparent, var(--line-2));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.event::after {
|
|
|
|
|
|
background: linear-gradient(270deg, transparent, var(--line-2));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.event-pill {
|
|
|
|
|
|
display: inline-flex;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 5px 14px;
|
|
|
|
|
|
border-radius: var(--radius-pill);
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
max-width: 78%;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.event-glyph {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.event-glyph :deep(svg) {
|
|
|
|
|
|
display: block;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.event-text {
|
|
|
|
|
|
min-width: 0;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.event time {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
flex: none;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.event.warn .event-pill {
|
|
|
|
|
|
color: var(--vermilion);
|
|
|
|
|
|
border-color: var(--vermilion-line);
|
|
|
|
|
|
background: color-mix(in srgb, var(--vermilion) 5%, var(--surface));
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.event.warn .event-glyph {
|
|
|
|
|
|
color: var(--vermilion);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* A hand-off is notable, not alarming: ink rather than 朱砂. */
|
|
|
|
|
|
.event.take .event-pill {
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
font-weight: 500;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.event.take .event-glyph {
|
|
|
|
|
|
color: var(--ink);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.thread-empty {
|
|
|
|
|
|
margin: auto;
|
|
|
|
|
|
padding: 48px 16px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
flex-direction: column;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 5px;
|
|
|
|
|
|
text-align: center;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.thread-empty-glyph {
|
|
|
|
|
|
width: 46px;
|
|
|
|
|
|
height: 46px;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.thread-empty p {
|
|
|
|
|
|
font-family: var(--font-serif);
|
|
|
|
|
|
font-size: 17px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--ink);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.thread-empty span:last-child {
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* Payload inside a bubble */
|
|
|
|
|
|
.bubble-text :deep(p) {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
2026-09-27 21:10:00 +08:00
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.bubble-text :deep(p + p),
|
|
|
|
|
|
.bubble-text :deep(ul + p),
|
|
|
|
|
|
.bubble-text :deep(p + ul) {
|
|
|
|
|
|
margin-top: 8px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.bubble-text :deep(ul),
|
|
|
|
|
|
.bubble-text :deep(ol) {
|
|
|
|
|
|
margin: 4px 0 0;
|
|
|
|
|
|
padding-left: 1.2em;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.bubble-text :deep(a) {
|
|
|
|
|
|
color: var(--indigo);
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
text-underline-offset: 2px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.human .bubble-text :deep(a) {
|
|
|
|
|
|
color: var(--bubble-human-fg);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.human .bubble-text :deep(a:hover) {
|
|
|
|
|
|
opacity: 0.75;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.bubble-text :deep(img) {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
margin: 8px 0;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.bubble-text :deep(code) {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 0.92em;
|
|
|
|
|
|
padding: 1px 5px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
background: var(--surface-sunk);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.human .message-tag {
|
|
|
|
|
|
background: color-mix(in srgb, var(--bubble-human-fg) 10%, transparent);
|
|
|
|
|
|
border-color: color-mix(in srgb, var(--bubble-human-fg) 22%, transparent);
|
|
|
|
|
|
color: color-mix(in srgb, var(--bubble-human-fg) 78%, transparent);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Attachments ───────────────────────────────────── */
|
|
|
|
|
|
.message-attachments {
|
|
|
|
|
|
margin-top: 10px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-link {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 7px;
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
color: var(--ink-2);
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
transition: border-color var(--t-fast);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-link:hover {
|
|
|
|
|
|
border-color: var(--line-3);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-link :deep(svg) {
|
|
|
|
|
|
width: 13px;
|
|
|
|
|
|
height: 13px;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-size {
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
margin-left: 2px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.human .attachment-link {
|
|
|
|
|
|
background: color-mix(in srgb, var(--bubble-human-fg) 12%, transparent);
|
|
|
|
|
|
border-color: color-mix(in srgb, var(--bubble-human-fg) 22%, transparent);
|
|
|
|
|
|
color: var(--bubble-human-fg);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.msg.human .attachment-link :deep(svg),
|
|
|
|
|
|
.msg.human .attachment-size {
|
|
|
|
|
|
color: color-mix(in srgb, var(--bubble-human-fg) 72%, transparent);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-image-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
max-width: 350px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-image {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
border: 1px solid var(--line);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-image-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-size: 12px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.attachment-image-info .attachment-link {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
font-size: 12px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Product carousel ──────────────────────────────── */
|
2026-09-27 21:10:00 +08:00
|
|
|
|
.products-carousel {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
margin: 2px 0;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
width: 100%;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 6px;
|
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
border-radius: var(--radius-card);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-message-text {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
margin-bottom: 6px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.carousel-items {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: 2px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
overflow-x: auto;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 2px 2px 10px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
scroll-snap-type: x mandatory;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-card-compact {
|
|
|
|
|
|
width: 180px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
flex: none;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: var(--radius-card);
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
transition: box-shadow var(--t-fast), transform var(--t-fast);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-card-compact:hover {
|
|
|
|
|
|
transform: translateY(-2px);
|
2026-10-22 17:44:36 +08:00
|
|
|
|
box-shadow: var(--shadow-md);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-image-compact {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-thumbnail {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
transition: transform var(--t-med);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-thumbnail:hover {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
transform: scale(1.04);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-info-compact {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 9px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 5px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-title-compact {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
font-size: 12.5px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
|
-webkit-line-clamp: 2;
|
|
|
|
|
|
line-clamp: 2;
|
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
|
min-height: 2.8em;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
color: var(--ink);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-variant-compact {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.product-price-compact {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
font-size: 13px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
font-weight: 600;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
color: var(--ink);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.view-details-button-compact {
|
|
|
|
|
|
width: 100%;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
border: 1px solid var(--line-2);
|
|
|
|
|
|
border-radius: var(--radius-pill);
|
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
color: var(--ink-2);
|
|
|
|
|
|
font-size: 11.5px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
font-weight: 500;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
display: inline-flex;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 4px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: border-color var(--t-fast), color var(--t-fast);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.view-details-button-compact:hover {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
border-color: var(--line-3);
|
|
|
|
|
|
color: var(--ink);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.external-link-icon {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
display: inline-flex;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.external-link-icon :deep(svg) {
|
|
|
|
|
|
display: block;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Composer ──────────────────────────────────────── */
|
|
|
|
|
|
.chat-input {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 10px 28px 22px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.composer {
|
|
|
|
|
|
max-width: 780px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
border: 1px solid var(--line-2);
|
|
|
|
|
|
border-radius: var(--radius-xl);
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
transition:
|
|
|
|
|
|
border-color var(--t-fast),
|
|
|
|
|
|
box-shadow var(--t-fast);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.composer:focus-within {
|
|
|
|
|
|
border-color: var(--line-3);
|
|
|
|
|
|
box-shadow: var(--shadow-sm), var(--ring);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.composer.disabled {
|
|
|
|
|
|
opacity: 0.72;
|
|
|
|
|
|
background: var(--surface-2);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.composer-row {
|
|
|
|
|
|
padding: 14px 18px 2px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.message-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 2px 0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
font-family: var(--font-sans);
|
|
|
|
|
|
font-size: 14.5px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-input::placeholder {
|
|
|
|
|
|
color: var(--faint);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-input:disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
padding: 4px 10px 10px 12px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.as-role {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 5px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.as-role b {
|
|
|
|
|
|
color: var(--ink-2);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar .hint {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
display: inline-flex;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 5px;
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar .send-button {
|
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* The attachment control is shared with other surfaces; compact it to a
|
|
|
|
|
|
toolbar glyph and let previews ride inline instead of stacking a panel. */
|
|
|
|
|
|
.composer-bar :deep(.file-upload-container) {
|
|
|
|
|
|
display: contents;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.upload-button) {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--text2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.upload-button:hover:not(:disabled)) {
|
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
border-color: var(--line-2);
|
|
|
|
|
|
color: var(--ink);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.upload-button svg) {
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-previews) {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: row;
|
|
|
|
|
|
flex-wrap: nowrap;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
gap: 6px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
max-width: 44%;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 3px 6px;
|
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview) {
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview:hover) {
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
border-color: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview::before) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview-content) {
|
|
|
|
|
|
width: 22px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
|
background: var(--surface-sunk);
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview-content::after) {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview-image) {
|
|
|
|
|
|
width: 22px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview-icon svg) {
|
|
|
|
|
|
width: 13px;
|
|
|
|
|
|
height: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview-icon) {
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview-name) {
|
|
|
|
|
|
max-width: 120px;
|
|
|
|
|
|
color: var(--ink-2);
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview-size) {
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
font-size: 10.5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-preview-remove) {
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
font-size: 13px;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
box-shadow: none;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.composer-bar :deep(.file-preview-remove:hover) {
|
|
|
|
|
|
background: var(--vermilion-soft);
|
|
|
|
|
|
color: var(--vermilion);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.composer-bar :deep(.upload-progress) {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.window-closed-notice {
|
|
|
|
|
|
max-width: 780px;
|
|
|
|
|
|
margin: 0 auto 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
|
border: 1px solid var(--brass-line);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
background: var(--brass-soft);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.window-closed-text {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
|
color: var(--ink-2);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.window-closed-text .btn-glyph {
|
|
|
|
|
|
color: var(--brass);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.input-message {
|
|
|
|
|
|
max-width: 780px;
|
|
|
|
|
|
margin: 8px auto 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: var(--muted);
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Takeover / closed footers ─────────────────────── */
|
|
|
|
|
|
.takeover-footer {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
gap: 10px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.takeover-caption {
|
|
|
|
|
|
max-width: 780px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.takeover-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
max-width: 780px;
|
|
|
|
|
|
margin: 0 auto;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.takeover-button {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 44px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.hand-over-button {
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
height: 44px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.chat-closed-footer {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
padding: 12px 28px 22px;
|
|
|
|
|
|
width: 100%;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.chat-closed-message {
|
|
|
|
|
|
max-width: 780px;
|
|
|
|
|
|
margin: 0 auto;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-10-22 17:44:36 +08:00
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 13px 18px;
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: var(--radius-card);
|
|
|
|
|
|
background: var(--surface);
|
|
|
|
|
|
color: var(--muted);
|
|
|
|
|
|
font-size: 13px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
/* ── Post-bubble status ────────────────────────────── */
|
|
|
|
|
|
.delivery-failed {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--vermilion);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fail-glyph {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fail-glyph :deep(svg) {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-glyph {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ai-glyph :deep(svg) {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── Responsive ────────────────────────────────────── */
|
|
|
|
|
|
@media (max-width: 1440px) {
|
|
|
|
|
|
.thread-head {
|
|
|
|
|
|
padding: 12px 18px;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-meta {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-name h2 {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.messages {
|
|
|
|
|
|
padding: 22px 18px 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-input {
|
|
|
|
|
|
padding: 8px 18px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-closed-footer {
|
|
|
|
|
|
padding: 10px 18px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* The three-column desktop gives the thread less width than the viewport,
|
|
|
|
|
|
so the action labels yield to the customer name before the thread does. */
|
|
|
|
|
|
.thread-actions .btn-label {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-actions .btn {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile: full-screen chat pane */
|
|
|
|
|
|
@media (max-width: 768px) {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.back-btn {
|
|
|
|
|
|
display: inline-flex;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.thread-head {
|
|
|
|
|
|
padding: 8px 8px 8px 6px;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-name h2 {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
max-width: 46vw;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-name {
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
row-gap: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-actions {
|
|
|
|
|
|
gap: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-actions .btn,
|
|
|
|
|
|
.thread-actions .icon-btn.sm,
|
2026-09-27 21:10:00 +08:00
|
|
|
|
.back-btn {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
width: 38px;
|
|
|
|
|
|
height: 38px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.thread-actions .btn {
|
|
|
|
|
|
border-color: transparent;
|
|
|
|
|
|
background: transparent;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.messages {
|
|
|
|
|
|
padding: 18px 12px 8px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.stream-inner {
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.msg {
|
|
|
|
|
|
max-width: 88%;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.msg .avatar {
|
|
|
|
|
|
--a: 26px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bubble {
|
|
|
|
|
|
padding: 10px 13px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.day {
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
letter-spacing: 0.24em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.event {
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.event-pill {
|
|
|
|
|
|
max-width: 88%;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar .hint {
|
2026-09-27 21:10:00 +08:00
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.composer-bar {
|
|
|
|
|
|
padding: 2px 8px 8px 10px;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.composer-row {
|
|
|
|
|
|
padding: 12px 14px 0;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 17:44:36 +08:00
|
|
|
|
.as-role {
|
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-input {
|
|
|
|
|
|
padding: 8px 12px calc(12px + var(--safe-bottom) + var(--kb-offset, 0px));
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-closed-footer {
|
2026-10-22 17:44:36 +08:00
|
|
|
|
padding: 8px 12px calc(12px + var(--safe-bottom));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.window-closed-notice {
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.takeover-button,
|
|
|
|
|
|
.hand-over-button {
|
|
|
|
|
|
height: 46px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer-bar :deep(.file-previews) {
|
|
|
|
|
|
order: -1;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
|
.thread-name .chip {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.thread-name h2 {
|
|
|
|
|
|
max-width: 52vw;
|
2026-09-27 21:10:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-22 17:44:36 +08:00
|
|
|
|
</style>
|