1 parent
756d4aedc9
commit
3f1544d45e
3 files changed
+59
-7
No files matched your search
@@ -133,7 +133,7 @@ import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { AI_TABLE_MENTION_CANDIDATE_LIMIT, AI_TABLE_MENTION_SCHEMA_LIMIT, filterAiTableMentionCandidates, formatAiTableMention, parseAiTableMentions, type AiTableMention } from "@/lib/ai/aiTableMentions";
|
||||
import { handleAiTableReferenceDropEvent } from "@/lib/ai/aiTableReferenceDrop";
|
||||
import { DBX_TABLE_REFERENCE_DROP_EVENT, clearActiveTableReferencePayload } from "@/lib/editor/queryEditorTableDrop";
|
||||
import { isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/lib/ai/aiPromptKeyboard";
|
||||
import { canSubmitAiPrompt, isAiPromptImeCompositionEvent, shouldSubmitAiPromptOnKeydown } from "@/lib/ai/aiPromptKeyboard";
|
||||
import { isActionableWriteProposalMessage, isActionableWriteSqlProposal, looksLikeActionProposal, looksLikeWriteSqlProposal, shouldGrantWriteSqlOnShortAffirmative } from "@/lib/ai/aiProposalDetect";
|
||||
import { visibleToActualIndex } from "@/lib/ai/aiMessageEdit";
|
||||
import { shouldShowReasoningCharCount, reasoningCharCountClass } from "@/lib/ai/aiReasoningPresentation";
|
||||
@@ -728,6 +728,15 @@ const previewImageAttachment = ref<AiImageAttachment | null>(null);
|
||||
const isAttachmentDragging = ref(false);
|
||||
const pendingAttachmentReads = ref(0);
|
||||
const isAttachmentProcessing = computed(() => pendingAttachmentReads.value > 0);
|
||||
const canSubmitPrompt = computed(() =>
|
||||
canSubmitAiPrompt({
|
||||
prompt: prompt.value,
|
||||
contextItemCount: selectedMentions.value.length + selectedSqlFileMentions.value.length + selectedCsvAttachments.value.length + selectedImageAttachments.value.length,
|
||||
isAttachmentProcessing: isAttachmentProcessing.value,
|
||||
hasTab: !!props.tab,
|
||||
hasConnection: !!props.connection,
|
||||
}),
|
||||
);
|
||||
let browserAttachmentDragDepth = 0;
|
||||
let attachmentDraftEpoch = 0;
|
||||
let attachmentReadQueue: Promise<void> = Promise.resolve();
|
||||
@@ -3871,12 +3880,7 @@ async function openExternalUrl(url: string) {
|
||||
<button v-if="isGenerating" class="h-7 w-7 shrink-0 rounded-full bg-destructive text-destructive-foreground flex items-center justify-center" :title="t('ai.stopGenerating')" @click="cancelStream">
|
||||
<Square class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="h-7 w-7 shrink-0 rounded-full bg-foreground text-background flex items-center justify-center disabled:opacity-30"
|
||||
:disabled="isAttachmentProcessing || (!prompt.trim() && !selectedMentions.length && !selectedSqlFileMentions.length && !selectedCsvAttachments.length && !selectedImageAttachments.length) || !props.tab?.database"
|
||||
@click="send"
|
||||
>
|
||||
<button v-else class="h-7 w-7 shrink-0 rounded-full bg-foreground text-background flex items-center justify-center disabled:opacity-30" :disabled="!canSubmitPrompt" @click="send">
|
||||
<ArrowUp class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { canSubmitAiPrompt } from "@/lib/ai/aiPromptKeyboard";
|
||||
|
||||
const aiAssistantSource = readFileSync(new URL("../../../components/editor/AiAssistant.vue", import.meta.url), "utf8");
|
||||
|
||||
function submissionState(overrides: Partial<Parameters<typeof canSubmitAiPrompt>[0]> = {}) {
|
||||
return {
|
||||
prompt: "",
|
||||
contextItemCount: 0,
|
||||
isAttachmentProcessing: false,
|
||||
hasTab: true,
|
||||
hasConnection: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("AI prompt submission eligibility", () => {
|
||||
it("allows text and attachment submissions without requiring a tab-local database", () => {
|
||||
expect(canSubmitAiPrompt(submissionState({ prompt: "show current users" }))).toBe(true);
|
||||
expect(canSubmitAiPrompt(submissionState({ contextItemCount: 1 }))).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects empty, processing, and missing-context submissions", () => {
|
||||
expect(canSubmitAiPrompt(submissionState())).toBe(false);
|
||||
expect(canSubmitAiPrompt(submissionState({ prompt: "ask", isAttachmentProcessing: true }))).toBe(false);
|
||||
expect(canSubmitAiPrompt(submissionState({ prompt: "ask", hasTab: false }))).toBe(false);
|
||||
expect(canSubmitAiPrompt(submissionState({ prompt: "ask", hasConnection: false }))).toBe(false);
|
||||
});
|
||||
|
||||
it("wires the click button to the shared eligibility rule", () => {
|
||||
expect(aiAssistantSource).toContain("const canSubmitPrompt = computed(() =>");
|
||||
expect(aiAssistantSource).toContain(':disabled="!canSubmitPrompt"');
|
||||
expect(aiAssistantSource).not.toMatch(/:disabled=.*props\.tab\?\.database/);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,18 @@ export interface AiPromptKeydownLikeEvent {
|
||||
keyCode?: number;
|
||||
}
|
||||
|
||||
export interface AiPromptSubmissionState {
|
||||
prompt: string;
|
||||
contextItemCount: number;
|
||||
isAttachmentProcessing: boolean;
|
||||
hasTab: boolean;
|
||||
hasConnection: boolean;
|
||||
}
|
||||
|
||||
export function canSubmitAiPrompt(state: AiPromptSubmissionState): boolean {
|
||||
return !state.isAttachmentProcessing && state.hasTab && state.hasConnection && (state.prompt.trim().length > 0 || state.contextItemCount > 0);
|
||||
}
|
||||
|
||||
export function isAiPromptImeCompositionEvent(event: AiPromptKeydownLikeEvent, compositionActive = false): boolean {
|
||||
return compositionActive || !!event.isComposing || event.keyCode === 229 || event.key === "Process";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user