fix(ai): reset action when reselecting active mode
This commit is contained in:
1 parent
f9c2fb27d3
commit
122853d089
10 files changed
+245
-114
No files matched your search
@@ -40,7 +40,6 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import LightDropdown from "@/components/ui/LightDropdown.vue";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useSettingsStore, AI_PROVIDER_PRESETS, normalizeAiConfig } from "@/stores/settingsStore";
|
||||
@@ -147,12 +146,13 @@ const messages = ref<ChatMessage[]>([]);
|
||||
const isGenerating = ref(false);
|
||||
const scrollRef = ref<InstanceType<typeof ScrollArea> | null>(null);
|
||||
const activeAction = ref<AiAction>("general");
|
||||
const assistantMode = ref<"ask" | "agent">("ask");
|
||||
const assistantMode = ref<AiAssistantMode>("ask");
|
||||
const currentSessionId = ref("");
|
||||
const conversationId = ref("");
|
||||
const conversations = ref<AiConversation[]>([]);
|
||||
const showConversationList = ref(false);
|
||||
const showTemplateSelector = ref(false);
|
||||
const modeActionOpen = ref(false);
|
||||
|
||||
// Prompt template selection (panel-session scope)
|
||||
const activeTemplateIds = ref<string[]>([]);
|
||||
@@ -213,6 +213,12 @@ const templateSelectorLabel = computed(() => {
|
||||
if (count === 1) return name;
|
||||
return `${name} +${count - 1}`;
|
||||
});
|
||||
const templateSelectorTriggerLabel = computed(() => {
|
||||
if (activeTemplates.value.length === 0) {
|
||||
return t("ai.templateSelectorLabel", { label: templateSelectorLabel.value });
|
||||
}
|
||||
return templateSelectorLabel.value;
|
||||
});
|
||||
const promptTextareaRef = ref<HTMLTextAreaElement | null>(null);
|
||||
const shouldAutoScroll = ref(true);
|
||||
const userPausedAutoScroll = ref(false);
|
||||
@@ -588,28 +594,6 @@ function sendProposalReply(positive: boolean) {
|
||||
}
|
||||
|
||||
const activePlaceholder = computed(() => `${t(`ai.placeholders.${activeAction.value}`)} ${t("ai.tableMentionPlaceholderHint")}`);
|
||||
const activeModeHint = computed(() => t(`ai.modeHints.${assistantMode.value}`));
|
||||
const assistantModeItems = computed(() => [
|
||||
{
|
||||
value: "ask",
|
||||
label: t("ai.modes.ask"),
|
||||
title: t("ai.modeHints.ask"),
|
||||
icon: MessageSquarePlus,
|
||||
},
|
||||
{
|
||||
value: "agent",
|
||||
label: t("ai.modes.agent"),
|
||||
title: t("ai.modeHints.agent"),
|
||||
icon: Bot,
|
||||
},
|
||||
]);
|
||||
const actionMenuItems = computed(() =>
|
||||
actionButtons.value.map((button) => ({
|
||||
value: button.action,
|
||||
label: t(button.key),
|
||||
icon: button.icon,
|
||||
})),
|
||||
);
|
||||
const aiCodeAppearance = computed(() => (isDark.value ? "dark" : "light"));
|
||||
|
||||
const showActionButtons = computed(() => {
|
||||
@@ -617,6 +601,30 @@ const showActionButtons = computed(() => {
|
||||
return !isVectorDbType(props.connection.db_type);
|
||||
});
|
||||
|
||||
const modeIcon = computed<Component>(() => (assistantMode.value === "agent" ? Bot : MessageSquarePlus));
|
||||
const modeLabel = computed(() => t(`ai.modes.${assistantMode.value}`));
|
||||
const selectedActionButton = computed<AiActionButton | undefined>(() => actionButtons.value.find((b) => b.action === activeAction.value));
|
||||
const modeActionTriggerLabel = computed(() => {
|
||||
const modePart = `${modeLabel.value}`;
|
||||
if (!showActionButtons.value || !selectedActionButton.value) return modePart;
|
||||
return `${modePart} · ${t(selectedActionButton.value.key)}`;
|
||||
});
|
||||
|
||||
function switchModeActionTab(mode: "ask" | "agent") {
|
||||
activeAction.value = resolveDefaultAction(mode);
|
||||
if (assistantMode.value !== mode) {
|
||||
// Set the mode after the action so the tab label and picker stay aligned.
|
||||
assistantMode.value = mode;
|
||||
}
|
||||
}
|
||||
|
||||
function selectModeActionItem(action: AiAction) {
|
||||
// Vector databases only support generation; keep this constraint at the selection boundary.
|
||||
if (!showActionButtons.value) return;
|
||||
selectAction(action);
|
||||
modeActionOpen.value = false;
|
||||
}
|
||||
|
||||
const { databaseOptions: allDbOptions, loadDatabaseOptions } = useDatabaseOptions();
|
||||
|
||||
const dbOptions = computed(() => {
|
||||
@@ -1713,6 +1721,9 @@ async function deleteConversation(id: string) {
|
||||
function startNewChat() {
|
||||
clearMessages();
|
||||
showConversationList.value = false;
|
||||
// A fresh conversation always starts from the safe, non-executing default.
|
||||
assistantMode.value = "ask";
|
||||
activeAction.value = resolveDefaultAction("ask");
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -2088,54 +2099,95 @@ async function openExternalUrl(url: string) {
|
||||
<div ref="promptPanelRef" class="relative rounded-[6px] border bg-background">
|
||||
<div class="resize-handle" @mousedown="startResize"></div>
|
||||
<div class="px-2 pb-2 pt-1">
|
||||
<div v-if="connectionStore.connections.length" class="flex items-center gap-1 mb-1 text-xs text-foreground/80">
|
||||
<DatabaseIcon v-if="connection" :db-type="connectionIconType(connection)" class="h-3 w-3 shrink-0" />
|
||||
<Server v-else class="h-3 w-3 shrink-0" />
|
||||
<Select
|
||||
:model-value="connection?.id || ''"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') changeConnection(v);
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="h-5 w-auto border-0 rounded-md bg-transparent dark:bg-transparent p-0 px-1 text-xs text-foreground/80 shadow-none focus:ring-0 focus-visible:ring-0 [&_svg]:size-3">
|
||||
<SelectValue :placeholder="t('editor.selectConnection')">{{ connection?.name || t("editor.selectConnection") }}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent class="min-w-48">
|
||||
<SelectItem v-for="conn in connectionStore.connections" :key="conn.id" :value="conn.id">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<DatabaseIcon :db-type="connectionIconType(conn)" class="h-3.5 w-3.5 shrink-0" />
|
||||
<ConnectionGroupBadge :connection-id="conn.id" />
|
||||
<span class="truncate">{{ conn.name }}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<template v-if="connection">
|
||||
<Database class="h-3 w-3 shrink-0 text-foreground/40" />
|
||||
<div class="flex items-center gap-1 mb-1 text-xs text-foreground/80">
|
||||
<template v-if="connectionStore.connections.length">
|
||||
<DatabaseIcon v-if="connection" :db-type="connectionIconType(connection)" class="h-3 w-3 shrink-0" />
|
||||
<Server v-else class="h-3 w-3 shrink-0" />
|
||||
<Select
|
||||
:model-value="selectedDatabaseSelectValue"
|
||||
:model-value="connection?.id || ''"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') changeDatabase(v);
|
||||
}
|
||||
"
|
||||
@update:open="
|
||||
(open: boolean) => {
|
||||
if (open) loadDatabases();
|
||||
if (typeof v === 'string') changeConnection(v);
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="h-5 w-auto border-0 rounded-md bg-transparent dark:bg-transparent p-0 px-1 text-xs text-foreground/80 shadow-none focus:ring-0 focus-visible:ring-0 [&_svg]:size-3">
|
||||
<SelectValue :placeholder="t('editor.selectDatabase')">{{ selectedDatabaseLabel }}</SelectValue>
|
||||
<SelectValue :placeholder="t('editor.selectConnection')">{{ connection?.name || t("editor.selectConnection") }}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="option in dbSelectOptions" :key="option.value" :value="option.value">{{ option.label }}</SelectItem>
|
||||
<SelectItem v-if="!dbSelectOptions.length && connection && tab" :value="selectedDatabaseSelectValue">{{ selectedDatabaseLabel }}</SelectItem>
|
||||
<SelectContent class="min-w-48">
|
||||
<SelectItem v-for="conn in connectionStore.connections" :key="conn.id" :value="conn.id">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<DatabaseIcon :db-type="connectionIconType(conn)" class="h-3.5 w-3.5 shrink-0" />
|
||||
<ConnectionGroupBadge :connection-id="conn.id" />
|
||||
<span class="truncate">{{ conn.name }}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<template v-if="connection">
|
||||
<Database class="h-3 w-3 shrink-0 text-foreground/40" />
|
||||
<Select
|
||||
:model-value="selectedDatabaseSelectValue"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (typeof v === 'string') changeDatabase(v);
|
||||
}
|
||||
"
|
||||
@update:open="
|
||||
(open: boolean) => {
|
||||
if (open) loadDatabases();
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="h-5 w-auto border-0 rounded-md bg-transparent dark:bg-transparent p-0 px-1 text-xs text-foreground/80 shadow-none focus:ring-0 focus-visible:ring-0 [&_svg]:size-3">
|
||||
<SelectValue :placeholder="t('editor.selectDatabase')">{{ selectedDatabaseLabel }}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="option in dbSelectOptions" :key="option.value" :value="option.value">{{ option.label }}</SelectItem>
|
||||
<SelectItem v-if="!dbSelectOptions.length && connection && tab" :value="selectedDatabaseSelectValue">{{ selectedDatabaseLabel }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</template>
|
||||
</template>
|
||||
<span class="min-w-0 flex-1" />
|
||||
<!-- Template selector -->
|
||||
<Popover v-model:open="showTemplateSelector">
|
||||
<PopoverTrigger as-child>
|
||||
<button type="button" class="flex min-w-0 max-w-[40%] items-center gap-1 rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="templateSelectorTriggerLabel" :title="templateSelectorTriggerLabel">
|
||||
<FileCode class="h-3 w-3" />
|
||||
<span class="truncate">{{ templateSelectorTriggerLabel }}</span>
|
||||
<svg class="h-3 w-3 shrink-0 opacity-60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" class="w-64 gap-0 p-1.5">
|
||||
<div class="max-h-64 overflow-auto">
|
||||
<div v-if="!promptTemplateStore.isLoaded" class="px-3 py-4 text-center text-xs text-muted-foreground">
|
||||
{{ t("ai.templateSelectorLoading") }}
|
||||
</div>
|
||||
<div v-else-if="promptTemplateStore.templates.length === 0" class="px-3 py-4 text-center text-xs text-muted-foreground">
|
||||
{{ t("ai.templateSelectorEmpty") }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-for="tpl in promptTemplateStore.templates" :key="tpl.id">
|
||||
<button type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs hover:bg-muted" @click="toggleTemplateId(tpl.id)">
|
||||
<div class="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border" :class="activeTemplateIds.includes(tpl.id) ? 'border-primary bg-primary text-primary-foreground' : ''">
|
||||
<Check v-if="activeTemplateIds.includes(tpl.id)" class="h-3 w-3" />
|
||||
</div>
|
||||
<div class="flex-1 truncate text-left">
|
||||
<div class="font-medium">{{ tpl.name }}</div>
|
||||
<div class="text-[10px] text-muted-foreground truncate">{{ tpl.content.slice(0, 60) }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="promptTemplateStore.isLoaded && promptTemplateStore.templates.length > 0" class="border-t mt-1 pt-1 px-1">
|
||||
<button type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground" @click="deselectAllTemplates">
|
||||
{{ t("ai.templateSelectorDeselectAll") }}
|
||||
</button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div v-if="mentionOpen" class="absolute bottom-full left-2 right-2 z-20 mb-1 max-h-56 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md">
|
||||
<div v-if="mentionLoading" class="flex items-center gap-2 px-2 py-2 text-xs text-muted-foreground">
|
||||
@@ -2214,60 +2266,50 @@ async function openExternalUrl(url: string) {
|
||||
@keydown="onPromptKeydown"
|
||||
/>
|
||||
<div class="flex min-w-0 flex-nowrap items-center gap-1.5 overflow-hidden">
|
||||
<!-- Template selector -->
|
||||
<Popover v-model:open="showTemplateSelector">
|
||||
<!-- Combined mode + action selector -->
|
||||
<Popover v-model:open="modeActionOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<button type="button" class="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground">
|
||||
<FileCode class="h-3 w-3" />
|
||||
{{ t("ai.templateSelectorLabel", { label: templateSelectorLabel }) }}
|
||||
<button type="button" class="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground" :aria-label="modeActionTriggerLabel">
|
||||
<component :is="modeIcon" class="h-3 w-3" />
|
||||
<span>{{ modeActionTriggerLabel }}</span>
|
||||
<svg class="h-3 w-3 shrink-0 opacity-60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" class="w-64 gap-0 p-1.5">
|
||||
<div class="max-h-64 overflow-auto">
|
||||
<div v-if="!promptTemplateStore.isLoaded" class="px-3 py-4 text-center text-xs text-muted-foreground">
|
||||
{{ t("ai.templateSelectorLoading") }}
|
||||
</div>
|
||||
<div v-else-if="promptTemplateStore.templates.length === 0" class="px-3 py-4 text-center text-xs text-muted-foreground">
|
||||
{{ t("ai.templateSelectorEmpty") }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-for="tpl in promptTemplateStore.templates" :key="tpl.id">
|
||||
<button type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs hover:bg-muted" @click="toggleTemplateId(tpl.id)">
|
||||
<div class="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm border" :class="activeTemplateIds.includes(tpl.id) ? 'border-primary bg-primary text-primary-foreground' : ''">
|
||||
<Check v-if="activeTemplateIds.includes(tpl.id)" class="h-3 w-3" />
|
||||
</div>
|
||||
<div class="flex-1 truncate text-left">
|
||||
<div class="font-medium">{{ tpl.name }}</div>
|
||||
<div class="text-[10px] text-muted-foreground truncate">{{ tpl.content.slice(0, 60) }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="promptTemplateStore.isLoaded && promptTemplateStore.templates.length > 0" class="border-t mt-1 pt-1 px-1">
|
||||
<button type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground" @click="deselectAllTemplates">
|
||||
{{ t("ai.templateSelectorDeselectAll") }}
|
||||
<PopoverContent align="start" class="w-56 gap-0 p-1.5" @click.stop>
|
||||
<!-- Mode tabs -->
|
||||
<div class="flex items-center gap-1 mb-1.5 px-0.5">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 flex items-center justify-center gap-1.5 rounded-sm px-2 py-1 text-xs"
|
||||
:class="assistantMode === 'ask' ? 'bg-accent text-accent-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-muted'"
|
||||
@click="switchModeActionTab('ask')"
|
||||
>
|
||||
<MessageSquarePlus class="h-3 w-3" />
|
||||
{{ t("ai.modes.ask") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 flex items-center justify-center gap-1.5 rounded-sm px-2 py-1 text-xs"
|
||||
:class="assistantMode === 'agent' ? 'bg-accent text-accent-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-muted'"
|
||||
@click="switchModeActionTab('agent')"
|
||||
>
|
||||
<Bot class="h-3 w-3" />
|
||||
{{ t("ai.modes.agent") }}
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="showActionButtons">
|
||||
<div class="border-t my-1" />
|
||||
<!-- Action list -->
|
||||
<div class="max-h-56 overflow-auto">
|
||||
<button v-for="button in actionButtons" :key="button.action" type="button" class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs" :class="activeAction === button.action ? 'bg-accent' : 'hover:bg-muted'" @click="selectModeActionItem(button.action)">
|
||||
<component :is="button.icon" class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="flex-1 text-left">{{ t(button.key) }}</span>
|
||||
<Check v-if="activeAction === button.action" class="h-3.5 w-3.5 shrink-0" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<LightDropdown
|
||||
v-model="assistantMode"
|
||||
:items="assistantModeItems"
|
||||
:aria-label="activeModeHint"
|
||||
trigger-class="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
item-class="text-xs px-2"
|
||||
/>
|
||||
<LightDropdown
|
||||
v-if="showActionButtons"
|
||||
:model-value="activeAction"
|
||||
:items="actionMenuItems"
|
||||
content-class="w-max min-w-0"
|
||||
trigger-class="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[6px] border px-2 py-0.5 text-[11px] text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
item-class="text-xs px-2"
|
||||
@update:model-value="(value) => selectAction(value as AiAction)"
|
||||
/>
|
||||
<span class="min-w-0 flex-1" />
|
||||
<template v-if="settings.aiConfigs.length > 0">
|
||||
<!-- Combined provider + model selector -->
|
||||
|
||||
Reference in new issue
Block a user