+
{canvasSkillTags.map((tag) => {
const isActive = selectedSkillTag?.id === tag.id;
+ const TagIcon = getCanvasSkillIconComponent(tag.iconName);
return (
);
})}
@@ -7671,7 +7683,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
decoding="async"
/>
) : (
-
+
)}
{selectedSkillTag.name}
@@ -7683,11 +7695,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
contentEditable={!isSubmitting}
suppressContentEditableWarning
className="min-h-[40px] max-h-[120px] overflow-y-auto text-sm outline-none empty:before:content-[attr(data-placeholder)] empty:before:text-[#9aa1ad]"
- data-placeholder={
- isDetailGenerationMode
- ? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)"
- : "请输入你的设计需求,例如:绿色椅子 替换 Image"
- }
+ data-placeholder={composerPlaceholder}
onInput={(e) => {
// 从 contenteditable 中提取纯文本(排除附件标签文本)
const target = e.target as HTMLDivElement;
diff --git a/components/canvas/canvas-skill-icons.tsx b/components/canvas/canvas-skill-icons.tsx
new file mode 100644
index 000000000..adadce390
--- /dev/null
+++ b/components/canvas/canvas-skill-icons.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import type { LucideIcon } from "lucide-react";
+import {
+ BadgePercent,
+ BookOpen,
+ Clapperboard,
+ ImageIcon,
+ LayoutGrid,
+ Megaphone,
+ MessageCircle,
+ Palette,
+ ShoppingBag,
+ Sparkles,
+} from "lucide-react";
+
+export type CanvasSkillIconOption = {
+ value: string;
+ label: string;
+ icon: LucideIcon;
+};
+
+export const CANVAS_SKILL_ICON_OPTIONS: CanvasSkillIconOption[] = [
+ { value: "message-circle", label: "对话气泡", icon: MessageCircle },
+ { value: "layout-grid", label: "网格", icon: LayoutGrid },
+ { value: "image", label: "图片", icon: ImageIcon },
+ { value: "palette", label: "调色盘", icon: Palette },
+ { value: "shopping-bag", label: "购物袋", icon: ShoppingBag },
+ { value: "megaphone", label: "扩音器", icon: Megaphone },
+ { value: "book-open", label: "故事板", icon: BookOpen },
+ { value: "clapperboard", label: "场记板", icon: Clapperboard },
+ { value: "badge-percent", label: "营销标签", icon: BadgePercent },
+ { value: "sparkles", label: "闪光", icon: Sparkles },
+];
+
+const ICON_MAP: Record = CANVAS_SKILL_ICON_OPTIONS.reduce(
+ (acc, item) => {
+ acc[item.value] = item.icon;
+ return acc;
+ },
+ {} as Record
+);
+
+export function getCanvasSkillIconComponent(iconName?: string | null): LucideIcon {
+ const key = String(iconName || "").trim();
+ return ICON_MAP[key] || MessageCircle;
+}
diff --git a/lib/canvas-skill-tags.ts b/lib/canvas-skill-tags.ts
index 9ff036cee..d23cc0146 100644
--- a/lib/canvas-skill-tags.ts
+++ b/lib/canvas-skill-tags.ts
@@ -4,6 +4,8 @@ export type CanvasSkillTag = {
id: string;
name: string;
prompt: string;
+ placeholder?: string;
+ iconName?: string;
iconUrl?: string;
};
@@ -11,36 +13,48 @@ export const DEFAULT_CANVAS_SKILL_TAGS: CanvasSkillTag[] = [
{
id: "social-carousel",
name: "社媒轮播图",
+ iconName: "layout-grid",
+ placeholder: "例如:做一组 6 张小红书轮播,主色橙白,突出卖点对比",
prompt:
"你是资深社媒设计师。请输出可直接落地的轮播图方案,包含封面、核心卖点页、场景页与转化页。保持统一视觉风格、品牌色和字体层级,文案精炼且有行动号召。",
},
{
id: "social-media-kit",
name: "社交媒体",
+ iconName: "image",
+ placeholder: "例如:给这款产品做 3 套社媒视觉方向,分别偏简约/科技/生活化",
prompt:
"请基于用户需求产出社交媒体视觉方案,优先考虑强识别封面、信息层级清晰和移动端可读性。输出时强调平台适配和可复用模板结构。",
},
{
id: "logo-branding",
name: "Logo 与品牌",
+ iconName: "palette",
+ placeholder: "例如:品牌名 AURORA,主打轻奢护肤,设计 3 个 Logo 概念方向",
prompt:
"请以品牌设计顾问视角工作:先定义品牌关键词与调性,再给出 Logo 方向、配色、字体与应用场景建议。风格需统一、可执行,避免空泛描述。",
},
{
id: "storyboard",
name: "分镜故事板",
+ iconName: "clapperboard",
+ placeholder: "例如:30 秒产品广告,拆成 8 镜头,强调开箱与使用前后对比",
prompt:
"请先构建分镜叙事结构(开场-冲突-转折-收束),再给出每一镜头的画面构图、主体动作、镜头语言和文案要点。保证镜头连续性和视觉一致性。",
},
{
id: "marketing-brochure",
name: "营销宣传册",
+ iconName: "megaphone",
+ placeholder: "例如:做一份 4 页产品宣传册,受众是经销商,风格专业稳重",
prompt:
"请输出可印刷/可投放的宣传册方案,包含封面、核心卖点、案例背书与行动引导。要求信息层级明确、版式节奏均衡、品牌元素统一。",
},
{
id: "ecommerce-main-image",
name: "亚马逊产品套图",
+ iconName: "shopping-bag",
+ placeholder: "例如:亚马逊 7 张套图,主图白底,副图展示材质细节和使用场景",
prompt:
"请按电商平台高转化标准输出主图+副图方案,强调产品主体清晰、卖点可视化、场景可信和对比信息明确。保持产品外观一致,不虚构结构细节。",
},
@@ -68,6 +82,8 @@ export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] {
const row = item && typeof item === "object" ? (item as Record) : {};
const name = normalizeString(row.name);
const prompt = normalizeString(row.prompt);
+ const placeholder = normalizeString(row.placeholder);
+ const iconName = normalizeString(row.iconName);
const iconUrl = normalizeString(row.iconUrl);
if (!name || !prompt) return;
@@ -76,7 +92,14 @@ export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] {
id = `${id}-${index + 1}`;
}
usedIds.add(id);
- normalized.push({ id, name, prompt, iconUrl: iconUrl || undefined });
+ normalized.push({
+ id,
+ name,
+ prompt,
+ placeholder: placeholder || undefined,
+ iconName: iconName || undefined,
+ iconUrl: iconUrl || undefined,
+ });
});
if (normalized.length === 0) {