Files
nexu-io-open-design/apps/web/tests/components/HomeView.activePluginChip.test.tsx
elihahah666 6313286232 feat(home): plugin detail "Use plugin" split menu + align use-with-query with example-prompt seed (#3997)
* feat(home): plugin detail "Use plugin" split menu + align use-with-query with example-prompt seed

- PreviewModal.primaryAction gains an optional split-button menu. The plugin
  detail modal's "Use plugin" now offers two variants — 仅使用插件 (use) and
  复刻此内容 (use-with-query) — wired through the existing routePluginUse action,
  i18n'd across all 19 locales (new preview.usePlugin / usePluginOnly* /
  replicateContent* keys).
- use-with-query now seeds the composer with the SAME human-friendly text the
  Home example-prompt cards use. Extracted examplePresetSeedPrompt (+ its
  dependencies) from HomeHero into a shared plugins-home/presetSeedPrompt module
  so both surfaces share one source of truth, instead of the detail modal
  dumping the raw od.useCase.query meta-instruction ("follow the en field
  verbatim; start from example.html") into the textarea.
- The example-prompt active plugin chip now shows the same × clear button as the
  Community path (activeBadge.isExplicitPlugin distinguishes a user-picked
  plugin from a task-chip default).
- Analytics: community_gallery/use_plugin (action=use|use_with_query) lands the
  documented row-118 event; chat_composer/plugin_chip_clear mirrors
  working_dir_clear for the chip ×.

* feat(prompt): generate artifact copy in the user's UI locale

Chinese (and other non-English) prompts were producing English pages: when a
template / plugin / design-system ships English reference content, the agent
copied that wording verbatim. The UI-locale override only covered chat prose
and generated form controls, not the artifact body.

Extend the locale override so all user-visible copy in the generated
HTML/React/page/deck (headings, body, nav, button/link labels, captions, alt
text, form fields) is written in the run's UI locale by default — and reference
content in another language must be translated/adapted, not shipped verbatim.
Brand names / code / identifiers stay as-is; an explicit user request for a
different output language still wins.

* fix(plugin-details): scenario fallback gets the use/use-with-query split menu

Review follow-up (#3997): PluginScenarioDetail was a single onUse(record,'use')
button, so any plugin routed to the text/scenario fallback lost the new split
action — users couldn't pick "Use plugin only" vs "Replicate this content" even
when od.useCase.query exists. Reuse the shared buildPluginUseMenu in the footer
as an upward-opening split button (caret menu), matching the html/design/media
variants, and keep a plain single button when there is no query.

Also update the home use-with-query visual e2e: use-with-query now seeds the
rendered preset text ("Draft a topic deck.") rather than the raw `{{topic}}`
query, matching the example-prompt card path.

Regression tests: scenario+query renders the split menu caret; scenario without
query keeps a single button.

* fix(home): preserve placeholder write-back when use-with-query seed is the query

Review follow-up (#3997, blocking): use-with-query unconditionally nulled
`queryTemplate`, dropping the only path that writes composer edits back into
`active.inputs`. For presets whose seed IS the rendered (human-friendly) query,
that meant editing a hydrated value before running submitted the plugin with
stale default inputs.

examplePresetSeedPrompt now returns `{ text, fromRenderedQuery }`. routePluginUse
keeps the raw `{{...}}` query template (and `queryTemplateAllowsPrefix`) only when
the seed came from the rendered query; it still nulls the template when we fell
back to a description / meta-instruction seed (no placeholders to extract). The
meta-instruction → description fix is unchanged.

Tests: friendly-query use-with-query now asserts the placeholder edit flows back
into pluginInputs (empty-draft and edited-draft-prefix cases); the
meta-instruction case still seeds the description with no write-back.

* fix(home): persist explicit-pick flag for active plugin chip visibility

Review follow-up (#3997): the active plugin chip's visibility + clear-button
gate inferred "explicit pick" from `record.id !== defaultPluginIdForChip(chipId)`.
An example-prompt preset whose plugin id equals the chip's default plugin — the
prototype rail binds `example-web-prototype`, which is also the prototype chip's
default — was therefore misclassified as a task-default binding, so its chip/×
never appeared even though the user explicitly picked the preset.

ActivePlugin now carries an `explicitPick` flag, set on the example-prompt
preset (useExamplePlugin) and Community / detail-modal (routePluginUse) paths.
Both `activeBadge` (chip label vs plugin title + ×) and
`shouldShowActivePluginChip` (the chip-visibility gate) short-circuit on it
before falling back to the id heuristic — so an explicit pick always surfaces
its own chip, while a type-chip default binding still stands in for the task
chip. Added a HomeView.activePluginChip regression covering the preset-equals-
default case.

* fix(home): keep prefix matching on for use-with-query placeholder extraction

Review follow-up (#3997): `queryTemplateAllowsPrefix` only enabled prefix
matching when the composer already had text BEFORE the user clicked Replicate.
The empty-draft → prepend-intro → edit-placeholder path then fell through:
extraction stayed anchored at `^`, so a placeholder edit after a freshly-added
intro stopped writing back into active.inputs and submit could re-apply stale
defaults.

Enable prefix matching whenever we track the query template (`hasTemplate`).
Suffix matching is equally correct with no prefix, and now tolerates an intro
the user prepends after the seed is inserted. Added the empty-draft → add
prefix → edit placeholder regression.

---------

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-06-09 16:04:03 +00:00

79 lines
2.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Regression for the active-plugin chip visibility gate (#3997 review).
//
// The gate decides whether the composer shows a dedicated plugin chip (with its
// own clear ×) or lets a task-type chip stand in. It must key off the persisted
// `explicitPick` flag — set when the user picks an example-prompt preset or a
// Community card — NOT off `record.id !== defaultPluginIdForChip(chipId)`. The
// prototype rail's default plugin is `example-web-prototype` (see
// home-hero/chips.ts), so a preset pick of that very plugin would be wrongly
// classified as a task-default binding and never surface its chip under the old
// id-equality heuristic.
import { describe, expect, it } from 'vitest';
import {
shouldShowActivePluginChip,
type ActivePlugin,
} from '../../src/components/HomeView';
function activeFor(
pluginId: string,
chipId: string | null,
explicitPick: boolean,
): ActivePlugin {
// Only `record.id`, `chipId`, and `explicitPick` drive the gate; the rest is
// padded to satisfy the type.
return {
record: { id: pluginId } as ActivePlugin['record'],
result: null,
inputs: {},
inputFields: [],
inputsValid: true,
queryTemplate: null,
lastRenderedPrompt: null,
projectKind: null,
chipId,
mediaSurface: null,
projectMetadata: null,
editableInputNames: [],
preserveInputFields: false,
suppressPromptSync: false,
explicitPick,
};
}
describe('shouldShowActivePluginChip', () => {
it('surfaces an explicit example-prompt preset even when its plugin id equals the chip default', () => {
// prototype → default plugin example-web-prototype; explicit preset pick.
expect(
shouldShowActivePluginChip(
activeFor('example-web-prototype', 'prototype', true),
),
).toBe(true);
});
it('suppresses the plugin chip for a task-chip default-plugin binding', () => {
// Same plugin + chip, but bound through the type chip (not an explicit pick):
// the task chip stands in, so no separate plugin chip.
expect(
shouldShowActivePluginChip(
activeFor('example-web-prototype', 'prototype', false),
),
).toBe(false);
});
it('surfaces a non-explicit plugin that is not the chip default', () => {
expect(
shouldShowActivePluginChip(
activeFor('some-other-plugin', 'prototype', false),
),
).toBe(true);
});
it('surfaces a Community pick (no chip) and ignores null active', () => {
expect(
shouldShowActivePluginChip(activeFor('community-plugin', null, true)),
).toBe(true);
expect(shouldShowActivePluginChip(null)).toBe(false);
});
});