LibreChat/e2e/specs/mock/model-spec-starters.spec.ts
Michael Harvey 05eb986097
💬 feat: Conversation Starters for Model Specs (#13710)
* 💬 feat: Conversation Starters for Model Specs

Adds an optional conversation_starters field to model specs in
librechat.yaml. When the active conversation uses a spec that defines
starters (and no agent/assistant starters apply), the chat landing
renders clickable starter prompts between the landing content and the
chat input; clicking one submits it as the first message.

- data-provider: add conversation_starters to TModelSpec and
  tModelSpecSchema so the field survives strict config parsing
- client: ConversationStarters falls back to the active spec's
  starters via getModelSpec; entity (agent/assistant) starters
  take precedence; starter cards are centered, size to content,
  wrap at word boundaries, stagger their fade-in, and gain a
  focus-visible ring
- sanitizeModelSpecs passes the field through (denylist); covered
  by a new unit test
- e2e: mock spec + tests for rendering, absence, click-to-submit,
  and the MAX_CONVO_STARTERS cap

Closes #3619

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: Sort ChatView imports

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-13 11:38:49 -04:00

49 lines
1.8 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { messagesView, NEW_CHAT_PATH, replyPrompt, replyText, selectModelSpec } from './helpers';
/** Spec with five `conversation_starters` in e2e/config/librechat.e2e.yaml; only four may render. */
const STARTER_SPEC_LABEL = 'E2E Starters';
const STARTER_PROMPTS = [
replyPrompt('starter'),
'Plan my week',
'Third starter prompt',
'Fourth starter prompt',
];
const STARTER_BEYOND_CAP = 'Fifth starter beyond the cap';
/** A spec without `conversation_starters`. */
const PLAIN_SPEC_LABEL = 'E2E Soft Default';
test.describe('model spec conversation starters', () => {
test('starter prompts render on the landing for a spec that defines them', async ({ page }) => {
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
await selectModelSpec(page, STARTER_SPEC_LABEL);
for (const text of STARTER_PROMPTS) {
await expect(page.getByRole('button', { name: text })).toBeVisible();
}
await expect(page.getByRole('button', { name: STARTER_BEYOND_CAP })).toBeHidden();
});
test('a spec without starters shows none', async ({ page }) => {
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
await selectModelSpec(page, PLAIN_SPEC_LABEL);
for (const text of STARTER_PROMPTS) {
await expect(page.getByRole('button', { name: text })).toBeHidden();
}
});
test('clicking a starter submits it as the first message', async ({ page }) => {
await page.goto(NEW_CHAT_PATH, { timeout: 10000 });
await selectModelSpec(page, STARTER_SPEC_LABEL);
const prompt = replyPrompt('starter');
await page.getByRole('button', { name: prompt }).click();
await expect(messagesView(page).getByText(prompt)).toBeVisible({ timeout: 30000 });
await expect(messagesView(page).getByText(replyText('starter'))).toBeVisible({
timeout: 30000,
});
});
});