Spaces:
Running
Running
Download web/src/components/ChatMessage.vue from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 6.47 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/web/src/components/ChatMessage.vue
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/web/src/components/ChatMessage.vue
-
curl -L -o ChatMessage.vue https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/web/src/components/ChatMessage.vue
6.47 kB
| <template> | |
| <div class="chat-message" :class="[`role-${message.role}`, { streaming: message.isStreaming }]"> | |
| <div class="message-avatar"> | |
| <n-avatar | |
| v-if="message.role === 'assistant'" | |
| :size="36" | |
| :style="{ background: 'var(--primary)' }" | |
| > | |
| <n-icon><shield-checkmark-outline /></n-icon> | |
| </n-avatar> | |
| <n-avatar | |
| v-else | |
| :size="36" | |
| :style="{ background: 'var(--accent)' }" | |
| > | |
| <n-icon><person-outline /></n-icon> | |
| </n-avatar> | |
| </div> | |
| <div class="message-content"> | |
| <!-- User Message --> | |
| <div v-if="message.role === 'user'" class="user-text"> | |
| {{ message.content }} | |
| </div> | |
| <!-- Assistant Message --> | |
| <div v-else class="assistant-message"> | |
| <!-- Confidence Badge --> | |
| <div v-if="message.confidence" class="confidence-badge"> | |
| <n-tag | |
| :type="confidenceType" | |
| size="small" | |
| class="confidence-tag" | |
| > | |
| <template #icon> | |
| <n-icon size="14"> | |
| <checkmark-circle-outline v-if="message.confidence === 'high'" /> | |
| <help-circle-outline v-else-if="message.confidence === 'medium'" /> | |
| <warning-outline v-else /> | |
| </n-icon> | |
| </template> | |
| {{ message.confidence }} confidence | |
| </n-tag> | |
| </div> | |
| <!-- Message Text (Markdown) --> | |
| <div class="message-text markdown-body" v-html="renderedContent" /> | |
| <!-- Citations --> | |
| <div v-if="message.citations && message.citations.length > 0" class="citations"> | |
| <p class="citations-label">Sources:</p> | |
| <div class="citation-list"> | |
| <n-button | |
| v-for="(citation, index) in message.citations" | |
| :key="index" | |
| quaternary | |
| size="tiny" | |
| class="citation-btn" | |
| @click="$emit('citation-click', citation)" | |
| > | |
| <template #icon> | |
| <n-icon size="14"><document-text-outline /></n-icon> | |
| </template> | |
| [{{ index + 1 }}] {{ citation.title }}, p.{{ citation.page }} | |
| </n-button> | |
| </div> | |
| </div> | |
| <!-- Feedback --> | |
| <div v-if="!message.isStreaming && message.role === 'assistant'" class="message-feedback"> | |
| <n-divider style="margin: 12px 0" /> | |
| <div class="feedback-row"> | |
| <span class="feedback-label">Was this helpful?</span> | |
| <n-button-group size="tiny"> | |
| <n-button quaternary @click="submitFeedback(1)"> | |
| <template #icon><n-icon><thumbs-up-outline /></n-icon></template> | |
| </n-button> | |
| <n-button quaternary @click="submitFeedback(-1)"> | |
| <template #icon><n-icon><thumbs-down-outline /></n-icon></template> | |
| </n-button> | |
| </n-button-group> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Timestamp --> | |
| <span class="message-time">{{ formatTime(message.timestamp) }}</span> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { computed } from 'vue' | |
| import { marked } from 'marked' | |
| import DOMPurify from 'dompurify' | |
| import type { ChatMessage } from '@/types' | |
| import { format } from 'date-fns' | |
| import { | |
| ShieldCheckmarkOutline, | |
| PersonOutline, | |
| DocumentTextOutline, | |
| ThumbsUpOutline, | |
| ThumbsDownOutline, | |
| CheckmarkCircleOutline, | |
| HelpCircleOutline, | |
| WarningOutline, | |
| } from '@vicons/ionicons5' | |
| const props = defineProps<{ | |
| message: ChatMessage | |
| }>() | |
| const emit = defineEmits<{ | |
| feedback: [messageId: string, rating: number, correction?: string] | |
| citationClick: [citation: any] | |
| }>() | |
| const renderedContent = computed(() => { | |
| const rawHtml = marked.parse(props.message.content, { async: false }) as string | |
| return DOMPurify.sanitize(rawHtml) | |
| }) | |
| const confidenceType = computed(() => { | |
| switch (props.message.confidence) { | |
| case 'high': return 'success' | |
| case 'medium': return 'warning' | |
| case 'low': return 'error' | |
| default: return 'default' | |
| } | |
| }) | |
| const formatTime = (timestamp: string) => { | |
| return format(new Date(timestamp), 'h:mm a') | |
| } | |
| const submitFeedback = (rating: number) => { | |
| emit('feedback', props.message.id, rating) | |
| } | |
| </script> | |
| <style scoped lang="scss"> | |
| .chat-message { | |
| display: flex; | |
| gap: 12px; | |
| padding: 8px 0; | |
| animation: slideIn 0.3s ease; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .role-user { | |
| flex-direction: row-reverse; | |
| .message-content { | |
| align-items: flex-end; | |
| } | |
| .user-text { | |
| background: var(--primary); | |
| color: white; | |
| border-radius: 16px 16px 4px 16px; | |
| padding: 12px 16px; | |
| max-width: 80%; | |
| font-size: 15px; | |
| line-height: 1.5; | |
| } | |
| } | |
| .role-assistant { | |
| .message-content { | |
| align-items: flex-start; | |
| max-width: 85%; | |
| } | |
| } | |
| .message-avatar { | |
| flex-shrink: 0; | |
| } | |
| .message-content { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .assistant-message { | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border); | |
| border-radius: 16px 16px 16px 4px; | |
| padding: 16px; | |
| width: 100%; | |
| } | |
| .confidence-badge { | |
| margin-bottom: 8px; | |
| } | |
| .confidence-tag { | |
| font-size: 11px; | |
| } | |
| .message-text { | |
| font-size: 15px; | |
| line-height: 1.6; | |
| color: var(--text-primary); | |
| :deep(p) { | |
| margin-bottom: 0.8em; | |
| &:last-child { | |
| margin-bottom: 0; | |
| } | |
| } | |
| } | |
| .citations { | |
| margin-top: 12px; | |
| padding-top: 12px; | |
| border-top: 1px dashed var(--border); | |
| } | |
| .citations-label { | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| margin-bottom: 8px; | |
| } | |
| .citation-list { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| .citation-btn { | |
| font-size: 12px; | |
| color: var(--primary); | |
| background: rgba(26, 95, 180, 0.08); | |
| border-radius: 6px; | |
| &:hover { | |
| background: rgba(26, 95, 180, 0.15); | |
| } | |
| } | |
| .message-feedback { | |
| margin-top: 8px; | |
| } | |
| .feedback-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .feedback-label { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| } | |
| .message-time { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| margin-top: 4px; | |
| } | |
| .streaming { | |
| .assistant-message { | |
| border-style: dashed; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .role-user .user-text { | |
| max-width: 90%; | |
| } | |
| .role-assistant .message-content { | |
| max-width: 90%; | |
| } | |
| } | |
| </style> | |