Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2778edbf4b |
@@ -11,6 +11,7 @@
|
||||
:currSessionId="currSessionId"
|
||||
:selectedProjectId="selectedProjectId"
|
||||
:transportMode="transportMode"
|
||||
:sendShortcut="sendShortcut"
|
||||
:isDark="isDark"
|
||||
:chatboxMode="chatboxMode"
|
||||
:isMobile="isMobile"
|
||||
@@ -29,6 +30,7 @@
|
||||
@editProject="showEditProjectDialog"
|
||||
@deleteProject="handleDeleteProject"
|
||||
@updateTransportMode="setTransportMode"
|
||||
@updateSendShortcut="setSendShortcut"
|
||||
/>
|
||||
|
||||
<!-- 右侧聊天内容区域 -->
|
||||
@@ -79,6 +81,7 @@
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@@ -110,6 +113,7 @@
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@@ -140,6 +144,7 @@
|
||||
:session-id="currSessionId || null"
|
||||
:current-session="getCurrentSession"
|
||||
:replyTo="replyTo"
|
||||
:send-shortcut="sendShortcut"
|
||||
@send="handleSendMessage"
|
||||
@stop="handleStopMessage"
|
||||
@toggleStreaming="toggleStreaming"
|
||||
@@ -226,6 +231,8 @@ import { useToast } from '@/utils/toast';
|
||||
interface Props {
|
||||
chatboxMode?: boolean;
|
||||
}
|
||||
type SendShortcut = 'enter' | 'shift_enter';
|
||||
const SEND_SHORTCUT_STORAGE_KEY = 'chat_send_shortcut';
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
chatboxMode: false
|
||||
@@ -334,6 +341,12 @@ interface ReplyInfo {
|
||||
const replyTo = ref<ReplyInfo | null>(null);
|
||||
|
||||
const isDark = computed(() => useCustomizerStore().uiTheme === 'PurpleThemeDark');
|
||||
const sendShortcut = ref<SendShortcut>('shift_enter');
|
||||
|
||||
function setSendShortcut(mode: SendShortcut) {
|
||||
sendShortcut.value = mode;
|
||||
localStorage.setItem(SEND_SHORTCUT_STORAGE_KEY, mode);
|
||||
}
|
||||
|
||||
// 检测是否为手机端
|
||||
function checkMobile() {
|
||||
@@ -725,6 +738,10 @@ watch(sessions, (newSessions) => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const storedShortcut = localStorage.getItem(SEND_SHORTCUT_STORAGE_KEY);
|
||||
if (storedShortcut === 'enter' || storedShortcut === 'shift_enter') {
|
||||
sendShortcut.value = storedShortcut;
|
||||
}
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
getSessions();
|
||||
|
||||
@@ -173,6 +173,7 @@ interface Props {
|
||||
currentSession?: Session | null;
|
||||
configId?: string | null;
|
||||
replyTo?: ReplyInfo | null;
|
||||
sendShortcut?: 'enter' | 'shift_enter';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -180,7 +181,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
currentSession: null,
|
||||
configId: null,
|
||||
stagedFiles: () => [],
|
||||
replyTo: null
|
||||
replyTo: null,
|
||||
sendShortcut: 'shift_enter'
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -253,21 +255,8 @@ watch(localPrompt, () => {
|
||||
});
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Enter 插入换行(桌面和手机端均如此,发送通过右下角发送按鈕)
|
||||
// Shift+Enter 发送(Ctrl+Enter / Cmd+Enter 也保留)
|
||||
if (e.keyCode === 13 && (e.shiftKey || e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
if (localPrompt.value.trim() === '/astr_live_dev') {
|
||||
emit('openLiveMode');
|
||||
localPrompt.value = '';
|
||||
return;
|
||||
}
|
||||
if (canSend.value) {
|
||||
emit('send');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isEnter = e.key === 'Enter';
|
||||
if (!isEnter) {
|
||||
// Ctrl+B 录音
|
||||
if (e.ctrlKey && e.keyCode === 66) {
|
||||
e.preventDefault();
|
||||
@@ -280,6 +269,26 @@ function handleKeyDown(e: KeyboardEvent) {
|
||||
}
|
||||
}, ctrlKeyLongPressThreshold);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isSendHotkey =
|
||||
e.ctrlKey ||
|
||||
e.metaKey ||
|
||||
(props.sendShortcut === 'enter' ? !e.shiftKey : e.shiftKey);
|
||||
|
||||
if (isSendHotkey) {
|
||||
e.preventDefault();
|
||||
if (localPrompt.value.trim() === '/astr_live_dev') {
|
||||
emit('openLiveMode');
|
||||
localPrompt.value = '';
|
||||
return;
|
||||
}
|
||||
if (canSend.value) {
|
||||
emit('send');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
|
||||
@@ -231,6 +231,50 @@
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- 发送快捷键(分组) -->
|
||||
<v-menu
|
||||
:open-on-hover="!isMobile"
|
||||
:open-on-click="isMobile"
|
||||
:open-delay="!isMobile ? 60 : 0"
|
||||
:close-delay="!isMobile ? 120 : 0"
|
||||
:location="isMobile ? 'bottom' : 'end center'"
|
||||
offset="8"
|
||||
close-on-content-click
|
||||
>
|
||||
<template v-slot:activator="{ props: sendShortcutMenuProps }">
|
||||
<v-list-item
|
||||
v-bind="sendShortcutMenuProps"
|
||||
class="styled-menu-item chat-settings-group-trigger"
|
||||
rounded="md"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-keyboard-outline</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ tm('shortcuts.sendKey.title') }}</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<span class="chat-settings-group-current chat-settings-transport-current">{{ currentSendShortcutLabel }}</span>
|
||||
<v-icon size="18" class="chat-settings-group-arrow">mdi-chevron-right</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-card class="styled-menu-card" style="min-width: 220px;" elevation="8" rounded="lg">
|
||||
<v-list density="compact" class="styled-menu-list pa-1">
|
||||
<v-list-item
|
||||
v-for="opt in sendShortcutOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
@click="handleSendShortcutChange(opt.value)"
|
||||
:class="{ 'styled-menu-item-active': props.sendShortcut === opt.value }"
|
||||
class="styled-menu-item"
|
||||
rounded="md"
|
||||
>
|
||||
<v-list-item-title>{{ opt.label }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
<!-- 全屏/退出全屏 -->
|
||||
<v-list-item class="styled-menu-item" @click="$emit('toggleFullscreen')">
|
||||
<template v-slot:prepend>
|
||||
@@ -277,6 +321,7 @@ interface Props {
|
||||
isMobile: boolean;
|
||||
mobileMenuOpen: boolean;
|
||||
projects?: Project[];
|
||||
sendShortcut: 'enter' | 'shift_enter';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -297,6 +342,7 @@ const emit = defineEmits<{
|
||||
editProject: [project: Project];
|
||||
deleteProject: [projectId: string];
|
||||
updateTransportMode: [mode: 'sse' | 'websocket'];
|
||||
updateSendShortcut: [mode: 'enter' | 'shift_enter'];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -357,6 +403,10 @@ const transportOptions = [
|
||||
{ label: tm('transport.sse'), value: 'sse' as const },
|
||||
{ label: tm('transport.websocket'), value: 'websocket' as const }
|
||||
];
|
||||
const sendShortcutOptions = [
|
||||
{ label: tm('shortcuts.sendKey.enterToSend'), value: 'enter' as const },
|
||||
{ label: tm('shortcuts.sendKey.shiftEnterToSend'), value: 'shift_enter' as const }
|
||||
];
|
||||
|
||||
// Language switcher
|
||||
const { languageOptions, currentLanguage, switchLanguage, locale } = useLanguageSwitcher();
|
||||
@@ -376,6 +426,10 @@ const currentTransportLabel = computed(() => {
|
||||
const found = transportOptions.find(opt => opt.value === props.transportMode);
|
||||
return found?.label ?? '';
|
||||
});
|
||||
const currentSendShortcutLabel = computed(() => {
|
||||
const found = sendShortcutOptions.find(opt => opt.value === props.sendShortcut);
|
||||
return found?.label ?? '';
|
||||
});
|
||||
|
||||
// 从 localStorage 读取侧边栏折叠状态
|
||||
const savedCollapsedState = localStorage.getItem('sidebarCollapsed');
|
||||
@@ -403,6 +457,12 @@ function handleTransportModeChange(mode: string | null) {
|
||||
emit('updateTransportMode', mode);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSendShortcutChange(mode: string | null) {
|
||||
if (mode === 'enter' || mode === 'shift_enter') {
|
||||
emit('updateSendShortcut', mode);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -71,10 +71,16 @@
|
||||
"modes": {
|
||||
"darkMode": "Switch to Dark Mode",
|
||||
"lightMode": "Switch to Light Mode"
|
||||
}, "shortcuts": {
|
||||
},
|
||||
"shortcuts": {
|
||||
"help": "Get Help",
|
||||
"voiceRecord": "Record Voice",
|
||||
"pasteImage": "Paste Image"
|
||||
"pasteImage": "Paste Image",
|
||||
"sendKey": {
|
||||
"title": "Send Shortcut",
|
||||
"enterToSend": "Enter to send",
|
||||
"shiftEnterToSend": "Shift+Enter to send"
|
||||
}
|
||||
},
|
||||
"streaming": {
|
||||
"enabled": "Streaming enabled",
|
||||
|
||||
@@ -75,7 +75,12 @@
|
||||
"shortcuts": {
|
||||
"help": "Справка",
|
||||
"voiceRecord": "Запись голоса",
|
||||
"pasteImage": "Вставить изображение"
|
||||
"pasteImage": "Вставить изображение",
|
||||
"sendKey": {
|
||||
"title": "Клавиша отправки",
|
||||
"enterToSend": "Enter для отправки",
|
||||
"shiftEnterToSend": "Shift+Enter для отправки"
|
||||
}
|
||||
},
|
||||
"streaming": {
|
||||
"enabled": "Потоковый ответ включен",
|
||||
|
||||
@@ -71,10 +71,16 @@
|
||||
"modes": {
|
||||
"darkMode": "切换到夜间模式",
|
||||
"lightMode": "切换到日间模式"
|
||||
}, "shortcuts": {
|
||||
},
|
||||
"shortcuts": {
|
||||
"help": "获取帮助",
|
||||
"voiceRecord": "录制语音",
|
||||
"pasteImage": "粘贴图片"
|
||||
"pasteImage": "粘贴图片",
|
||||
"sendKey": {
|
||||
"title": "发送快捷键",
|
||||
"enterToSend": "Enter 发送",
|
||||
"shiftEnterToSend": "Shift+Enter 发送"
|
||||
}
|
||||
},
|
||||
"streaming": {
|
||||
"enabled": "流式响应已开启",
|
||||
|
||||
Reference in New Issue
Block a user