Skip to content

Commit

Permalink
Merge pull request #592 from sugarforever/feature/preview
Browse files Browse the repository at this point in the history
feat: Markdown content preview
  • Loading branch information
sugarforever authored Dec 17, 2024
2 parents 0c5aa25 + 6cc3c96 commit 93d0e06
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 102 deletions.
94 changes: 57 additions & 37 deletions components/Chat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -272,48 +272,68 @@ async function saveMessage(data: Omit<ChatHistory, 'sessionId'>) {
? await clientDB.chatHistories.add({ ...data, sessionId: props.sessionId })
: Math.random()
}
// Add new state for preview
const showPreview = ref(false)
const previewContent = ref('')
// Add method to handle preview requests from messages
const onPreviewRequest = (content: string) => {
previewContent.value = content
showPreview.value = true
}
</script>

<template>
<div class="flex flex-col box-border dark:text-gray-300 md:-mx-4">
<div class="px-4 border-b border-gray-200 dark:border-gray-700 box-border h-[57px] flex items-center">
<slot name="left-menu-btn"></slot>
<ChatConfigInfo v-if="instructionInfo" icon="i-iconoir-terminal"
:title="instructionInfo.name"
:description="instructionInfo.instruction"
class="hidden md:block" />
<ChatConfigInfo v-if="knowledgeBaseInfo" icon="i-heroicons-book-open"
:title="knowledgeBaseInfo.name"
class="mx-2 hidden md:block" />
<div class="mx-auto px-4 text-center">
<h2 class="line-clamp-1">{{ sessionInfo?.title || t('chat.untitled') }}</h2>
<div class="text-xs text-muted line-clamp-1">{{ instructionInfo?.name }}</div>
<div class="flex box-border dark:text-gray-300 md:-mx-4 h-[calc(100vh-64px)]">
<!-- Main chat area -->
<div class="flex flex-col flex-1 min-w-0">
<div class="px-4 border-b border-gray-200 dark:border-gray-700 box-border h-[57px] flex items-center">
<slot name="left-menu-btn"></slot>
<ChatConfigInfo v-if="instructionInfo" icon="i-iconoir-terminal"
:title="instructionInfo.name"
:description="instructionInfo.instruction"
class="hidden md:block" />
<ChatConfigInfo v-if="knowledgeBaseInfo" icon="i-heroicons-book-open"
:title="knowledgeBaseInfo.name"
class="mx-2 hidden md:block" />
<div class="mx-auto px-4 text-center">
<h2 class="line-clamp-1">{{ sessionInfo?.title || t('chat.untitled') }}</h2>
<div class="text-xs text-muted line-clamp-1">{{ instructionInfo?.name }}</div>
</div>
<UTooltip v-if="sessionId" :text="t('chat.modifyTips')">
<UButton icon="i-iconoir-edit-pencil" color="gray" @click="onOpenSettings" />
</UTooltip>
</div>
<UTooltip v-if="sessionId" :text="t('chat.modifyTips')">
<UButton icon="i-iconoir-edit-pencil" color="gray" @click="onOpenSettings" />
</UTooltip>
</div>
<div ref="messageListEl" class="relative flex-1 overflow-x-hidden overflow-y-auto px-4">
<ChatMessageItem v-for="message in visibleMessages" :key="message.id"
:message :sending="sendingCount > 0" :show-toggle-button="models.length > 1"
class="my-2" @resend="onResend" @remove="onRemove" />
</div>
<div class="shrink-0 pt-4 px-4 border-t border-gray-200 dark:border-gray-800">
<ChatInputBox ref="chatInputBoxRef"
:disabled="models.length === 0" :loading="sendingCount > 0"
@submit="onSend" @stop="onAbortChat">
<div class="text-muted flex">
<div class="mr-4">
<ModelsMultiSelectMenu v-model="models" @change="onModelsChange" />
</div>
<UTooltip :text="t('chat.attachedMessagesCount')" :popper="{ placement: 'top-start' }">
<div class="flex items-center cursor-pointer hover:text-primary-400" @click="onOpenSettings">
<UIcon name="i-material-symbols-history" class="mr-1"></UIcon>
<span class="text-sm">{{ sessionInfo?.attachedMessagesCount }}</span>
<div ref="messageListEl" class="relative flex-1 overflow-x-hidden overflow-y-auto px-4">
<ChatMessageItem v-for="message in visibleMessages" :key="message.id"
:message :sending="sendingCount > 0" :show-toggle-button="models.length > 1"
:is-previewing="showPreview && message.content === previewContent"
class="my-2" @resend="onResend" @remove="onRemove" @preview="onPreviewRequest" />
</div>
<div class="shrink-0 pt-4 px-4 border-t border-gray-200 dark:border-gray-800">
<ChatInputBox ref="chatInputBoxRef"
:disabled="models.length === 0" :loading="sendingCount > 0"
@submit="onSend" @stop="onAbortChat">
<div class="text-muted flex">
<div class="mr-4">
<ModelsMultiSelectMenu v-model="models" @change="onModelsChange" />
</div>
</UTooltip>
</div>
</ChatInputBox>
<UTooltip :text="t('chat.attachedMessagesCount')" :popper="{ placement: 'top-start' }">
<div class="flex items-center cursor-pointer hover:text-primary-400" @click="onOpenSettings">
<UIcon name="i-material-symbols-history" class="mr-1"></UIcon>
<span class="text-sm">{{ sessionInfo?.attachedMessagesCount }}</span>
</div>
</UTooltip>
</div>
</ChatInputBox>
</div>
</div>

<!-- Preview panel -->
<MarkdownPreview
:content="previewContent"
:show="showPreview"
@close="showPreview = false" />
</div>
</template>
170 changes: 105 additions & 65 deletions components/ChatMessageItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,101 +2,141 @@
import type { ChatMessage } from '~/types/chat'
const props = defineProps<{
message: ChatMessage
sending: boolean
showToggleButton?: boolean
message: ChatMessage
sending: boolean
showToggleButton?: boolean
isPreviewing?: boolean
}>()
const emits = defineEmits<{
resend: [message: ChatMessage]
remove: [message: ChatMessage]
resend: [message: ChatMessage]
remove: [message: ChatMessage]
preview: [content: string]
}>()
const markdown = useMarkdown()
const opened = ref(props.showToggleButton === true ? false : true)
const isModelMessage = computed(() => props.message.role === 'assistant')
const contentClass = computed(() => {
return [
isModelMessage.value ? 'max-w-[calc(100%-2rem)]' : 'max-w-full',
props.message.type === 'error'
? 'bg-red-50 dark:bg-red-800/60'
: (isModelMessage.value ? 'bg-gray-50 dark:bg-gray-800' : 'bg-primary-50 dark:bg-primary-400/60'),
]
return [
isModelMessage.value ? 'max-w-[calc(100%-2rem)]' : 'max-w-full',
props.message.type === 'error'
? 'bg-red-50 dark:bg-red-800/60'
: (isModelMessage.value ? 'bg-gray-50 dark:bg-gray-800' : 'bg-primary-50 dark:bg-primary-400/60'),
]
})
const timeUsed = computed(() => {
const endTime = props.message.type === 'loading' ? Date.now() : props.message.endTime
return Number(((endTime - props.message.startTime) / 1000).toFixed(1))
const endTime = props.message.type === 'loading' ? Date.now() : props.message.endTime
return Number(((endTime - props.message.startTime) / 1000).toFixed(1))
})
const modelName = computed(() => {
return parseModelValue(props.message.model)
return parseModelValue(props.message.model)
})
watch(() => props.showToggleButton, (value) => {
opened.value = value === true ? false : true
opened.value = value === true ? false : true
})
const showPreview = ref(false)
const togglePreview = () => {
emits('preview', props.message.content)
}
const contentDisplay = computed(() => {
if (props.isPreviewing && isModelMessage.value) {
return 'preview-mode'
}
return props.message.type === 'loading' ? 'loading' : 'normal'
})
</script>

<template>
<div class="flex flex-col my-2"
:class="{ 'items-end': message.role === 'user' }">
<div class="text-gray-500 dark:text-gray-400 p-1">
<Icon v-if="message.role === 'user'" name="i-material-symbols-account-circle" class="text-lg" />
<div v-else class="text-sm flex items-center">
<UTooltip :text="modelName.family" :popper="{ placement: 'top' }">
<span class="text-primary/80">{{ modelName.name }}</span>
</UTooltip>
<template v-if="timeUsed > 0">
<span class="mx-2 text-muted/20 text-xs">|</span>
<span class="text-gray-400 dark:text-gray-500 text-xs">{{ timeUsed }}s</span>
</template>
</div>
</div>
<div class="leading-6 text-sm flex items-center max-w-full message-content"
:class="{ 'text-gray-400 dark:text-gray-500': message.type === 'canceled', 'flex-row-reverse': !isModelMessage }">
<div class="flex border border-primary/20 rounded-lg overflow-hidden box-border"
:class="contentClass">
<div v-if="message.type === 'loading'" class="text-xl text-primary p-3">
<span class="block i-svg-spinners-3-dots-scale"></span>
<div class="flex flex-col my-2"
:class="{ 'items-end': message.role === 'user' }">
<div class="text-gray-500 dark:text-gray-400 p-1">
<Icon v-if="message.role === 'user'" name="i-material-symbols-account-circle" class="text-lg" />
<div v-else class="text-sm flex items-center">
<UTooltip :text="modelName.family" :popper="{ placement: 'top' }">
<span class="text-primary/80">{{ modelName.name }}</span>
</UTooltip>
<template v-if="timeUsed > 0">
<span class="mx-2 text-muted/20 text-xs">|</span>
<span class="text-gray-400 dark:text-gray-500 text-xs">{{ timeUsed }}s</span>
</template>
</div>
</div>
<div class="leading-6 text-sm flex items-center max-w-full message-content"
:class="{ 'text-gray-400 dark:text-gray-500': message.type === 'canceled', 'flex-row-reverse': !isModelMessage }">
<div class="flex border border-primary/20 rounded-lg overflow-hidden box-border"
:class="contentClass">
<div v-if="contentDisplay === 'loading'" class="text-xl text-primary p-3">
<span class="block i-svg-spinners-3-dots-scale"></span>
</div>
<div v-else-if="contentDisplay === 'preview-mode'" class="p-3 flex items-center text-gray-500">
<UIcon name="i-heroicons-document-text" class="mr-2" />
<span>Content in preview</span>
</div>
<template v-else-if="isModelMessage">
<div class="p-3 overflow-hidden">
<div v-html="markdown.render(message.content || '')" class="md-body" :class="{ 'line-clamp-3 max-h-[5rem]': !opened }" />
<Sources v-show="opened" :relevant_documents="message?.relevantDocs || []" />
</div>
<div class="flex flex-col">
<MessageToggleCollapseButton v-if="showToggleButton" :opened="opened" @click="opened = !opened" />
<UButton v-if="message.content"
icon="i-heroicons-eye-20-solid"
color="gray"
variant="ghost"
size="xs"
class="mt-1 preview-btn"
:class="{ 'text-primary-500': isPreviewing }"
@click="togglePreview" />
</div>
</template>
<pre v-else v-text="message.content" class="p-3 whitespace-break-spaces" />
</div>
<ChatMessageActionMore :message="message"
:disabled="sending"
@resend="emits('resend', message)"
@remove="emits('remove', message)">
<UButton :class="{ invisible: sending }" icon="i-material-symbols-more-vert" color="gray"
:variant="'link'"
class="action-more">
</UButton>
</ChatMessageActionMore>
</div>
<template v-else-if="isModelMessage">
<div class="p-3 overflow-hidden">
<div v-html="markdown.render(message.content || '')" class="md-body" :class="{ 'line-clamp-3 max-h-[5rem]': !opened }" />
<Sources v-show="opened" :relevant_documents="message?.relevantDocs || []" />
</div>
<MessageToggleCollapseButton v-if="showToggleButton" :opened="opened" @click="opened = !opened" />
</template>
<pre v-else v-text="message.content" class="p-3 whitespace-break-spaces" />
</div>
<ChatMessageActionMore :message="message"
:disabled="sending"
@resend="emits('resend', message)"
@remove="emits('remove', message)">
<UButton :class="{ invisible: sending }" icon="i-material-symbols-more-vert" color="gray"
:variant="'link'"
class="action-more">
</UButton>
</ChatMessageActionMore>
</div>
</div>
</template>

<style scoped lang="scss">
.message-content {
.action-more {
transform-origin: center center;
transition: all 0.3s;
transform: scale(0);
opacity: 0;
}
&:hover {
.action-more {
transform: scale(1);
opacity: 1;
transform-origin: center center;
transition: all 0.3s;
transform: scale(0);
opacity: 0;
}
&:hover {
.action-more {
transform: scale(1);
opacity: 1;
}
}
.preview-btn {
opacity: 0;
transition: opacity 0.3s;
}
&:hover {
.preview-btn {
opacity: 1;
}
}
}
}
</style>
29 changes: 29 additions & 0 deletions components/MarkdownPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
const props = defineProps<{
content: string
show: boolean
}>()
const emits = defineEmits<{
close: []
}>()
const markdown = useMarkdown()
</script>

<template>
<div v-show="show"
class="w-[400px] border-l dark:border-gray-800 flex flex-col h-[calc(100vh-64px)] shrink-0">
<div class="p-4 border-b dark:border-gray-800 flex items-center">
<span class="mr-auto font-bold">Preview</span>
<UButton icon="i-material-symbols-close-rounded"
color="gray"
variant="ghost"
size="sm"
@click="emits('close')" />
</div>
<div class="flex-1 overflow-y-auto p-4">
<div class="md-body" v-html="markdown.render(content || '')" />
</div>
</div>
</template>

0 comments on commit 93d0e06

Please sign in to comment.