fix: resolve streaming messages splitting into individual bubbles
Simplify addMessage/updateMessage to only write to messages.value, add syncMessagesToSession() to copy messages back on session switch and stream completion. Also fix mobile viewport, session list overlay, hamburger logo, and various responsive improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -45,7 +45,7 @@ useKeyboard()
|
|||||||
<NNotificationProvider>
|
<NNotificationProvider>
|
||||||
<div v-if="ready" class="app-layout" :class="{ 'no-sidebar': isLoginPage }">
|
<div v-if="ready" class="app-layout" :class="{ 'no-sidebar': isLoginPage }">
|
||||||
<button v-if="!isLoginPage" class="hamburger-btn" @click="appStore.toggleSidebar">
|
<button v-if="!isLoginPage" class="hamburger-btn" @click="appStore.toggleSidebar">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
<img src="/logo.png" alt="Menu" style="width: 24px; height: 24px;" />
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!isLoginPage && appStore.sidebarOpen" class="mobile-backdrop" @click="appStore.closeSidebar" />
|
<div v-if="!isLoginPage && appStore.sidebarOpen" class="mobile-backdrop" @click="appStore.closeSidebar" />
|
||||||
<AppSidebar v-if="!isLoginPage" />
|
<AppSidebar v-if="!isLoginPage" />
|
||||||
@@ -64,7 +64,7 @@ useKeyboard()
|
|||||||
|
|
||||||
.app-layout {
|
.app-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ useKeyboard()
|
|||||||
background-color: $bg-primary;
|
background-color: $bg-primary;
|
||||||
|
|
||||||
.no-sidebar & {
|
.no-sidebar & {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -379,6 +379,9 @@ function isImage(type: string): boolean {
|
|||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ const { t } = useI18n()
|
|||||||
const showSessions = ref(true)
|
const showSessions = ref(true)
|
||||||
let mobileQuery: MediaQueryList | null = null
|
let mobileQuery: MediaQueryList | null = null
|
||||||
|
|
||||||
|
function handleSessionClick(sessionId: string) {
|
||||||
|
chatStore.switchSession(sessionId)
|
||||||
|
if (mobileQuery?.matches) showSessions.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function handleMobileChange(e: MediaQueryListEvent | MediaQueryList) {
|
function handleMobileChange(e: MediaQueryListEvent | MediaQueryList) {
|
||||||
if (e.matches && showSessions.value) {
|
if (e.matches && showSessions.value) {
|
||||||
showSessions.value = false
|
showSessions.value = false
|
||||||
@@ -265,14 +270,20 @@ async function handleRenameConfirm() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-panel">
|
<div class="chat-panel">
|
||||||
<!-- Session List -->
|
<!-- Session List -->
|
||||||
|
<div class="session-backdrop" :class="{ active: showSessions }" @click="showSessions = false" />
|
||||||
<aside class="session-list" :class="{ collapsed: !showSessions }">
|
<aside class="session-list" :class="{ collapsed: !showSessions }">
|
||||||
<div class="session-list-header">
|
<div class="session-list-header">
|
||||||
<span v-if="showSessions" class="session-list-title">{{ t('chat.sessions') }}</span>
|
<span v-if="showSessions" class="session-list-title">{{ t('chat.sessions') }}</span>
|
||||||
<NButton quaternary size="tiny" @click="handleNewChat" circle>
|
<div class="session-list-actions">
|
||||||
|
<button class="session-close-btn" @click="showSessions = false">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
|
</button>
|
||||||
|
<NButton quaternary size="tiny" @click="handleNewChat" circle>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showSessions" class="session-items">
|
<div v-if="showSessions" class="session-items">
|
||||||
<div v-if="chatStore.isLoadingSessions && chatStore.sessions.length === 0" class="session-loading">{{ t('common.loading') }}</div>
|
<div v-if="chatStore.isLoadingSessions && chatStore.sessions.length === 0" class="session-loading">{{ t('common.loading') }}</div>
|
||||||
@@ -289,7 +300,7 @@ async function handleRenameConfirm() {
|
|||||||
:key="s.id"
|
:key="s.id"
|
||||||
class="session-item"
|
class="session-item"
|
||||||
:class="{ active: s.id === chatStore.activeSessionId }"
|
:class="{ active: s.id === chatStore.activeSessionId }"
|
||||||
@click="chatStore.switchSession(s.id)"
|
@click="handleSessionClick(s.id)"
|
||||||
@contextmenu="handleContextMenu($event, s.id)"
|
@contextmenu="handleContextMenu($event, s.id)"
|
||||||
>
|
>
|
||||||
<div class="session-item-content">
|
<div class="session-item-content">
|
||||||
@@ -392,6 +403,7 @@ async function handleRenameConfirm() {
|
|||||||
.chat-panel {
|
.chat-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-list {
|
.session-list {
|
||||||
@@ -409,6 +421,43 @@ async function handleRenameConfirm() {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-mobile) {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
background: $bg-card;
|
||||||
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 280px;
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-mobile) {
|
||||||
|
.session-close-btn {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 9;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity $transition-fast;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-list-header {
|
.session-list-header {
|
||||||
@@ -419,6 +468,26 @@ async function handleRenameConfirm() {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-list-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-close-btn {
|
||||||
|
display: none;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $text-secondary;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: $radius-sm;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba($accent-primary, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.session-list-title {
|
.session-list-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -546,7 +615,7 @@ async function handleRenameConfirm() {
|
|||||||
|
|
||||||
.session-item-delete {
|
.session-item-delete {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
opacity: 0;
|
opacity: 0.5;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
|
|||||||
@@ -469,16 +469,16 @@ const formattedToolResult = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-mobile) {
|
@media (max-width: $breakpoint-mobile) {
|
||||||
.msg-user .msg-body {
|
.message.user .msg-body {
|
||||||
max-width: 90%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-assistant .msg-body {
|
.message.assistant .msg-body {
|
||||||
max-width: 92%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-system .msg-body {
|
.message.system .msg-body {
|
||||||
max-width: 92%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ function handleNav(key: string) {
|
|||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: $sidebar-width;
|
width: $sidebar-width;
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
background-color: $bg-sidebar;
|
background-color: $bg-sidebar;
|
||||||
border-right: 1px solid $border-color;
|
border-right: 1px solid $border-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -448,6 +448,12 @@ function handleNav(key: string) {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -459,6 +465,11 @@ function handleNav(key: string) {
|
|||||||
&.open {
|
&.open {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override global utility — sidebar is always 240px wide
|
||||||
|
.input-sm {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+41
-15
@@ -1,7 +1,7 @@
|
|||||||
import { startRun, streamRunEvents, type ChatMessage, type RunEvent } from '@/api/chat'
|
import { startRun, streamRunEvents, type ChatMessage, type RunEvent } from '@/api/chat'
|
||||||
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, type HermesMessage, type SessionSummary } from '@/api/sessions'
|
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, type HermesMessage, type SessionSummary } from '@/api/sessions'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useAppStore } from './app'
|
import { useAppStore } from './app'
|
||||||
|
|
||||||
export interface Attachment {
|
export interface Attachment {
|
||||||
@@ -157,8 +157,10 @@ function mapHermesSession(s: SessionSummary): Session {
|
|||||||
export const useChatStore = defineStore('chat', () => {
|
export const useChatStore = defineStore('chat', () => {
|
||||||
const sessions = ref<Session[]>([])
|
const sessions = ref<Session[]>([])
|
||||||
const activeSessionId = ref<string | null>(null)
|
const activeSessionId = ref<string | null>(null)
|
||||||
const isStreaming = ref(false)
|
const streamSessionId = ref<string | null>(null)
|
||||||
|
const _isStreaming = ref(false)
|
||||||
const abortController = ref<AbortController | null>(null)
|
const abortController = ref<AbortController | null>(null)
|
||||||
|
const isStreaming = computed(() => _isStreaming.value && activeSessionId.value === streamSessionId.value)
|
||||||
const isLoadingSessions = ref(false)
|
const isLoadingSessions = ref(false)
|
||||||
const isLoadingMessages = ref(false)
|
const isLoadingMessages = ref(false)
|
||||||
|
|
||||||
@@ -196,6 +198,10 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function switchSession(sessionId: string) {
|
async function switchSession(sessionId: string) {
|
||||||
|
// Sync current messages back to the streaming session before switching
|
||||||
|
if (streamSessionId.value && sessionId !== streamSessionId.value) {
|
||||||
|
syncMessagesToSession()
|
||||||
|
}
|
||||||
activeSessionId.value = sessionId
|
activeSessionId.value = sessionId
|
||||||
activeSession.value = sessions.value.find(s => s.id === sessionId) || null
|
activeSession.value = sessions.value.find(s => s.id === sessionId) || null
|
||||||
|
|
||||||
@@ -263,6 +269,13 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncMessagesToSession() {
|
||||||
|
const targetSession = sessions.value.find(s => s.id === streamSessionId.value)
|
||||||
|
if (targetSession) {
|
||||||
|
targetSession.messages = [...messages.value]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addMessage(msg: Message) {
|
function addMessage(msg: Message) {
|
||||||
messages.value.push(msg)
|
messages.value.push(msg)
|
||||||
}
|
}
|
||||||
@@ -275,17 +288,19 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSessionTitle() {
|
function updateSessionTitle() {
|
||||||
if (!activeSession.value) return
|
const target = sessions.value.find(s => s.id === (streamSessionId.value || activeSessionId.value))
|
||||||
if (activeSession.value.title === 'New Chat') {
|
if (!target) return
|
||||||
const firstUser = messages.value.find(m => m.role === 'user')
|
const msgs = target.messages.length > 0 ? target.messages : messages.value
|
||||||
|
if (target.title === 'New Chat') {
|
||||||
|
const firstUser = msgs.find(m => m.role === 'user')
|
||||||
if (firstUser) {
|
if (firstUser) {
|
||||||
const title = firstUser.attachments?.length
|
const title = firstUser.attachments?.length
|
||||||
? firstUser.attachments.map(a => a.name).join(', ')
|
? firstUser.attachments.map(a => a.name).join(', ')
|
||||||
: firstUser.content
|
: firstUser.content
|
||||||
activeSession.value.title = title.slice(0, 40) + (title.length > 40 ? '...' : '')
|
target.title = title.slice(0, 40) + (title.length > 40 ? '...' : '')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activeSession.value.updatedAt = Date.now()
|
target.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage(content: string, attachments?: Attachment[]) {
|
async function sendMessage(content: string, attachments?: Attachment[]) {
|
||||||
@@ -306,7 +321,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
addMessage(userMsg)
|
addMessage(userMsg)
|
||||||
updateSessionTitle()
|
updateSessionTitle()
|
||||||
|
|
||||||
isStreaming.value = true
|
_isStreaming.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build conversation history from past messages
|
// Build conversation history from past messages
|
||||||
@@ -325,6 +340,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
// Use session-level model if set, otherwise fall back to global
|
// Use session-level model if set, otherwise fall back to global
|
||||||
const sessionModel = activeSession.value?.model || appStore.selectedModel
|
const sessionModel = activeSession.value?.model || appStore.selectedModel
|
||||||
|
streamSessionId.value = activeSessionId.value
|
||||||
const run = await startRun({
|
const run = await startRun({
|
||||||
input: inputText,
|
input: inputText,
|
||||||
conversation_history: history,
|
conversation_history: history,
|
||||||
@@ -340,7 +356,7 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
content: `Error: startRun returned no run ID. Response: ${JSON.stringify(run)}`,
|
content: `Error: startRun returned no run ID. Response: ${JSON.stringify(run)}`,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
isStreaming.value = false
|
_isStreaming.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,8 +418,10 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
if (lastMsg?.isStreaming) {
|
if (lastMsg?.isStreaming) {
|
||||||
updateMessage(lastMsg.id, { isStreaming: false })
|
updateMessage(lastMsg.id, { isStreaming: false })
|
||||||
}
|
}
|
||||||
isStreaming.value = false
|
_isStreaming.value = false
|
||||||
|
streamSessionId.value = null
|
||||||
abortController.value = null
|
abortController.value = null
|
||||||
|
syncMessagesToSession()
|
||||||
updateSessionTitle()
|
updateSessionTitle()
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -428,7 +446,8 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
messages.value[i] = { ...m, toolStatus: 'error' }
|
messages.value[i] = { ...m, toolStatus: 'error' }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
isStreaming.value = false
|
_isStreaming.value = false
|
||||||
|
streamSessionId.value = null
|
||||||
abortController.value = null
|
abortController.value = null
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -439,8 +458,10 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
if (last?.isStreaming) {
|
if (last?.isStreaming) {
|
||||||
updateMessage(last.id, { isStreaming: false })
|
updateMessage(last.id, { isStreaming: false })
|
||||||
}
|
}
|
||||||
isStreaming.value = false
|
_isStreaming.value = false
|
||||||
|
streamSessionId.value = null
|
||||||
abortController.value = null
|
abortController.value = null
|
||||||
|
syncMessagesToSession()
|
||||||
updateSessionTitle()
|
updateSessionTitle()
|
||||||
},
|
},
|
||||||
// onError
|
// onError
|
||||||
@@ -460,8 +481,10 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
isStreaming.value = false
|
_isStreaming.value = false
|
||||||
|
streamSessionId.value = null
|
||||||
abortController.value = null
|
abortController.value = null
|
||||||
|
syncMessagesToSession()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -471,19 +494,22 @@ export const useChatStore = defineStore('chat', () => {
|
|||||||
content: `Error: ${err.message}`,
|
content: `Error: ${err.message}`,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
})
|
})
|
||||||
isStreaming.value = false
|
_isStreaming.value = false
|
||||||
|
streamSessionId.value = null
|
||||||
abortController.value = null
|
abortController.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopStreaming() {
|
function stopStreaming() {
|
||||||
abortController.value?.abort()
|
abortController.value?.abort()
|
||||||
isStreaming.value = false
|
_isStreaming.value = false
|
||||||
|
streamSessionId.value = null
|
||||||
const lastMsg = messages.value[messages.value.length - 1]
|
const lastMsg = messages.value[messages.value.length - 1]
|
||||||
if (lastMsg?.isStreaming) {
|
if (lastMsg?.isStreaming) {
|
||||||
updateMessage(lastMsg.id, { isStreaming: false })
|
updateMessage(lastMsg.id, { isStreaming: false })
|
||||||
}
|
}
|
||||||
abortController.value = null
|
abortController.value = null
|
||||||
|
syncMessagesToSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load sessions on init
|
// Load sessions on init
|
||||||
|
|||||||
@@ -8,6 +8,17 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vh: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix mobile viewport height (address bar)
|
||||||
|
@supports (height: 100dvh) {
|
||||||
|
:root {
|
||||||
|
--vh: 1dvh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html, body, #app {
|
html, body, #app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ onMounted(() => {
|
|||||||
@use '@/styles/variables' as *;
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
.channels-view {
|
.channels-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.chat-view {
|
.chat-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ async function handleSave() {
|
|||||||
@use '@/styles/variables' as *;
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
.jobs-view {
|
.jobs-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ async function handleLogin() {
|
|||||||
@use "@/styles/variables" as *;
|
@use "@/styles/variables" as *;
|
||||||
|
|
||||||
.login-view {
|
.login-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ onMounted(async () => {
|
|||||||
@use '@/styles/variables' as *;
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
.logs-view {
|
.logs-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ const displayUser = computed(() => (data.value?.user || '').replace(/§/g, '\n\n
|
|||||||
@use '@/styles/variables' as *;
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
.memory-view {
|
.memory-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ async function handleSaved() {
|
|||||||
@use '@/styles/variables' as *;
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
.models-view {
|
.models-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ async function saveApiServer(values: Record<string, any>) {
|
|||||||
@use '@/styles/variables' as *;
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
.settings-view {
|
.settings-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ async function loadSkills() {
|
|||||||
function handleSelect(category: string, skill: string) {
|
function handleSelect(category: string, skill: string) {
|
||||||
selectedCategory.value = category
|
selectedCategory.value = category
|
||||||
selectedSkill.value = skill
|
selectedSkill.value = skill
|
||||||
|
if (window.innerWidth <= 768) {
|
||||||
|
showSidebar.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -61,13 +64,14 @@ function handleSelect(category: string, skill: string) {
|
|||||||
:placeholder="t('skills.searchPlaceholder')"
|
:placeholder="t('skills.searchPlaceholder')"
|
||||||
size="small"
|
size="small"
|
||||||
clearable
|
clearable
|
||||||
class="search-input"
|
style="width: 160px"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="skills-content">
|
<div class="skills-content">
|
||||||
<div v-if="loading && categories.length === 0" class="skills-loading">Loading...</div>
|
<div v-if="loading && categories.length === 0" class="skills-loading">Loading...</div>
|
||||||
<div v-else class="skills-layout">
|
<div v-else class="skills-layout">
|
||||||
|
<div class="mobile-backdrop" :class="{ active: showSidebar }" @click="showSidebar = false" />
|
||||||
<div v-if="showSidebar" class="skills-sidebar">
|
<div v-if="showSidebar" class="skills-sidebar">
|
||||||
<SkillList
|
<SkillList
|
||||||
:categories="categories"
|
:categories="categories"
|
||||||
@@ -100,13 +104,13 @@ function handleSelect(category: string, skill: string) {
|
|||||||
@use '@/styles/variables' as *;
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
.skills-view {
|
.skills-view {
|
||||||
height: 100vh;
|
height: calc(100 * var(--vh));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 220px;
|
width: 100px;
|
||||||
|
|
||||||
@media (max-width: $breakpoint-mobile) {
|
@media (max-width: $breakpoint-mobile) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -181,9 +185,22 @@ function handleSelect(category: string, skill: string) {
|
|||||||
.skills-layout {
|
.skills-layout {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
min-width: 0;
|
.mobile-backdrop {
|
||||||
min-height: 0;
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 9;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity $transition-fast;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-detail {
|
.empty-detail {
|
||||||
|
|||||||
Reference in New Issue
Block a user