Files
multica/packages/views/agents/components/instructions-editor.tsx
Naiyuan Qing 5f2222b83d feat(agents): one-click create-from-template UI
Frontend half of Phase 1. CreateAgentDialog becomes a state machine
spanning four steps:

  chooser          → Start blank / From template cards
  blank-form       → existing manual form (post-chooser)
  duplicate-form   → existing form pre-filled from a duplicated agent
  template-picker  → grid of templates, click navigates to detail
  template-detail  → instructions + skill list preview + one-click Use

Picking a template never lands on the form: name auto-deduped against
existingAgentNames, runtime = first usable one, visibility = private.
Refinement happens on the agent detail page if needed. Same rationale
the doc spells out — templates exist precisely to skip configuration.

New components, all collapsible-by-default so quick-create stays fast:
  - template-picker.tsx — categorised grid, lucide icons + semantic
    accent tokens resolved through static maps so Tailwind's JIT picks
    up every variant (dynamic class strings would silently miss).
  - template-detail.tsx — instructions preview, skill list with cached
    descriptions, Use CTA. Renders the failedURLs banner when a 422
    fires — the only step that can trigger that response.
  - instructions-editor.tsx — collapsed preview-card / expanded full
    ContentEditor.
  - skill-multi-select.tsx + skill-picker-list.tsx — shared multi-
    select surface, also adopted by the existing skill-add-dialog.
  - avatar-picker.tsx — agent avatar upload, mirrors the inspector's
    visual language.

Schema-defended client (CLAUDE.md → API Response Compatibility): the
three new endpoints are wired through parseWithFallback with lenient
zod schemas. Desktop builds outlive any given server — a future
field rename / wrapping must not white-screen older installs.
listAgentTemplates accepts both the current bare array and a future
{templates: [...]} envelope. Coverage: 7 new schema-test cases in
schema.test.ts (null body, missing skills/instructions, malformed
create response, envelope migration).

Catalog + detail go through TanStack Query with staleTime: Infinity —
workspace-independent static data, no per-mount refetch.

Other:
- skill-add-dialog becomes a true multi-select (Confirm button +
  checkbox list); attached skills are filtered out of the list.
- agents-page hands the freshly-created Agent back to the dialog so a
  follow-up setAgentSkills can attach the form-selected skills.
- agent-overview-pane drops the mx-auto/max-w-2xl frame on config-
  tab content; the wider dialog visual language reads better with
  tabs filling the column.
- Every new UI string lives in both en/agents.json and
  zh-Hans/agents.json under create_dialog.* / tab_body.skills.* —
  locales/parity.test.ts blocks drift in CI.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:47:21 +08:00

117 lines
3.9 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
import { ChevronDown, FileText, X } from "lucide-react";
import { ContentEditor, type ContentEditorRef } from "../../editor";
import { Button } from "@multica/ui/components/ui/button";
import { cn } from "@multica/ui/lib/utils";
import { useT } from "../../i18n";
interface InstructionsEditorProps {
/** Markdown source. Used both as default value when expanded and as
* preview text when collapsed. */
value: string;
/** Fires on every keystroke (debounced inside ContentEditor). */
onChange: (value: string) => void;
/** Optional placeholder override. Defaults to the i18n "click to write"
* copy; the create dialog passes the duplicate-specific string for
* agents being cloned. */
placeholder?: string;
}
/**
* Collapsible Instructions field for the create-agent dialog. Stays compact
* until the user wants to write — most agents only need instructions when
* they're being authored carefully, not on every quick-create.
*
* Two states:
* collapsed → small clickable card, shows a preview of `value` (or the
* placeholder when empty). One click expands.
* expanded → full ContentEditor (markdown, bubble menu, mention support,
* attachment upload). "Collapse" button on the right of the
* header tucks it back; value is preserved.
*/
export function InstructionsEditor({
value,
onChange,
placeholder,
}: InstructionsEditorProps) {
const { t } = useT("agents");
const [expanded, setExpanded] = useState(false);
const editorRef = useRef<ContentEditorRef>(null);
const label = t(($) => $.create_dialog.instructions.label);
const resolvedPlaceholder =
placeholder ?? t(($) => $.create_dialog.instructions.placeholder_blank);
const expand = () => {
setExpanded(true);
// Focus on next tick so the editor mounts first.
setTimeout(() => editorRef.current?.focus(), 0);
};
if (!expanded) {
return (
<div>
<div className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{label}
</div>
<button
type="button"
onClick={expand}
className="mt-1.5 flex w-full items-start gap-2.5 rounded-lg border bg-card px-3 py-3 text-left transition-colors hover:border-primary/40 hover:bg-accent/40"
>
<FileText className="mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<div className="min-w-0 flex-1">
{value.trim() ? (
// Preview: first 2 lines of markdown, ellipsised.
<div className="line-clamp-2 whitespace-pre-wrap text-sm text-foreground/80">
{value}
</div>
) : (
<div className="text-sm text-muted-foreground">{resolvedPlaceholder}</div>
)}
</div>
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground/40" />
</button>
</div>
);
}
return (
<div>
<div className="flex items-center justify-between">
<div className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{label}
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setExpanded(false)}
className="h-6 gap-1 px-2 text-xs"
>
<X className="h-3 w-3" />
{t(($) => $.create_dialog.instructions.collapse)}
</Button>
</div>
<div
className={cn(
"mt-1.5 rounded-lg border bg-card",
"focus-within:border-primary/40",
)}
>
<ContentEditor
ref={editorRef}
defaultValue={value}
onUpdate={onChange}
placeholder={t(($) => $.create_dialog.instructions.editor_placeholder)}
className="min-h-[160px] max-h-[320px] overflow-y-auto px-3 py-2.5 text-sm"
showBubbleMenu={true}
disableMentions={true}
/>
</div>
</div>
);
}