Skip to content

Commit ac44d1f

Browse files
authored
feat: enhance chat interface and mobile responsiveness (#5635)
1 parent 66d0f0a commit ac44d1f

File tree

8 files changed

+124
-44
lines changed

8 files changed

+124
-44
lines changed

dashboard/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="UTF-8" />
55
<link rel="icon" href="/favicon.svg" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
77
<meta name="keywords" content="AstrBot Soulter" />
88
<meta name="description" content="AstrBot Dashboard" />
99
<meta name="robots" content="noindex, nofollow" />

dashboard/src/components/chat/Chat.vue

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,7 @@
3737

3838
<!-- 正常聊天界面 -->
3939
<template v-else>
40-
<div class="conversation-header fade-in" v-if="isMobile">
41-
<!-- 手机端菜单按钮 -->
42-
<v-btn icon class="mobile-menu-btn" @click="toggleMobileSidebar" variant="text">
43-
<v-icon>mdi-menu</v-icon>
44-
</v-btn>
45-
</div>
4640

47-
<!-- 面包屑导航 -->
4841
<div v-if="currentSessionProject && messages && messages.length > 0" class="breadcrumb-container">
4942
<div class="breadcrumb-content">
5043
<span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span>
@@ -241,6 +234,7 @@ const route = useRoute();
241234
const { t } = useI18n();
242235
const { tm } = useModuleI18n('features/chat');
243236
const theme = useTheme();
237+
const customizer = useCustomizerStore();
244238
245239
// UI 状态
246240
const isMobile = ref(false);
@@ -342,19 +336,28 @@ function checkMobile() {
342336
isMobile.value = window.innerWidth <= 768;
343337
if (!isMobile.value) {
344338
mobileMenuOpen.value = false;
339+
customizer.SET_CHAT_SIDEBAR(false);
345340
}
346341
}
347342
348343
function toggleMobileSidebar() {
349344
mobileMenuOpen.value = !mobileMenuOpen.value;
345+
customizer.SET_CHAT_SIDEBAR(mobileMenuOpen.value);
350346
}
351347
352348
function closeMobileSidebar() {
353349
mobileMenuOpen.value = false;
350+
customizer.SET_CHAT_SIDEBAR(false);
354351
}
355352
353+
// 同步 nav header 中的 sidebar toggle
354+
watch(() => customizer.chatSidebarOpen, (val) => {
355+
if (isMobile.value) {
356+
mobileMenuOpen.value = val;
357+
}
358+
});
359+
356360
function toggleTheme() {
357-
const customizer = useCustomizerStore();
358361
const newTheme = customizer.uiTheme === 'PurpleTheme' ? 'PurpleThemeDark' : 'PurpleTheme';
359362
customizer.SET_UI_THEME(newTheme);
360363
theme.global.name.value = newTheme;
@@ -722,6 +725,7 @@ onBeforeUnmount(() => {
722725
height: 100%;
723726
max-height: 100%;
724727
overflow: hidden;
728+
overscroll-behavior: none;
725729
}
726730
727731
.chat-page-container {

dashboard/src/components/chat/ChatInput.vue

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@
3232
</div>
3333
</transition>
3434
<textarea ref="inputField" v-model="localPrompt" @keydown="handleKeyDown" :disabled="disabled"
35-
placeholder="Ask AstrBot..."
36-
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 12px 16px; min-height: 40px; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
35+
placeholder="Ask AstrBot..." class="chat-textarea"
36+
autocomplete="off" autocorrect="off" autocapitalize="sentences" spellcheck="false"
37+
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 16px 20px; min-height: 40px; max-height: 200px; overflow-y: auto; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
3738
<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 14px;">
3839
<div
39-
style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px;">
40+
style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px; min-width: 0; flex: 1; overflow: hidden;">
4041
<!-- Settings Menu -->
4142
<StyledMenu offset="8" location="top start" :close-on-content-click="false">
4243
<template v-slot:activator="{ props: activatorProps }">
@@ -72,9 +73,9 @@
7273
<!-- Provider/Model Selector Menu -->
7374
<ProviderModelMenu v-if="showProviderSelector" ref="providerModelMenuRef" />
7475
</div>
75-
<div style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center;">
76+
<div style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center; flex-shrink: 0;">
7677
<input type="file" ref="imageInputRef" @change="handleFileSelect" style="display: none" multiple />
77-
<v-progress-circular v-if="disabled" indeterminate size="16" class="mr-1" width="1.5" />
78+
<v-progress-circular v-if="disabled && !mobile" indeterminate size="16" class="mr-1" width="1.5" />
7879
<!-- <v-btn @click="$emit('openLiveMode')"
7980
icon
8081
variant="text"
@@ -87,36 +88,21 @@
8788
</v-tooltip>
8889
</v-btn> -->
8990
<v-btn @click="handleRecordClick" icon variant="text" :color="isRecording ? 'error' : 'deep-purple'"
90-
class="record-btn" size="small">
91+
class="record-btn">
9192
<v-icon :icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'" variant="text"
9293
plain></v-icon>
9394
<v-tooltip activator="parent" location="top">
9495
{{ isRecording ? tm('voice.speaking') : tm('voice.startRecording') }}
9596
</v-tooltip>
9697
</v-btn>
97-
<v-btn
98-
icon
99-
v-if="isRunning"
100-
@click="$emit('stop')"
101-
variant="text"
102-
class="send-btn"
103-
size="small"
104-
>
98+
<v-btn icon v-if="isRunning" @click="$emit('stop')" variant="tonal" color="deep-purple" class="send-btn">
10599
<v-icon icon="mdi-stop" variant="text" plain></v-icon>
106100
<v-tooltip activator="parent" location="top">
107101
{{ tm('input.stopGenerating') }}
108102
</v-tooltip>
109103
</v-btn>
110-
<v-btn
111-
v-else
112-
@click="$emit('send')"
113-
icon="mdi-send"
114-
variant="text"
115-
color="deep-purple"
116-
:disabled="!canSend"
117-
class="send-btn"
118-
size="small"
119-
/>
104+
<v-btn v-else @click="$emit('send')" icon="mdi-send" variant="tonal" color="deep-purple"
105+
:disabled="!canSend" class="send-btn" />
120106
</div>
121107
</div>
122108
</div>
@@ -152,7 +138,8 @@
152138
</template>
153139

154140
<script setup lang="ts">
155-
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
141+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue';
142+
import { useDisplay } from 'vuetify';
156143
import { useModuleI18n } from '@/i18n/composables';
157144
import { useCustomizerStore } from '@/stores/customizer';
158145
import ConfigSelector from './ConfigSelector.vue';
@@ -251,21 +238,34 @@ function handleReplyAfterLeave() {
251238
isReplyClosing.value = false;
252239
}
253240
241+
const { mobile } = useDisplay();
242+
243+
// Auto-resize textarea
244+
function autoResize() {
245+
const el = inputField.value;
246+
if (!el) return;
247+
el.style.height = 'auto';
248+
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
249+
}
250+
251+
watch(localPrompt, () => {
252+
nextTick(autoResize);
253+
});
254+
254255
function handleKeyDown(e: KeyboardEvent) {
255-
// Enter 发送消息或触发命令
256-
if (e.keyCode === 13 && !e.shiftKey) {
256+
// Enter 插入换行(桌面和手机端均如此,发送通过右下角发送按鈕)
257+
// Shift+Enter 发送(Ctrl+Enter / Cmd+Enter 也保留)
258+
if (e.keyCode === 13 && (e.shiftKey || e.ctrlKey || e.metaKey)) {
257259
e.preventDefault();
258-
259-
// 检查是否是 /astr_live_dev 命令
260260
if (localPrompt.value.trim() === '/astr_live_dev') {
261261
emit('openLiveMode');
262262
localPrompt.value = '';
263263
return;
264264
}
265-
266265
if (canSend.value) {
267266
emit('send');
268267
}
268+
return;
269269
}
270270
271271
// Ctrl+B 录音
@@ -588,11 +588,20 @@ defineExpose({
588588
@media (max-width: 768px) {
589589
.input-area {
590590
padding: 0 !important;
591+
padding-bottom: 10px !important;
591592
}
592593
593594
.input-container {
594595
width: 100% !important;
595596
max-width: 100% !important;
596597
}
598+
599+
.input-area textarea,
600+
.chat-textarea {
601+
min-height: 32px !important;
602+
max-height: 160px !important;
603+
font-size: 16px !important;
604+
padding: 16px 16px 12px 16px !important;
605+
}
597606
}
598607
</style>

dashboard/src/components/chat/ConversationSidebar.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
@deleteProject="$emit('deleteProject', $event)"
3838
/>
3939

40-
<div style="overflow-y: auto; flex-grow: 1;"
40+
<div style="overflow-y: auto; flex-grow: 1; overscroll-behavior-y: contain;"
4141
v-if="!sidebarCollapsed || isMobile">
4242
<v-card v-if="sessions.length > 0" flat style="background-color: transparent;">
4343
<v-list density="compact" nav class="conversation-list"
@@ -326,6 +326,13 @@ function handleTransportModeChange(mode: string | null) {
326326
transition: all 0.2s ease;
327327
}
328328
329+
@media (max-width: 768px) {
330+
.conversation-actions {
331+
opacity: 1 !important;
332+
visibility: visible !important;
333+
}
334+
}
335+
329336
.edit-title-btn,
330337
.delete-conversation-btn {
331338
opacity: 0.7;

dashboard/src/components/chat/MessageList.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,7 @@ export default {
965965
height: 100%;
966966
max-height: 100%;
967967
overflow-y: auto;
968+
overscroll-behavior-y: contain;
968969
padding: 16px;
969970
display: flex;
970971
flex-direction: column;

dashboard/src/layouts/full/vertical-header/VerticalHeader.vue

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,12 @@ onMounted(async () => {
468468
<v-icon>mdi-menu</v-icon>
469469
</v-btn>
470470

471+
<!-- 移动端 chat sidebar 展开按钮 - 仅在 chat 模式下的小屏幕显示 -->
472+
<v-btn v-if="customizer.viewMode === 'chat'" class="hidden-lg-and-up ms-1" icon rounded="sm" variant="flat"
473+
@click.stop="customizer.TOGGLE_CHAT_SIDEBAR()">
474+
<v-icon>mdi-menu</v-icon>
475+
</v-btn>
476+
471477
<div class="logo-container" :class="{ 'mobile-logo': $vuetify.display.xs, 'chat-mode-logo': customizer.viewMode === 'chat' }" @click="handleLogoClick">
472478
<span class="logo-text Outfit">Astr<span class="logo-text bot-text-wrapper">Bot
473479
<img v-if="isChristmas" src="@/assets/images/xmas-hat.png" alt="Christmas hat" class="xmas-hat" />
@@ -488,13 +494,13 @@ onMounted(async () => {
488494
</small>
489495
</div>
490496

491-
<!-- Bot/Chat 模式切换按钮 -->
497+
<!-- Bot/Chat 模式切换按钮 - 手机端隐藏,移入 ... 菜单 -->
492498
<v-btn-toggle
493499
v-model="viewMode"
494500
mandatory
495501
variant="outlined"
496502
density="compact"
497-
class="mr-4"
503+
class="mr-4 hidden-xs"
498504
color="primary"
499505
>
500506
<v-btn value="bot" size="small">
@@ -524,6 +530,30 @@ onMounted(async () => {
524530
</v-btn>
525531
</template>
526532

533+
<!-- Bot/Chat 模式切换 - 仅在手机端显示 -->
534+
<template v-if="$vuetify.display.xs">
535+
<div class="mobile-mode-toggle-wrapper">
536+
<v-btn-toggle
537+
v-model="viewMode"
538+
mandatory
539+
variant="outlined"
540+
density="compact"
541+
color="primary"
542+
class="mobile-mode-toggle"
543+
>
544+
<v-btn value="bot" size="small">
545+
<v-icon start>mdi-robot</v-icon>
546+
Bot
547+
</v-btn>
548+
<v-btn value="chat" size="small">
549+
<v-icon start>mdi-chat</v-icon>
550+
Chat
551+
</v-btn>
552+
</v-btn-toggle>
553+
</div>
554+
<v-divider class="my-1" />
555+
</template>
556+
527557
<!-- 语言切换 -->
528558
<v-list-item
529559
v-for="lang in languages"
@@ -888,6 +918,10 @@ onMounted(async () => {
888918
margin-left: 22px;
889919
}
890920
921+
.mobile-logo.chat-mode-logo {
922+
margin-left: 4px;
923+
}
924+
891925
.logo-text {
892926
font-size: 24px;
893927
font-weight: 1000;
@@ -926,6 +960,20 @@ onMounted(async () => {
926960
margin-right: 8px;
927961
}
928962
963+
.mobile-mode-toggle-wrapper {
964+
display: flex;
965+
justify-content: center;
966+
padding: 8px 12px 4px;
967+
}
968+
969+
.mobile-mode-toggle {
970+
width: 100%;
971+
}
972+
973+
.mobile-mode-toggle .v-btn {
974+
flex: 1;
975+
}
976+
929977
/* 移动端对话框标题样式 */
930978
.mobile-card-title {
931979
display: flex;

dashboard/src/scss/style.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@
1515
@import './components/VScrollbar';
1616

1717
@import './pages/dashboards';
18+
19+
html, body {
20+
overscroll-behavior-y: none;
21+
}

dashboard/src/stores/customizer.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export const useCustomizerStore = defineStore({
1010
fontTheme: "Poppins",
1111
uiTheme: config.uiTheme,
1212
inputBg: config.inputBg,
13-
viewMode: (localStorage.getItem('viewMode') as 'bot' | 'chat') || 'bot' // 'bot' 或 'chat'
13+
viewMode: (localStorage.getItem('viewMode') as 'bot' | 'chat') || 'bot', // 'bot' 或 'chat'
14+
chatSidebarOpen: false // chat mode mobile sidebar state
1415
}),
1516

1617
getters: {},
@@ -30,7 +31,13 @@ export const useCustomizerStore = defineStore({
3031
},
3132
SET_VIEW_MODE(payload: 'bot' | 'chat') {
3233
this.viewMode = payload;
33-
localStorage.setItem("viewMode", payload);
34+
localStorage.setItem('viewMode', payload);
35+
},
36+
TOGGLE_CHAT_SIDEBAR() {
37+
this.chatSidebarOpen = !this.chatSidebarOpen;
38+
},
39+
SET_CHAT_SIDEBAR(payload: boolean) {
40+
this.chatSidebarOpen = payload;
3441
},
3542
}
3643
});

0 commit comments

Comments
 (0)