| <template> |
| <v-card class="chat-page-card" elevation="0" rounded="0"> |
| <v-card-text class="chat-page-container"> |
| |
| <div class="mobile-overlay" v-if="isMobile && mobileMenuOpen" @click="closeMobileSidebar"></div> |
|
|
| <div class="chat-layout"> |
| <ConversationSidebar |
| :sessions="sessions" |
| :selectedSessions="selectedSessions" |
| :currSessionId="currSessionId" |
| :selectedProjectId="selectedProjectId" |
| :transportMode="transportMode" |
| :isDark="isDark" |
| :chatboxMode="chatboxMode" |
| :isMobile="isMobile" |
| :mobileMenuOpen="mobileMenuOpen" |
| :projects="projects" |
| @newChat="handleNewChat" |
| @selectConversation="handleSelectConversation" |
| @editTitle="showEditTitleDialog" |
| @deleteConversation="handleDeleteConversation" |
| @closeMobileSidebar="closeMobileSidebar" |
| @toggleTheme="toggleTheme" |
| @toggleFullscreen="toggleFullscreen" |
| @selectProject="handleSelectProject" |
| @createProject="showCreateProjectDialog" |
| @editProject="showEditProjectDialog" |
| @deleteProject="handleDeleteProject" |
| @updateTransportMode="setTransportMode" |
| /> |
|
|
| |
| <div class="chat-content-panel"> |
| |
| <LiveMode v-if="liveModeOpen" @close="closeLiveMode" /> |
|
|
| |
| <template v-else> |
|
|
| <div v-if="currentSessionProject && messages && messages.length > 0" class="breadcrumb-container"> |
| <div class="breadcrumb-content"> |
| <span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span> |
| <span class="breadcrumb-project" @click="handleSelectProject(currentSessionProject.project_id)">{{ currentSessionProject.title }}</span> |
| <v-icon size="small" class="breadcrumb-separator">mdi-chevron-right</v-icon> |
| <span class="breadcrumb-session">{{ getCurrentSession?.display_name || tm('conversation.newConversation') }}</span> |
| </div> |
| </div> |
|
|
| <div class="message-list-wrapper" v-if="currSessionId && !selectedProjectId"> |
| <MessageList :messages="messages" :isDark="isDark" |
| :isStreaming="isStreaming || isConvRunning" |
| :isLoadingMessages="isLoadingMessages" |
| @openImagePreview="openImagePreview" |
| @replyMessage="handleReplyMessage" |
| @replyWithText="handleReplyWithText" |
| @openRefs="handleOpenRefs" |
| ref="messageList" /> |
| <div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div> |
| </div> |
| <ProjectView |
| v-else-if="selectedProjectId" |
| :project="currentProject" |
| :sessions="projectSessions" |
| @selectSession="(sessionId) => handleSelectConversation([sessionId])" |
| @editSessionTitle="showEditTitleDialog" |
| @deleteSession="handleDeleteConversation" |
| > |
| <ChatInput |
| v-model:prompt="prompt" |
| :stagedImagesUrl="stagedImagesUrl" |
| :stagedAudioUrl="stagedAudioUrl" |
| :stagedFiles="stagedNonImageFiles" |
| :disabled="isStreaming" |
| :is-running="isStreaming || isConvRunning" |
| :enableStreaming="enableStreaming" |
| :isRecording="isRecording" |
| :session-id="currSessionId || null" |
| :current-session="getCurrentSession" |
| :replyTo="replyTo" |
| @send="handleSendMessage" |
| @stop="handleStopMessage" |
| @toggleStreaming="toggleStreaming" |
| @removeImage="removeImage" |
| @removeAudio="removeAudio" |
| @removeFile="removeFile" |
| @startRecording="handleStartRecording" |
| @stopRecording="handleStopRecording" |
| @pasteImage="handlePaste" |
| @fileSelect="handleFileSelect" |
| @clearReply="clearReply" |
| @openLiveMode="openLiveMode" |
| ref="chatInputRef" |
| /> |
| </ProjectView> |
| <WelcomeView |
| v-else |
| :isLoading="isLoadingMessages" |
| > |
| <ChatInput |
| v-model:prompt="prompt" |
| :stagedImagesUrl="stagedImagesUrl" |
| :stagedAudioUrl="stagedAudioUrl" |
| :stagedFiles="stagedNonImageFiles" |
| :disabled="isStreaming" |
| :is-running="isStreaming || isConvRunning" |
| :enableStreaming="enableStreaming" |
| :isRecording="isRecording" |
| :session-id="currSessionId || null" |
| :current-session="getCurrentSession" |
| :replyTo="replyTo" |
| @send="handleSendMessage" |
| @stop="handleStopMessage" |
| @toggleStreaming="toggleStreaming" |
| @removeImage="removeImage" |
| @removeAudio="removeAudio" |
| @removeFile="removeFile" |
| @startRecording="handleStartRecording" |
| @stopRecording="handleStopRecording" |
| @pasteImage="handlePaste" |
| @fileSelect="handleFileSelect" |
| @clearReply="clearReply" |
| @openLiveMode="openLiveMode" |
| ref="chatInputRef" |
| /> |
| </WelcomeView> |
|
|
| |
| <ChatInput |
| v-if="currSessionId && !selectedProjectId" |
| v-model:prompt="prompt" |
| :stagedImagesUrl="stagedImagesUrl" |
| :stagedAudioUrl="stagedAudioUrl" |
| :stagedFiles="stagedNonImageFiles" |
| :disabled="isStreaming" |
| :is-running="isStreaming || isConvRunning" |
| :enableStreaming="enableStreaming" |
| :isRecording="isRecording" |
| :session-id="currSessionId || null" |
| :current-session="getCurrentSession" |
| :replyTo="replyTo" |
| @send="handleSendMessage" |
| @stop="handleStopMessage" |
| @toggleStreaming="toggleStreaming" |
| @removeImage="removeImage" |
| @removeAudio="removeAudio" |
| @removeFile="removeFile" |
| @startRecording="handleStartRecording" |
| @stopRecording="handleStopRecording" |
| @pasteImage="handlePaste" |
| @fileSelect="handleFileSelect" |
| @clearReply="clearReply" |
| @openLiveMode="openLiveMode" |
| ref="chatInputRef" |
| /> |
| </template> |
| </div> |
|
|
| |
| <RefsSidebar v-model="refsSidebarOpen" :refs="refsSidebarRefs" /> |
| </div> |
| </v-card-text> |
| </v-card> |
| |
| |
| <v-dialog v-model="editTitleDialog" max-width="400"> |
| <v-card> |
| <v-card-title class="dialog-title">{{ tm('actions.editTitle') }}</v-card-title> |
| <v-card-text> |
| <v-text-field v-model="editingTitle" :label="tm('conversation.newConversation')" variant="outlined" |
| hide-details class="mt-2" @keyup.enter="handleSaveTitle" autofocus /> |
| </v-card-text> |
| <v-card-actions> |
| <v-spacer></v-spacer> |
| <v-btn variant="text" @click="editTitleDialog = false" color="grey-darken-1">{{ t('core.common.cancel') }}</v-btn> |
| <v-btn variant="text" @click="handleSaveTitle" color="primary">{{ t('core.common.save') }}</v-btn> |
| </v-card-actions> |
| </v-card> |
| </v-dialog> |
|
|
| |
| <v-dialog v-model="imagePreviewDialog" max-width="90vw" max-height="90vh"> |
| <v-card class="image-preview-card" elevation="8"> |
| <v-card-title class="d-flex justify-space-between align-center pa-4"> |
| <span>{{ t('core.common.imagePreview') }}</span> |
| <v-btn icon="mdi-close" variant="text" @click="imagePreviewDialog = false" /> |
| </v-card-title> |
| <v-card-text class="text-center pa-4"> |
| <img :src="previewImageUrl" class="preview-image-large" /> |
| </v-card-text> |
| </v-card> |
| </v-dialog> |
|
|
| |
| <ProjectDialog |
| v-model="projectDialog" |
| :project="editingProject" |
| @save="handleSaveProject" |
| /> |
| </template> |
|
|
| <script setup lang="ts"> |
| import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'; |
| import { useRouter, useRoute } from 'vue-router'; |
| import { useCustomizerStore } from '@/stores/customizer'; |
| import { useI18n, useModuleI18n } from '@/i18n/composables'; |
| import { useTheme } from 'vuetify'; |
| import MessageList from '@/components/chat/MessageList.vue'; |
| import ConversationSidebar from '@/components/chat/ConversationSidebar.vue'; |
| import ChatInput from '@/components/chat/ChatInput.vue'; |
| import ProjectDialog from '@/components/chat/ProjectDialog.vue'; |
| import ProjectView from '@/components/chat/ProjectView.vue'; |
| import WelcomeView from '@/components/chat/WelcomeView.vue'; |
| import RefsSidebar from '@/components/chat/message_list_comps/RefsSidebar.vue'; |
| import LiveMode from '@/components/chat/LiveMode.vue'; |
| import type { ProjectFormData } from '@/components/chat/ProjectDialog.vue'; |
| import { useSessions } from '@/composables/useSessions'; |
| import { useMessages } from '@/composables/useMessages'; |
| import { useMediaHandling } from '@/composables/useMediaHandling'; |
| import { useProjects } from '@/composables/useProjects'; |
| import type { Project } from '@/components/chat/ProjectList.vue'; |
| import { useRecording } from '@/composables/useRecording'; |
| |
| interface Props { |
| chatboxMode?: boolean; |
| } |
| |
| const props = withDefaults(defineProps<Props>(), { |
| chatboxMode: false |
| }); |
| |
| const router = useRouter(); |
| const route = useRoute(); |
| const { t } = useI18n(); |
| const { tm } = useModuleI18n('features/chat'); |
| const theme = useTheme(); |
| const customizer = useCustomizerStore(); |
| |
| |
| const isMobile = ref(false); |
| const mobileMenuOpen = ref(false); |
| const imagePreviewDialog = ref(false); |
| const previewImageUrl = ref(''); |
| const isLoadingMessages = ref(false); |
| const liveModeOpen = ref(false); |
| |
| |
| const { |
| sessions, |
| selectedSessions, |
| currSessionId, |
| pendingSessionId, |
| editTitleDialog, |
| editingTitle, |
| editingSessionId, |
| getCurrentSession, |
| getSessions, |
| newSession, |
| deleteSession: deleteSessionFn, |
| showEditTitleDialog, |
| saveTitle, |
| updateSessionTitle, |
| newChat |
| } = useSessions(props.chatboxMode); |
| |
| const { |
| stagedImagesUrl, |
| stagedAudioUrl, |
| stagedFiles, |
| stagedNonImageFiles, |
| getMediaFile, |
| processAndUploadImage, |
| processAndUploadFile, |
| handlePaste, |
| removeImage, |
| removeAudio, |
| removeFile, |
| clearStaged, |
| cleanupMediaCache |
| } = useMediaHandling(); |
| |
| const { isRecording: isRecording, startRecording: startRec, stopRecording: stopRec } = useRecording(); |
| |
| const { |
| projects, |
| selectedProjectId, |
| getProjects, |
| createProject, |
| updateProject, |
| deleteProject, |
| addSessionToProject, |
| getProjectSessions |
| } = useProjects(); |
| |
| const { |
| messages, |
| isStreaming, |
| isConvRunning, |
| enableStreaming, |
| transportMode, |
| currentSessionProject, |
| getSessionMessages: getSessionMsg, |
| sendMessage: sendMsg, |
| stopMessage: stopMsg, |
| toggleStreaming, |
| setTransportMode, |
| cleanupTransport |
| } = useMessages(currSessionId, getMediaFile, updateSessionTitle, getSessions); |
| |
| |
| const messageList = ref<InstanceType<typeof MessageList> | null>(null); |
| const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null); |
| |
| |
| const prompt = ref(''); |
| |
| |
| const projectDialog = ref(false); |
| const editingProject = ref<Project | null>(null); |
| const projectSessions = ref<any[]>([]); |
| const currentProject = computed(() => |
| projects.value.find(p => p.project_id === selectedProjectId.value) |
| ); |
| |
| |
| interface ReplyInfo { |
| messageId: number; |
| selectedText?: string; |
| } |
| const replyTo = ref<ReplyInfo | null>(null); |
| |
| const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark'); |
| |
| |
| function checkMobile() { |
| isMobile.value = window.innerWidth <= 768; |
| if (!isMobile.value) { |
| mobileMenuOpen.value = false; |
| customizer.SET_CHAT_SIDEBAR(false); |
| } |
| } |
| |
| function toggleMobileSidebar() { |
| mobileMenuOpen.value = !mobileMenuOpen.value; |
| customizer.SET_CHAT_SIDEBAR(mobileMenuOpen.value); |
| } |
| |
| function closeMobileSidebar() { |
| mobileMenuOpen.value = false; |
| customizer.SET_CHAT_SIDEBAR(false); |
| } |
| |
| |
| watch(() => customizer.chatSidebarOpen, (val) => { |
| if (isMobile.value) { |
| mobileMenuOpen.value = val; |
| } |
| }); |
| |
| function toggleTheme() { |
| const newTheme = customizer.uiTheme === 'PurpleTheme' ? 'PurpleThemeDark' : 'PurpleTheme'; |
| customizer.SET_UI_THEME(newTheme); |
| theme.global.name.value = newTheme; |
| } |
| |
| function toggleFullscreen() { |
| if (props.chatboxMode) { |
| router.push(currSessionId.value ? `/chat/${currSessionId.value}` : '/chat'); |
| } else { |
| router.push(currSessionId.value ? `/chatbox/${currSessionId.value}` : '/chatbox'); |
| } |
| } |
| |
| function openImagePreview(imageUrl: string) { |
| previewImageUrl.value = imageUrl; |
| imagePreviewDialog.value = true; |
| } |
| |
| async function handleSaveTitle() { |
| await saveTitle(); |
| |
| |
| if (selectedProjectId.value) { |
| const sessions = await getProjectSessions(selectedProjectId.value); |
| projectSessions.value = sessions; |
| } |
| } |
| |
| function handleReplyMessage(msg: any, index: number) { |
| |
| const messageId = msg.id; |
| if (!messageId) { |
| console.warn('Message does not have an id'); |
| return; |
| } |
| |
| |
| let messageContent = ''; |
| if (typeof msg.content.message === 'string') { |
| messageContent = msg.content.message; |
| } else if (Array.isArray(msg.content.message)) { |
| |
| const textParts = msg.content.message |
| .filter((part: any) => part.type === 'plain' && part.text) |
| .map((part: any) => part.text); |
| messageContent = textParts.join(''); |
| } |
| |
| |
| if (messageContent.length > 100) { |
| messageContent = messageContent.substring(0, 100) + '...'; |
| } |
| |
| replyTo.value = { |
| messageId, |
| selectedText: messageContent || '[媒体内容]' |
| }; |
| } |
| |
| function clearReply() { |
| replyTo.value = null; |
| } |
| |
| function handleReplyWithText(replyData: any) { |
| |
| const { messageId, selectedText, messageIndex } = replyData; |
| |
| if (!messageId) { |
| console.warn('Message does not have an id'); |
| return; |
| } |
| |
| replyTo.value = { |
| messageId, |
| selectedText: selectedText |
| }; |
| } |
| |
| |
| const refsSidebarOpen = ref(false); |
| const refsSidebarRefs = ref<any>(null); |
| |
| function handleOpenRefs(refs: any) { |
| |
| if (refsSidebarOpen.value && refsSidebarRefs.value === refs) { |
| refsSidebarOpen.value = false; |
| } else { |
| |
| refsSidebarRefs.value = refs; |
| refsSidebarOpen.value = true; |
| } |
| } |
| |
| async function handleSelectConversation(sessionIds: string[]) { |
| if (!sessionIds[0]) return; |
| |
| |
| selectedProjectId.value = null; |
| projectSessions.value = []; |
| |
| |
| currSessionId.value = sessionIds[0]; |
| selectedSessions.value = [sessionIds[0]]; |
| |
| |
| const basePath = props.chatboxMode ? '/chatbox' : '/chat'; |
| if (route.path !== `${basePath}/${sessionIds[0]}`) { |
| router.push(`${basePath}/${sessionIds[0]}`); |
| } |
| |
| |
| if (isMobile.value) { |
| closeMobileSidebar(); |
| } |
| |
| |
| clearReply(); |
| |
| |
| isLoadingMessages.value = true; |
| |
| try { |
| await getSessionMsg(sessionIds[0]); |
| } finally { |
| isLoadingMessages.value = false; |
| } |
| |
| nextTick(() => { |
| messageList.value?.scrollToBottom(); |
| }); |
| } |
| |
| function handleNewChat() { |
| newChat(closeMobileSidebar); |
| messages.value = []; |
| clearReply(); |
| |
| selectedProjectId.value = null; |
| projectSessions.value = []; |
| } |
| |
| async function handleDeleteConversation(sessionId: string) { |
| await deleteSessionFn(sessionId); |
| messages.value = []; |
| |
| |
| if (selectedProjectId.value) { |
| const sessions = await getProjectSessions(selectedProjectId.value); |
| projectSessions.value = sessions; |
| } |
| } |
| |
| async function handleSelectProject(projectId: string) { |
| selectedProjectId.value = projectId; |
| const sessions = await getProjectSessions(projectId); |
| projectSessions.value = sessions; |
| messages.value = []; |
| |
| |
| currSessionId.value = ''; |
| selectedSessions.value = []; |
| |
| |
| if (isMobile.value) { |
| closeMobileSidebar(); |
| } |
| } |
| |
| function showCreateProjectDialog() { |
| editingProject.value = null; |
| projectDialog.value = true; |
| } |
| |
| function showEditProjectDialog(project: Project) { |
| editingProject.value = project; |
| projectDialog.value = true; |
| } |
| |
| async function handleSaveProject(formData: ProjectFormData, projectId?: string) { |
| if (projectId) { |
| await updateProject( |
| projectId, |
| formData.title, |
| formData.emoji, |
| formData.description |
| ); |
| } else { |
| await createProject( |
| formData.title, |
| formData.emoji, |
| formData.description |
| ); |
| } |
| } |
| |
| async function handleDeleteProject(projectId: string) { |
| await deleteProject(projectId); |
| } |
| |
| async function handleStartRecording() { |
| await startRec(); |
| } |
| |
| async function handleStopRecording() { |
| const audioFilename = await stopRec(); |
| stagedAudioUrl.value = audioFilename; |
| } |
| |
| async function handleFileSelect(files: FileList) { |
| const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; |
| |
| const fileArray = Array.from(files); |
| for (let i = 0; i < fileArray.length; i++) { |
| const file = fileArray[i]; |
| if (imageTypes.includes(file.type)) { |
| await processAndUploadImage(file); |
| } else { |
| await processAndUploadFile(file); |
| } |
| } |
| } |
| |
| function openLiveMode() { |
| liveModeOpen.value = true; |
| } |
| |
| function closeLiveMode() { |
| liveModeOpen.value = false; |
| } |
| |
| async function handleSendMessage() { |
| |
| if (!prompt.value.trim() && stagedFiles.value.length === 0 && !stagedAudioUrl.value) { |
| return; |
| } |
| |
| const isCreatingNewSession = !currSessionId.value; |
| const currentProjectId = selectedProjectId.value; |
| |
| if (isCreatingNewSession) { |
| await newSession(); |
| |
| |
| if (currentProjectId) { |
| selectedProjectId.value = null; |
| projectSessions.value = []; |
| } |
| } |
| |
| const promptToSend = prompt.value.trim(); |
| const audioNameToSend = stagedAudioUrl.value; |
| const filesToSend = stagedFiles.value.map(f => ({ |
| attachment_id: f.attachment_id, |
| url: f.url, |
| original_name: f.original_name, |
| type: f.type |
| })); |
| const replyToSend = replyTo.value ? { ...replyTo.value } : null; |
| |
| |
| prompt.value = ''; |
| clearStaged(); |
| clearReply(); |
| |
| |
| const selection = chatInputRef.value?.getCurrentSelection(); |
| const selectedProviderId = selection?.providerId || ''; |
| const selectedModelName = selection?.modelName || ''; |
| |
| await sendMsg( |
| promptToSend, |
| filesToSend, |
| audioNameToSend, |
| selectedProviderId, |
| selectedModelName, |
| replyToSend |
| ); |
| |
| |
| if (isCreatingNewSession && currentProjectId && currSessionId.value) { |
| await addSessionToProject(currSessionId.value, currentProjectId); |
| |
| await getSessions(); |
| |
| await getSessionMsg(currSessionId.value); |
| } |
| } |
| |
| async function handleStopMessage() { |
| await stopMsg(); |
| } |
| |
| |
| watch( |
| () => route.path, |
| (to, from) => { |
| if (from && |
| ((from.startsWith('/chat') && to.startsWith('/chatbox')) || |
| (from.startsWith('/chatbox') && to.startsWith('/chat')))) { |
| return; |
| } |
| |
| if (to.startsWith('/chat/') || to.startsWith('/chatbox/')) { |
| const pathSessionId = to.split('/')[2]; |
| if (pathSessionId && pathSessionId !== currSessionId.value) { |
| if (sessions.value.length > 0) { |
| const session = sessions.value.find(s => s.session_id === pathSessionId); |
| if (session) { |
| handleSelectConversation([pathSessionId]); |
| } |
| } else { |
| pendingSessionId.value = pathSessionId; |
| } |
| } |
| } |
| }, |
| { immediate: true } |
| ); |
| |
| |
| watch(sessions, (newSessions) => { |
| if (pendingSessionId.value && newSessions.length > 0) { |
| const session = newSessions.find(s => s.session_id === pendingSessionId.value); |
| if (session) { |
| selectedSessions.value = [pendingSessionId.value]; |
| handleSelectConversation([pendingSessionId.value]); |
| pendingSessionId.value = null; |
| } |
| } else if (!currSessionId.value && newSessions.length > 0) { |
| const firstSession = newSessions[0]; |
| selectedSessions.value = [firstSession.session_id]; |
| handleSelectConversation([firstSession.session_id]); |
| } |
| }); |
| |
| onMounted(() => { |
| checkMobile(); |
| window.addEventListener('resize', checkMobile); |
| getSessions(); |
| getProjects(); |
| }); |
| |
| onBeforeUnmount(() => { |
| window.removeEventListener('resize', checkMobile); |
| cleanupMediaCache(); |
| cleanupTransport(); |
| }); |
| </script> |
|
|
| <style scoped> |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .chat-page-card { |
| width: 100%; |
| height: 100%; |
| max-height: 100%; |
| overflow: hidden; |
| overscroll-behavior: none; |
| } |
| |
| .chat-page-container { |
| width: 100%; |
| height: 100%; |
| max-height: 100%; |
| padding: 0; |
| overflow: hidden; |
| } |
| |
| .chat-layout { |
| height: 100%; |
| max-height: 100%; |
| display: flex; |
| overflow: hidden; |
| } |
| |
| |
| .mobile-overlay { |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background-color: rgba(0, 0, 0, 0.5); |
| z-index: 999; |
| animation: fadeIn 0.3s ease; |
| } |
| |
| .chat-content-panel { |
| height: 100%; |
| max-height: 100%; |
| width: 100%; |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| } |
| |
| .message-list-wrapper { |
| flex: 1; |
| position: relative; |
| overflow: hidden; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .message-list-fade { |
| position: absolute; |
| bottom: 0; |
| left: 0; |
| right: 0; |
| height: 40px; |
| background: linear-gradient(to top, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); |
| pointer-events: none; |
| z-index: 1; |
| } |
| |
| .message-list-fade.fade-dark { |
| background: linear-gradient(to top, rgba(30, 30, 30, 1) 0%, rgba(30, 30, 30, 0) 100%); |
| } |
| |
| .conversation-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 8px; |
| padding-left: 16px; |
| border-bottom: 1px solid var(--v-theme-border); |
| width: 100%; |
| padding-right: 32px; |
| flex-shrink: 0; |
| } |
| |
| .mobile-menu-btn { |
| margin-right: 8px; |
| } |
| |
| .conversation-header-actions { |
| display: flex; |
| gap: 8px; |
| align-items: center; |
| } |
| |
| .fullscreen-icon { |
| cursor: pointer; |
| margin-left: 8px; |
| } |
| |
| .breadcrumb-container { |
| padding: 8px 16px; |
| border-bottom: 1px solid var(--v-theme-border); |
| flex-shrink: 0; |
| } |
| |
| .breadcrumb-content { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-size: 14px; |
| } |
| |
| .breadcrumb-emoji { |
| font-size: 16px; |
| } |
| |
| .breadcrumb-project { |
| font-weight: 500; |
| cursor: pointer; |
| transition: opacity 0.2s; |
| } |
| |
| .breadcrumb-project:hover { |
| opacity: 0.7; |
| } |
| |
| .breadcrumb-separator { |
| opacity: 0.5; |
| } |
| |
| .breadcrumb-session { |
| opacity: 0.7; |
| } |
| |
| .fade-in { |
| animation: fadeIn 0.3s ease-in-out; |
| } |
| |
| .dialog-title { |
| font-size: 18px; |
| font-weight: 500; |
| padding-bottom: 8px; |
| } |
| |
| |
| @media (max-width: 768px) { |
| .chat-content-panel { |
| width: 100%; |
| } |
| |
| .chat-page-container { |
| padding: 0 !important; |
| } |
| |
| .conversation-header { |
| padding: 2px; |
| } |
| } |
| </style> |
|
|