mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-08 23:59:36 +08:00
* 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>
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
// 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);
|
||
});
|
||
});
|