first implementation - image/drawing integration
This commit is contained in:
@@ -5,22 +5,47 @@
|
||||
<div v-if="isUser" class="message-content">{{ message.content }}</div>
|
||||
<div v-else class="message-content message-content--markdown" v-html="renderedContent"></div>
|
||||
|
||||
<!-- Source chips for assistant messages -->
|
||||
<!-- Sources for assistant messages -->
|
||||
<div v-if="!isUser && message.sources && message.sources.length > 0" class="message-sources">
|
||||
<div class="sources-label">Sources:</div>
|
||||
<div class="source-list">
|
||||
<!-- TEXT sources -->
|
||||
<div
|
||||
v-for="(source, idx) in message.sources"
|
||||
:key="idx"
|
||||
v-for="(source, idx) in textSources"
|
||||
:key="'text-' + idx"
|
||||
class="source-item"
|
||||
>
|
||||
<div class="source-chip">
|
||||
<span class="source-book-icon">📖</span>
|
||||
<div class="source-chip source-chip--text">
|
||||
<span class="source-icon">📖</span>
|
||||
<span class="source-book-title">{{ source.bookTitle }}</span>
|
||||
<span v-if="source.page" class="source-page">p. {{ source.page }}</span>
|
||||
</div>
|
||||
<div v-if="source.chunkText" class="source-chunk">{{ source.chunkText }}</div>
|
||||
</div>
|
||||
|
||||
<!-- FIGURE sources -->
|
||||
<div
|
||||
v-for="(source, idx) in figureSources"
|
||||
:key="'fig-' + idx"
|
||||
class="source-item source-item--figure"
|
||||
>
|
||||
<div class="source-chip source-chip--figure">
|
||||
<span class="source-icon">🖼️</span>
|
||||
<span class="source-figure-label">{{ source.label || 'Figure' }}</span>
|
||||
<span v-if="source.page" class="source-page">p. {{ source.page }}</span>
|
||||
<span v-if="source.figureType" class="source-figure-type">{{ formatFigureType(source.figureType) }}</span>
|
||||
</div>
|
||||
<div v-if="source.caption" class="source-caption">{{ source.caption }}</div>
|
||||
<div class="source-figure-image">
|
||||
<img
|
||||
:src="source.imageUrl"
|
||||
:alt="source.caption || source.label || 'Figure'"
|
||||
class="figure-img"
|
||||
loading="lazy"
|
||||
@error="onImageError"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +57,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import type { ChatMessage } from '@/stores/chatStore'
|
||||
import type { ChatMessage, ChatSource } from '@/stores/chatStore'
|
||||
|
||||
const props = defineProps<{
|
||||
message: ChatMessage
|
||||
@@ -41,6 +66,36 @@ const props = defineProps<{
|
||||
const isUser = computed(() => props.message.role === 'USER')
|
||||
const renderedContent = computed(() => marked.parse(props.message.content) as string)
|
||||
|
||||
const textSources = computed(() =>
|
||||
(props.message.sources ?? []).filter((s: ChatSource) => s.type === 'TEXT' || !s.type)
|
||||
)
|
||||
|
||||
const figureSources = computed(() =>
|
||||
(props.message.sources ?? []).filter((s: ChatSource) => s.type === 'FIGURE')
|
||||
)
|
||||
|
||||
function formatFigureType(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
ANATOMICAL_DIAGRAM: 'Anatomical Diagram',
|
||||
SURGICAL_PHOTOGRAPH: 'Surgical Photo',
|
||||
MRI_CT_SCAN: 'MRI / CT',
|
||||
TABLE: 'Table',
|
||||
CHART: 'Chart',
|
||||
INTRAOPERATIVE_IMAGE: 'Intraoperative'
|
||||
}
|
||||
return labels[type] ?? type
|
||||
}
|
||||
|
||||
function onImageError(e: Event) {
|
||||
const img = e.target as HTMLImageElement
|
||||
img.alt = 'Image unavailable'
|
||||
img.style.display = 'none'
|
||||
const wrapper = img.parentElement
|
||||
if (wrapper) {
|
||||
wrapper.innerHTML = '<span class="figure-missing">Image unavailable</span>'
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(iso: string): string {
|
||||
return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
@@ -182,6 +237,55 @@ function formatTime(iso: string): string {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.source-item--figure {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.source-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.source-chip--text {
|
||||
background: #ebf8ff;
|
||||
border: 1px solid #bee3f8;
|
||||
}
|
||||
|
||||
.source-chip--figure {
|
||||
background: #f0fff4;
|
||||
border: 1px solid #9ae6b4;
|
||||
}
|
||||
|
||||
.source-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.source-book-title {
|
||||
color: #2b6cb0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.source-figure-label {
|
||||
color: #276749;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.source-figure-type {
|
||||
color: #718096;
|
||||
font-size: 0.72rem;
|
||||
background: #e2e8f0;
|
||||
border-radius: 3px;
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
|
||||
.source-page {
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.source-chunk {
|
||||
font-size: 0.78rem;
|
||||
color: #4a5568;
|
||||
@@ -194,28 +298,28 @@ function formatTime(iso: string): string {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.source-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: #ebf8ff;
|
||||
border: 1px solid #bee3f8;
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
.source-caption {
|
||||
font-size: 0.78rem;
|
||||
color: #4a5568;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.source-book-icon {
|
||||
font-size: 0.8rem;
|
||||
.source-figure-image {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.source-book-title {
|
||||
color: #2b6cb0;
|
||||
font-weight: 500;
|
||||
.figure-img {
|
||||
max-width: 100%;
|
||||
max-height: 300px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.source-page {
|
||||
color: #718096;
|
||||
.figure-missing {
|
||||
font-size: 0.78rem;
|
||||
color: #a0aec0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.message-timestamp {
|
||||
|
||||
@@ -2,11 +2,25 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { api } from '@/services/api'
|
||||
|
||||
export interface ChatSource {
|
||||
type: 'TEXT' | 'FIGURE'
|
||||
bookTitle: string
|
||||
page: number | null
|
||||
// TEXT-specific
|
||||
chunkText?: string
|
||||
// FIGURE-specific
|
||||
figureId?: string
|
||||
label?: string
|
||||
caption?: string
|
||||
figureType?: string
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string
|
||||
role: 'USER' | 'ASSISTANT'
|
||||
content: string
|
||||
sources: Array<{ bookTitle: string; page: number | null; chunkText?: string }>
|
||||
sources: ChatSource[]
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user