Commit 703f338b authored by edy's avatar edy

feat(ui): add role team panel, collaboration flow, and new expert empty states

- Add ProjectRole model with normalizeProjectRoles() persistence
- Extract ExpertPanelLead, RoleTeamPanel, RoleCollaborationFlow components
- Add roleIconPaths.tsx for SVG icon rendering by role type
- Add empty states for planner, wechat, geo, zhihu experts
- Revamp xiaohongshu empty state with CSS variable-driven theme system
- Add brand card and workspace logo styles for new expert types
- Wire isStreaming from sendPhase for role status indicators
- Fix geo regex from /geo/ to /\bgeo\b/ to avoid false matches
- Strengthen normalizeProjectRoles validation (non-empty strings, skillIds element check)
- Add prefers-reduced-motion overrides for title animations
- Remove unused --expert-arrow CSS variable
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 3f6095e5
Pipeline #18502 failed
...@@ -7,6 +7,7 @@ import type { ...@@ -7,6 +7,7 @@ import type {
ProjectPackageConfig, ProjectPackageConfig,
ProjectPackageEntry, ProjectPackageEntry,
ProjectPackageEntryType, ProjectPackageEntryType,
ProjectRole,
ProjectSessionState, ProjectSessionState,
ProjectSessionSummary, ProjectSessionSummary,
ProjectSummary, ProjectSummary,
...@@ -306,6 +307,26 @@ function normalizeEntryType(value: unknown): ProjectPackageEntryType | undefined ...@@ -306,6 +307,26 @@ function normalizeEntryType(value: unknown): ProjectPackageEntryType | undefined
return undefined; return undefined;
} }
function normalizeProjectRoles(value: unknown): ProjectRole[] | undefined {
if (!Array.isArray(value)) return undefined
const valid: ProjectRole[] = []
for (const item of value) {
if (!item || typeof item !== "object") continue
const obj = item as Record<string, unknown>
const id = typeof obj.id === "string" ? obj.id.trim() : ""
const name = typeof obj.name === "string" ? obj.name.trim() : ""
const icon = typeof obj.icon === "string" ? obj.icon.trim() : ""
const description = typeof obj.description === "string" ? obj.description.trim() : ""
const skillIds = Array.isArray(obj.skillIds)
? (obj.skillIds as unknown[]).filter((s): s is string => typeof s === "string" && s.length > 0)
: []
if (id && name && icon && description) {
valid.push({ id, name, icon, skillIds, description } as ProjectRole)
}
}
return valid.length > 0 ? valid : undefined
}
function normalizeConfirmationPolicy(value: unknown): ProjectConfirmationPolicy | undefined { function normalizeConfirmationPolicy(value: unknown): ProjectConfirmationPolicy | undefined {
if (value === "always" || value === "never" || value === "publish-only") { if (value === "always" || value === "never" || value === "publish-only") {
return value; return value;
...@@ -1080,7 +1101,8 @@ export class ProjectStoreService { ...@@ -1080,7 +1101,8 @@ export class ProjectStoreService {
projectType: packageConfig?.projectType, projectType: packageConfig?.projectType,
platform: packageConfig?.platform, platform: packageConfig?.platform,
defaultEntryId: packageConfig?.defaultEntry?.id, defaultEntryId: packageConfig?.defaultEntry?.id,
defaultEntryType: packageConfig?.defaultEntry?.type defaultEntryType: packageConfig?.defaultEntry?.type,
roles: normalizeProjectRoles(record.roles)
}; };
} }
......
...@@ -1368,11 +1368,33 @@ export default function App() { ...@@ -1368,11 +1368,33 @@ export default function App() {
</button> </button>
); );
const conversationPanelTitle = viewMode === "experts" ? activeExpertName : "对话"; const conversationPanelTitle = viewMode === "experts" ? activeExpertName : "对话";
const expertWorkspaceLogo = viewMode === "experts" && (activeExpertKey === "xiaohongshu" || activeExpertKey === "douyin") ? ( const expertWorkspaceLogo = viewMode === "experts" ? (() => {
<span className={"expert-workspace-logo expert-workspace-logo-" + activeExpertKey} aria-hidden="true"> switch (activeExpertKey) {
{activeExpertKey === "xiaohongshu" ? <RedBookIcon /> : <DouyinNoteIcon />} case "xiaohongshu":
</span> return (
) : null; <span className="expert-workspace-logo expert-workspace-logo-xiaohongshu" aria-hidden="true">
<RedBookIcon />
</span>
)
case "douyin":
return (
<span className="expert-workspace-logo expert-workspace-logo-douyin" aria-hidden="true">
<DouyinNoteIcon />
</span>
)
case "planner":
case "wechat":
case "geo":
case "zhihu":
return (
<span className={"expert-workspace-logo expert-workspace-logo-" + activeExpertKey} aria-hidden="true">
{renderExpertIcon(activeExpertVisualKey)}
</span>
)
default:
return null
}
})() : null;
const panelActions = ( const panelActions = (
<> <>
<StatusChip <StatusChip
...@@ -1432,6 +1454,8 @@ export default function App() { ...@@ -1432,6 +1454,8 @@ export default function App() {
homeEmptyTitle: homeChatCopy.emptyTitle, homeEmptyTitle: homeChatCopy.emptyTitle,
homeShowcaseEmployees: homeChatCopy.employees homeShowcaseEmployees: homeChatCopy.employees
}, },
isStreaming: sendPhase === "streaming" || sendPhase === "finalizing",
// TODO: wire activeRoleId from the current active role in the ongoing AI collaboration flow
messages: { messages: {
messageListRef, messageListRef,
messages, messages,
......
...@@ -11,6 +11,7 @@ import type { ...@@ -11,6 +11,7 @@ import type {
import type { ChatAttachment, ChatLaunchState, ProjectIntentSuggestion } from "@qjclaw/shared-types" import type { ChatAttachment, ChatLaunchState, ProjectIntentSuggestion } from "@qjclaw/shared-types"
import type { ExpertGuideContent } from "../experts/ExpertsView" import type { ExpertGuideContent } from "../experts/ExpertsView"
import { ExpertEmptyState } from "../experts/ExpertsView" import { ExpertEmptyState } from "../experts/ExpertsView"
import { ExpertPanelLead } from "../experts/ExpertPanelLead"
import type { ExpertVisualKey } from "../shell/ExpertTree" import type { ExpertVisualKey } from "../shell/ExpertTree"
import { BindEntry } from "./BindEntry" import { BindEntry } from "./BindEntry"
import { ChatComposer } from "./ChatComposer" import { ChatComposer } from "./ChatComposer"
...@@ -174,6 +175,8 @@ interface ConversationWorkspaceViewProps { ...@@ -174,6 +175,8 @@ interface ConversationWorkspaceViewProps {
messages: ConversationWorkspaceMessagesProps messages: ConversationWorkspaceMessagesProps
composer: ConversationWorkspaceComposerProps composer: ConversationWorkspaceComposerProps
actions: ConversationWorkspaceActionsProps actions: ConversationWorkspaceActionsProps
activeRoleId?: string
isStreaming?: boolean
} }
export function ConversationWorkspaceView({ export function ConversationWorkspaceView({
...@@ -183,7 +186,9 @@ export function ConversationWorkspaceView({ ...@@ -183,7 +186,9 @@ export function ConversationWorkspaceView({
emptyState, emptyState,
messages, messages,
composer, composer,
actions actions,
activeRoleId,
isStreaming
}: ConversationWorkspaceViewProps) { }: ConversationWorkspaceViewProps) {
const homeMicrocopyStatus = emptyState.selectedSkillIsDefault ? "默认工作区" : "已切换" const homeMicrocopyStatus = emptyState.selectedSkillIsDefault ? "默认工作区" : "已切换"
...@@ -202,22 +207,14 @@ export function ConversationWorkspaceView({ ...@@ -202,22 +207,14 @@ export function ConversationWorkspaceView({
{homeMicrocopyStatus} {homeMicrocopyStatus}
</span> </span>
</div> </div>
) : emptyState.expertWorkspaceLogo ? (
<div className={"expert-hero-heading expert-brand-card expert-brand-card-" + emptyState.activeExpertKey}>
{emptyState.expertWorkspaceLogo}
<span className="expert-hero-body">
<strong className="expert-hero-title">{emptyState.activeExpertName}</strong>
</span>
</div>
) : ( ) : (
<div className="conversation-panel-kicker expert-hero-kicker"> <ExpertPanelLead
<span className={"expert-hero-icon expert-hero-icon-" + emptyState.activeExpertVisualKey} aria-hidden="true"> activeExpertKey={emptyState.activeExpertKey}
{actions.renderExpertIcon(emptyState.activeExpertVisualKey)} activeExpertName={emptyState.activeExpertName}
</span> activeExpertVisualKey={emptyState.activeExpertVisualKey}
<span className="expert-hero-copy"> expertWorkspaceLogo={emptyState.expertWorkspaceLogo}
<strong>{emptyState.activeExpertName}</strong> renderExpertIcon={actions.renderExpertIcon}
</span> />
</div>
) )
const activeEmptyState = viewMode === "experts" ? ( const activeEmptyState = viewMode === "experts" ? (
...@@ -227,6 +224,8 @@ export function ConversationWorkspaceView({ ...@@ -227,6 +224,8 @@ export function ConversationWorkspaceView({
activeExpertGuide={emptyState.activeExpertGuide} activeExpertGuide={emptyState.activeExpertGuide}
starterQuestionsHint={emptyState.starterQuestionsHint} starterQuestionsHint={emptyState.starterQuestionsHint}
onStarterPrompt={actions.onStarterPrompt} onStarterPrompt={actions.onStarterPrompt}
activeRoleId={activeRoleId}
isStreaming={isStreaming}
/> />
) : ( ) : (
<div className="empty-state home-empty-state"> <div className="empty-state home-empty-state">
......
import type { ReactNode } from "react"
export interface RoleIconOptions {
width?: number
height?: number
strokeWidth?: number
}
type RoleIconKey = "lightbulb" | "pen" | "users" | "chat" | "chart" | "search" | "settings"
const SVG_PATHS: Record<RoleIconKey, string[]> = {
lightbulb: [
"M9 18h6",
"M10 22h4",
"M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0018 8 6 6 0 006 8c0 1.23.5 2.47 1.5 3.5.76.76 1.23 1.52 1.41 2.5",
],
pen: [
"M12 20h9",
"M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4L16.5 3.5z",
],
users: [
"M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2",
"M9 7a4 4 0 100-8 4 4 0 000 8z",
"M23 21v-2a4 4 0 00-3-3.87",
"M16 3.13a4 4 0 010 7.75",
],
chat: [
"M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z",
],
chart: [
"M18 20V10",
"M12 20V4",
"M6 20v-6",
],
search: [
"M21 21l-4.3-4.3",
"M11 3a8 8 0 100 16 8 8 0 000-16z",
],
settings: [
"M12 15a3 3 0 100-6 3 3 0 000 6z",
"M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 002.83-2.83l-.06-.06a1.65 1.65 0 00-.33-1.82 1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z",
],
}
export function renderRoleIcon(key: string, opts: RoleIconOptions = {}): ReactNode {
const { width = 22, height = 22, strokeWidth = 1.6 } = opts
const paths = SVG_PATHS[key as RoleIconKey] ?? SVG_PATHS.lightbulb
return (
<svg
width={width}
height={height}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
>
{paths.map((d) => (
<path key={d} d={d} />
))}
</svg>
)
}
import type { ReactNode } from "react"
import type { ExpertKey, ExpertVisualKey } from "./ExpertsView"
interface ExpertPanelLeadProps {
activeExpertKey: ExpertKey
activeExpertName: string
activeExpertVisualKey: ExpertVisualKey
expertWorkspaceLogo: ReactNode
renderExpertIcon: (expertKey: ExpertVisualKey) => ReactNode
}
export function ExpertPanelLead({
activeExpertKey,
activeExpertName,
activeExpertVisualKey,
expertWorkspaceLogo,
renderExpertIcon
}: ExpertPanelLeadProps) {
return expertWorkspaceLogo ? (
<div className={"expert-hero-heading expert-brand-card expert-brand-card-" + activeExpertKey}>
{expertWorkspaceLogo}
<span className="expert-hero-body">
<strong className="expert-hero-title">{activeExpertName}</strong>
</span>
</div>
) : (
<div className="conversation-panel-kicker expert-hero-kicker">
<span className={"expert-hero-icon expert-hero-icon-" + activeExpertVisualKey} aria-hidden="true">
{renderExpertIcon(activeExpertVisualKey)}
</span>
<span className="expert-hero-copy">
<strong>{activeExpertName}</strong>
</span>
</div>
)
}
import type { ProjectRole } from "@qjclaw/shared-types"
import { Fragment } from "react"
import { renderRoleIcon } from "../chat/roleIconPaths"
export interface CollaborationHandoff {
fromRoleId: string
toRoleId: string
label: string
}
interface RoleCollaborationFlowProps {
roles: ProjectRole[]
handoffs: CollaborationHandoff[]
activeRoleId?: string
isStreaming?: boolean
}
export function RoleCollaborationFlow({ roles, handoffs, activeRoleId, isStreaming }: RoleCollaborationFlowProps) {
if (!roles || roles.length === 0) return null
const handoffByFromId = new Map(handoffs.map((h) => [h.fromRoleId, h]))
return (
<div className="role-collaboration-flow">
<div className="collab-header">
<span className="collab-label">运营团队</span>
</div>
<div className="collab-pipeline">
{roles.map((role, index) => {
const isActive = isStreaming && activeRoleId === role.id
const handoff = handoffByFromId.get(role.id)
return (
<Fragment key={role.id}>
<div
className={`collab-role-card${isActive ? " collab-role-active" : ""}`}
title={`${role.name}:${role.description}`}
>
<div className="collab-role-icon" aria-hidden="true">
{renderRoleIcon(role.icon, { width: 22, height: 22, strokeWidth: 1.6 })}
</div>
<div className="collab-role-body">
<strong className="collab-role-name">{role.name}</strong>
<span className="collab-role-desc">{role.description}</span>
</div>
{isActive ? (
<span className="collab-role-dot active" aria-label="工作中" />
) : null}
</div>
{handoff ? (
<div className="collab-connector">
<div className="collab-connector-line" aria-hidden="true">
<span className="collab-connector-arrow" />
</div>
<span className="collab-handoff-label">{handoff.label}</span>
</div>
) : index < roles.length - 1 ? (
<div className="collab-connector">
<div className="collab-connector-line" aria-hidden="true">
<span className="collab-connector-arrow" />
</div>
</div>
) : null}
</Fragment>
)
})}
</div>
</div>
)
}
import type { ProjectRole } from "@qjclaw/shared-types"
import { renderRoleIcon } from "../chat/roleIconPaths"
interface RoleTeamPanelProps {
roles: ProjectRole[]
onRoleClick?: (role: ProjectRole) => void
activeRoleId?: string
isStreaming?: boolean
}
function RoleIcon({ icon }: { icon: string }) {
return (
<span className="role-card-icon" aria-hidden="true">
{renderRoleIcon(icon, { width: 22, height: 22, strokeWidth: 1.6 })}
</span>
)
}
function getDotState(roleId: string, activeRoleId: string | undefined, isStreaming: boolean | undefined): string {
if (!isStreaming) return "idle"
if (activeRoleId === roleId) return "active"
return "idle"
}
export function RoleTeamPanel({ roles, onRoleClick, activeRoleId, isStreaming }: RoleTeamPanelProps) {
if (!roles || roles.length === 0) return null
return (
<div className="role-team-panel">
<div className="role-team-header">
<span className="role-team-label">运营团队</span>
</div>
<div className="role-team-grid">
{roles.map((role) => {
const dotState = getDotState(role.id, activeRoleId, isStreaming)
return (
<button
key={role.id}
type="button"
className="role-card"
onClick={() => onRoleClick?.(role)}
title={`${role.name}:${role.description}`}
aria-label={`${role.name} - ${role.description}`}
>
<RoleIcon icon={role.icon} />
<div className="role-card-body">
<strong className="role-card-name">{role.name}</strong>
<span className="role-card-desc">{role.description}</span>
</div>
<span
className={`role-status-dot ${dotState}`}
aria-label={dotState === "active" ? "工作中" : "待命中"}
title={dotState === "active" ? "工作中" : "待命中"}
/>
</button>
)
})}
</div>
</div>
)
}
...@@ -60,7 +60,7 @@ export function resolveExpertKey(project: ExpertProject | undefined): ExpertKey ...@@ -60,7 +60,7 @@ export function resolveExpertKey(project: ExpertProject | undefined): ExpertKey
if (/content-account|planner|账号规划|内容账号规划|内容创作/.test(seed)) { if (/content-account|planner|账号规划|内容账号规划|内容创作/.test(seed)) {
return "planner" return "planner"
} }
if (/(^|[\s-])geo($|[\s-])/.test(seed)) { if (/\bgeo\b/.test(seed)) {
return "geo" return "geo"
} }
if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) { if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) {
...@@ -105,7 +105,7 @@ export function resolveExpertVisualKey(project?: ExpertProject, definition?: Exp ...@@ -105,7 +105,7 @@ export function resolveExpertVisualKey(project?: ExpertProject, definition?: Exp
if (/(^|[\s-])x($|[\s-])|twitter/.test(seed)) { if (/(^|[\s-])x($|[\s-])|twitter/.test(seed)) {
return "x" return "x"
} }
if (/(^|[\s-])geo($|[\s-])/.test(seed)) { if (/\bgeo\b/.test(seed)) {
return "geo" return "geo"
} }
if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) { if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) {
...@@ -176,8 +176,8 @@ function getExpertGuide(project: ExpertProject | undefined): ExpertGuideContent ...@@ -176,8 +176,8 @@ function getExpertGuide(project: ExpertProject | undefined): ExpertGuideContent
requirementChecklist: ["选题方向", "文章类型", "目标读者", "核心信息", "期望效果"], requirementChecklist: ["选题方向", "文章类型", "目标读者", "核心信息", "期望效果"],
workflowSteps: ["选题规划与内容日历", "标题优化(提高打开率)", "文章结构梳理与排版建议", "涨粉策略与粉丝运营"], workflowSteps: ["选题规划与内容日历", "标题优化(提高打开率)", "文章结构梳理与排版建议", "涨粉策略与粉丝运营"],
prompts: [ prompts: [
"帮我策划一篇公众号科普长文,主题是XXX", "帮我策划一篇公众号科普长文,主题是职场效率工具,目标读者是职场新人。",
"分析我这个公众号的选题方向,给出优化建议", "分析生活类公众号的选题方向,给出优化建议",
"给我5个能涨粉的互动活动方案" "给我5个能涨粉的互动活动方案"
] ]
} }
...@@ -202,12 +202,12 @@ function getExpertGuide(project: ExpertProject | undefined): ExpertGuideContent ...@@ -202,12 +202,12 @@ function getExpertGuide(project: ExpertProject | undefined): ExpertGuideContent
case "zhihu": case "zhihu":
return { return {
greeting: "在知乎,专业是最好的流量", greeting: "在知乎,专业是最好的流量",
summary: "帮你选题、写回答、建人设,把专业积累变成影响力。", summary: "",
intro: "知乎用户看重专业深度和真实经验。告诉我你的专业领域和人设目标,我来帮你策划内容策略。", intro: "知乎用户看重专业深度和真实经验。告诉我你的专业领域和人设目标,我来帮你策划内容策略。",
requirementChecklist: ["专业领域", "人设定位", "目标受众", "内容类型", "更新频率"], requirementChecklist: ["专业领域", "人设定位", "目标受众", "内容类型", "更新频率"],
workflowSteps: ["人设定位:你希望被记住的专业标签是什么?", "选题策略:追踪热点问题 vs 深耕垂直领域", "回答结构:开篇抓人 → 专业拆解 → 金句收尾"], workflowSteps: ["人设定位:你希望被记住的专业标签是什么?", "选题策略:追踪热点问题 vs 深耕垂直领域", "回答结构:开篇抓人 → 专业拆解 → 金句收尾"],
prompts: [ prompts: [
"帮我找5个XX领域的高关注问题,写深度回答", "帮我找5个职场领域的高关注问题,写深度回答",
"给我设计一个知乎个人主页的专业定位方案", "给我设计一个知乎个人主页的专业定位方案",
"把这篇公众号文章改写成知乎回答风格" "把这篇公众号文章改写成知乎回答风格"
] ]
......
...@@ -127,7 +127,15 @@ const mockExpertDefinitions: ExpertDefinition[] = [ ...@@ -127,7 +127,15 @@ const mockExpertDefinitions: ExpertDefinition[] = [
const mockProjects: WorkspaceSummary["projects"] = [ const mockProjects: WorkspaceSummary["projects"] = [
{ id: "content-account-planner", name: "content-account-planner", displayName: "内容创作者", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "planner" }, { id: "content-account-planner", name: "content-account-planner", displayName: "内容创作者", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "planner" },
{ id: "zhihu", name: "zhihu-expert", displayName: "知乎策略师", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "zhihu" }, { id: "zhihu", name: "zhihu-expert", displayName: "知乎策略师", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "zhihu" },
{ id: "xiaohongshu", name: "openclaw-xiaohongshu-skills-delivery", displayName: "小红书运营专家", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "xiaohongshu" }, { id: "xiaohongshu", name: "openclaw-xiaohongshu-skills-delivery", displayName: "小红书运营专家", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 9, ready: true, platform: "xiaohongshu",
roles: [
{ id: "strategist", name: "策划专家", icon: "lightbulb", skillIds: ["xhs-strategist"], description: "品牌诊断、内容策略与选题规划" },
{ id: "copywriter", name: "文案专家", icon: "pen", skillIds: ["xhs-copywriter", "xhs-create-note", "xhs-pipeline"], description: "笔记标题、正文、标签与封面文案" },
{ id: "kol", name: "达人投放", icon: "users", skillIds: ["xhs-kol"], description: "KOL/KOC筛选与投放管理" },
{ id: "community", name: "社区运营", icon: "chat", skillIds: ["xhs-community"], description: "评论互动与品牌口碑管理" },
{ id: "analytics", name: "数据分析", icon: "chart", skillIds: ["xhs-analytics"], description: "数据追踪与策略优化" },
]
},
{ id: "douyin", name: "openclaw-douyin-skills-delivery", displayName: "抖音专家", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "douyin" }, { id: "douyin", name: "openclaw-douyin-skills-delivery", displayName: "抖音专家", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "douyin" },
{ id: "wechat-official-account", name: "wechat-official-account", displayName: "微信公众号运营", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "wechat" }, { id: "wechat-official-account", name: "wechat-official-account", displayName: "微信公众号运营", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "wechat" },
{ id: "geo", name: "geo", displayName: "AI推荐引擎优化", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "geo" }, { id: "geo", name: "geo", displayName: "AI推荐引擎优化", version: "demo-project", updatedAt: new Date().toISOString(), skillCount: 2, ready: true, platform: "geo" },
......
...@@ -652,6 +652,70 @@ ...@@ -652,6 +652,70 @@
box-shadow: 0 12px 24px rgba(109, 125, 255, 0.2); box-shadow: 0 12px 24px rgba(109, 125, 255, 0.2);
} }
.conversation-shell .expert-brand-card-xiaohongshu {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(255, 241, 242, 0.78));
}
.conversation-shell .expert-brand-card-douyin {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(236, 254, 255, 0.78));
}
.conversation-shell .expert-brand-card-planner {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(245, 243, 255, 0.78));
}
.conversation-shell .expert-brand-card-wechat {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(240, 253, 244, 0.78));
}
.conversation-shell .expert-brand-card-geo {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(239, 246, 255, 0.78));
}
.conversation-shell .expert-brand-card-zhihu {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(238, 242, 255, 0.78));
}
.conversation-shell .expert-workspace-logo-planner {
background: linear-gradient(135deg, rgba(245, 243, 255, 0.96), rgba(237, 233, 254, 0.9));
box-shadow: 0 14px 28px rgba(168, 85, 247, 0.16);
}
.conversation-shell .expert-brand-card .expert-workspace-logo-planner {
background: linear-gradient(135deg, #a855f7 0%, #7c3aed 56%, #6d28d9 100%);
box-shadow: 0 12px 24px rgba(168, 85, 247, 0.22);
}
.conversation-shell .expert-workspace-logo-wechat {
background: linear-gradient(135deg, rgba(240, 253, 244, 0.96), rgba(220, 252, 231, 0.9));
box-shadow: 0 14px 28px rgba(34, 197, 94, 0.16);
}
.conversation-shell .expert-brand-card .expert-workspace-logo-wechat {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 56%, #15803d 100%);
box-shadow: 0 12px 24px rgba(34, 197, 94, 0.22);
}
.conversation-shell .expert-workspace-logo-geo {
background: linear-gradient(135deg, rgba(239, 246, 255, 0.96), rgba(219, 234, 254, 0.9));
box-shadow: 0 14px 28px rgba(59, 130, 246, 0.16);
}
.conversation-shell .expert-brand-card .expert-workspace-logo-geo {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 56%, #1d4ed8 100%);
box-shadow: 0 12px 24px rgba(59, 130, 246, 0.22);
}
.conversation-shell .expert-workspace-logo-zhihu {
background: linear-gradient(135deg, rgba(238, 242, 255, 0.96), rgba(224, 231, 255, 0.9));
box-shadow: 0 14px 28px rgba(0, 102, 255, 0.16);
}
.conversation-shell .expert-brand-card .expert-workspace-logo-zhihu {
background: linear-gradient(135deg, #0066ff 0%, #0052cc 56%, #003d99 100%);
box-shadow: 0 12px 24px rgba(0, 102, 255, 0.22);
}
.conversation-shell .conversation-panel-actions { .conversation-shell .conversation-panel-actions {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
......
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment