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