Files
gochat/frontend/app/javascript/dashboard/components/widgets/WootWriter/ReplyTopPanel.vue
T
Rogeeandrogee e41d40feb8 HH-546: flatten conversation AI actions (#128)
* fix(HH-546): flatten conversation AI actions

* fix(HH-546): wrap AI actions on narrow screens

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-23 21:16:19 +08:00

213 lines
5.6 KiB
Vue

<script>
import { computed } from 'vue';
import { useStore } from 'vuex';
import { useI18n } from 'vue-i18n';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useCaptain } from 'dashboard/composables/useCaptain';
import { useAlert } from 'dashboard/composables';
import { useMapGetter } from 'dashboard/composables/store';
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
import NextButton from 'dashboard/components-next/button/Button.vue';
import EditorModeToggle from './EditorModeToggle.vue';
import CopilotMenuBar from './CopilotMenuBar.vue';
export default {
name: 'ReplyTopPanel',
components: {
NextButton,
EditorModeToggle,
CopilotMenuBar,
},
props: {
mode: {
type: String,
default: REPLY_EDITOR_MODES.REPLY,
},
isReplyRestricted: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
isEditorDisabled: {
type: Boolean,
default: false,
},
conversationId: {
type: Number,
default: null,
},
isMessageLengthReachingThreshold: {
type: Boolean,
default: () => false,
},
charactersRemaining: {
type: Number,
default: () => 0,
},
editorContent: {
type: String,
default: undefined,
},
hasContent: {
type: Boolean,
default: false,
},
},
emits: ['setReplyMode', 'toggleEditorSize', 'executeCopilotAction'],
setup(props, { emit }) {
const store = useStore();
const { t } = useI18n();
const currentChat = useMapGetter('getSelectedChat');
const setReplyMode = mode => {
emit('setReplyMode', mode);
};
const handleReplyClick = () => {
if (props.isReplyRestricted) return;
setReplyMode(REPLY_EDITOR_MODES.REPLY);
};
const handleNoteClick = () => {
setReplyMode(REPLY_EDITOR_MODES.NOTE);
};
const handleModeToggle = () => {
const newMode =
props.mode === REPLY_EDITOR_MODES.REPLY
? REPLY_EDITOR_MODES.NOTE
: REPLY_EDITOR_MODES.REPLY;
setReplyMode(newMode);
};
const { captainTasksEnabled } = useCaptain();
const handleCopilotAction = (actionKey, data) => {
emit('executeCopilotAction', actionKey, data || props.editorContent);
};
const aiTakeoverActive = computed(
() => currentChat.value?.ai_takeover_active === true
);
const toggleAITakeover = async () => {
try {
const wasActive = aiTakeoverActive.value;
await store.dispatch(
wasActive ? 'exitAITakeover' : 'startAITakeover',
currentChat.value?.id
);
useAlert(
wasActive
? t('CONVERSATION.AI_TAKEOVER.EXIT_SUCCESS')
: t('CONVERSATION.AI_TAKEOVER.START_SUCCESS')
);
} catch (error) {
useAlert(
error?.response?.data?.error || t('CONVERSATION.AI_TAKEOVER.ERROR')
);
}
};
const keyboardEvents = {
'Alt+KeyP': {
action: () => handleNoteClick(),
allowOnFocusedInput: true,
},
'Alt+KeyL': {
action: () => handleReplyClick(),
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents);
return {
handleModeToggle,
handleReplyClick,
handleNoteClick,
REPLY_EDITOR_MODES,
captainTasksEnabled,
handleCopilotAction,
aiTakeoverActive,
toggleAITakeover,
};
},
computed: {
replyButtonClass() {
return {
'is-active': this.mode === REPLY_EDITOR_MODES.REPLY,
};
},
noteButtonClass() {
return {
'is-active': this.mode === REPLY_EDITOR_MODES.NOTE,
};
},
charLengthClass() {
return this.charactersRemaining < 0 ? 'text-n-ruby-9' : 'text-n-slate-11';
},
characterLengthWarning() {
return this.charactersRemaining < 0
? `${-this.charactersRemaining} ${CHAR_LENGTH_WARNING.NEGATIVE}`
: `${this.charactersRemaining} ${CHAR_LENGTH_WARNING.UNDER_50}`;
},
},
};
</script>
<template>
<div
class="flex min-h-[3.25rem] flex-wrap items-center justify-between gap-2 py-2 ltr:pl-3 ltr:pr-2 rtl:pr-3 rtl:pl-2 sm:h-[3.25rem] sm:flex-nowrap sm:py-0"
>
<EditorModeToggle
:mode="mode"
:disabled="disabled"
:is-reply-restricted="isReplyRestricted"
@toggle-mode="handleModeToggle"
/>
<div class="flex min-w-0 flex-1 items-center">
<div v-if="isMessageLengthReachingThreshold" class="text-xs">
<span :class="charLengthClass">
{{ characterLengthWarning }}
</span>
</div>
</div>
<div
data-testid="reply-top-panel-actions"
class="flex w-full flex-shrink-0 flex-wrap items-center justify-end gap-2 sm:w-auto sm:flex-nowrap"
>
<NextButton
type="button"
:label="
aiTakeoverActive
? $t('CONVERSATION.AI_TAKEOVER.EXIT')
: $t('CONVERSATION.AI_TAKEOVER.START')
"
slate
outline
sm
class="flex-shrink-0"
@click="toggleAITakeover"
/>
<CopilotMenuBar
v-if="captainTasksEnabled"
inline
:disabled="disabled || isEditorDisabled"
:has-selection="false"
:has-content="hasContent"
:conversation-id="conversationId"
@execute-copilot-action="handleCopilotAction"
/>
<NextButton
v-if="captainTasksEnabled"
type="button"
ghost
class="flex-shrink-0 text-n-slate-11"
sm
icon="i-lucide-maximize-2"
@click="$emit('toggleEditorSize')"
/>
</div>
</div>
</template>