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 {
ProjectPackageConfig,
ProjectPackageEntry,
ProjectPackageEntryType,
ProjectRole,
ProjectSessionState,
ProjectSessionSummary,
ProjectSummary,
......@@ -306,6 +307,26 @@ function normalizeEntryType(value: unknown): ProjectPackageEntryType | 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 {
if (value === "always" || value === "never" || value === "publish-only") {
return value;
......@@ -1080,7 +1101,8 @@ export class ProjectStoreService {
projectType: packageConfig?.projectType,
platform: packageConfig?.platform,
defaultEntryId: packageConfig?.defaultEntry?.id,
defaultEntryType: packageConfig?.defaultEntry?.type
defaultEntryType: packageConfig?.defaultEntry?.type,
roles: normalizeProjectRoles(record.roles)
};
}
......
......@@ -1368,11 +1368,33 @@ export default function App() {
</button>
);
const conversationPanelTitle = viewMode === "experts" ? activeExpertName : "对话";
const expertWorkspaceLogo = viewMode === "experts" && (activeExpertKey === "xiaohongshu" || activeExpertKey === "douyin") ? (
const expertWorkspaceLogo = viewMode === "experts" ? (() => {
switch (activeExpertKey) {
case "xiaohongshu":
return (
<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">
{activeExpertKey === "xiaohongshu" ? <RedBookIcon /> : <DouyinNoteIcon />}
{renderExpertIcon(activeExpertVisualKey)}
</span>
) : null;
)
default:
return null
}
})() : null;
const panelActions = (
<>
<StatusChip
......@@ -1432,6 +1454,8 @@ export default function App() {
homeEmptyTitle: homeChatCopy.emptyTitle,
homeShowcaseEmployees: homeChatCopy.employees
},
isStreaming: sendPhase === "streaming" || sendPhase === "finalizing",
// TODO: wire activeRoleId from the current active role in the ongoing AI collaboration flow
messages: {
messageListRef,
messages,
......
......@@ -11,6 +11,7 @@ import type {
import type { ChatAttachment, ChatLaunchState, ProjectIntentSuggestion } from "@qjclaw/shared-types"
import type { ExpertGuideContent } from "../experts/ExpertsView"
import { ExpertEmptyState } from "../experts/ExpertsView"
import { ExpertPanelLead } from "../experts/ExpertPanelLead"
import type { ExpertVisualKey } from "../shell/ExpertTree"
import { BindEntry } from "./BindEntry"
import { ChatComposer } from "./ChatComposer"
......@@ -174,6 +175,8 @@ interface ConversationWorkspaceViewProps {
messages: ConversationWorkspaceMessagesProps
composer: ConversationWorkspaceComposerProps
actions: ConversationWorkspaceActionsProps
activeRoleId?: string
isStreaming?: boolean
}
export function ConversationWorkspaceView({
......@@ -183,7 +186,9 @@ export function ConversationWorkspaceView({
emptyState,
messages,
composer,
actions
actions,
activeRoleId,
isStreaming
}: ConversationWorkspaceViewProps) {
const homeMicrocopyStatus = emptyState.selectedSkillIsDefault ? "默认工作区" : "已切换"
......@@ -202,22 +207,14 @@ export function ConversationWorkspaceView({
{homeMicrocopyStatus}
</span>
</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">
<span className={"expert-hero-icon expert-hero-icon-" + emptyState.activeExpertVisualKey} aria-hidden="true">
{actions.renderExpertIcon(emptyState.activeExpertVisualKey)}
</span>
<span className="expert-hero-copy">
<strong>{emptyState.activeExpertName}</strong>
</span>
</div>
<ExpertPanelLead
activeExpertKey={emptyState.activeExpertKey}
activeExpertName={emptyState.activeExpertName}
activeExpertVisualKey={emptyState.activeExpertVisualKey}
expertWorkspaceLogo={emptyState.expertWorkspaceLogo}
renderExpertIcon={actions.renderExpertIcon}
/>
)
const activeEmptyState = viewMode === "experts" ? (
......@@ -227,6 +224,8 @@ export function ConversationWorkspaceView({
activeExpertGuide={emptyState.activeExpertGuide}
starterQuestionsHint={emptyState.starterQuestionsHint}
onStarterPrompt={actions.onStarterPrompt}
activeRoleId={activeRoleId}
isStreaming={isStreaming}
/>
) : (
<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>
)
}
......@@ -2,6 +2,9 @@ import type { ReactNode, RefObject } from "react"
import type { ProjectRole } from "@qjclaw/shared-types"
import { Panel } from "../../components/ui/Panel"
import { ChatWorkspace } from "../chat/ChatWorkspace"
import { ExpertPanelLead } from "./ExpertPanelLead"
import { RoleTeamPanel } from "./RoleTeamPanel"
import { RoleCollaborationFlow } from "./RoleCollaborationFlow"
export type ExpertKey = "xiaohongshu" | "douyin" | "browser" | "general" | "planner" | "wechat" | "geo" | "zhihu"
......@@ -63,6 +66,8 @@ interface ExpertEmptyStateProps {
activeExpertGuide: ExpertGuideContent
starterQuestionsHint: string
onStarterPrompt: (prompt: string) => void
activeRoleId?: string
isStreaming?: boolean
}
export function ExpertsView({
......@@ -81,20 +86,14 @@ export function ExpertsView({
return (
<ChatWorkspace
panelLead={expertWorkspaceLogo ? (
<div className="expert-hero-heading">
{expertWorkspaceLogo}
<strong className="expert-hero-title">{activeExpertName}</strong>
</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>
panelLead={(
<ExpertPanelLead
activeExpertKey={activeExpertKey}
activeExpertName={activeExpertName}
activeExpertVisualKey={activeExpertVisualKey}
expertWorkspaceLogo={expertWorkspaceLogo}
renderExpertIcon={renderExpertIcon}
/>
)}
panelActions={panelActions}
workspaceRef={workspaceRef}
......@@ -111,29 +110,34 @@ export function ExpertEmptyState({
activeExpertKey,
activeExpertGuide,
starterQuestionsHint,
onStarterPrompt
onStarterPrompt,
activeRoleId,
isStreaming
}: ExpertEmptyStateProps) {
if (activeExpertKey === "xiaohongshu") {
const starterPrompts = activeExpertGuide.starterPrompts ?? activeExpertGuide.prompts.map((prompt) => ({
title: prompt,
description: activeExpertGuide.summary,
prompt
const starterPrompts = activeExpertGuide.starterPrompts ?? activeExpertGuide.prompts.map((item) => ({
title: item,
description: "",
prompt: item
}))
return (
<div className="empty-state expert-empty-state expert-empty-state-xiaohongshu">
<div className="xiaohongshu-empty-copy">
<strong className="xiaohongshu-empty-title">{activeExpertGuide.greeting}</strong>
<p>{activeExpertGuide.summary}</p>
{activeExpertGuide.summary ? <p>{activeExpertGuide.summary}</p> : null}
</div>
<div className="starter-prompt-list" aria-label="小红书任务入口">
{activeExpertGuide.roles && activeExpertGuide.roles.length > 0 ? (
<RoleTeamPanel roles={activeExpertGuide.roles} activeRoleId={activeRoleId} isStreaming={isStreaming} />
) : null}
<div className="starter-prompt-list">
{starterPrompts.map((item) => (
<button
key={item.prompt}
type="button"
className="starter-prompt"
title={item.prompt}
aria-label={item.prompt}
title={item.prompt}
onClick={() => onStarterPrompt(item.prompt)}
>
<span className="starter-prompt-title">{item.title}</span>
......@@ -224,6 +228,163 @@ export function ExpertEmptyState({
)
}
if (activeExpertKey === "planner") {
return (
<div className="empty-state expert-empty-state expert-empty-state-planner">
<div className="planner-empty-copy">
<strong className="planner-empty-title">{activeExpertGuide.greeting}</strong>
{activeExpertGuide.summary ? <p>{activeExpertGuide.summary}</p> : null}
</div>
{activeExpertGuide.intro ? <p className="planner-guide-intro">{activeExpertGuide.intro}</p> : null}
{activeExpertGuide.requirementChecklist?.length ? (
<div className="planner-brief-chip-list">
{activeExpertGuide.requirementChecklist.map((item) => (
<span key={item} className="planner-brief-chip">{item}</span>
))}
</div>
) : null}
<div className="starter-prompt-list">
{activeExpertGuide.prompts.map((item) => (
<button
key={item}
type="button"
className="starter-prompt planner-starter-prompt"
onClick={() => onStarterPrompt(item)}
>
<span className="starter-prompt-title">{item}</span>
</button>
))}
</div>
</div>
)
}
if (activeExpertKey === "wechat") {
return (
<div className="empty-state expert-empty-state expert-empty-state-wechat">
<div className="wechat-empty-copy">
<strong className="wechat-empty-title">{activeExpertGuide.greeting}</strong>
{activeExpertGuide.summary ? <p>{activeExpertGuide.summary}</p> : null}
</div>
{activeExpertGuide.intro ? <p className="wechat-guide-intro">{activeExpertGuide.intro}</p> : null}
{activeExpertGuide.requirementChecklist?.length ? (
<div className="wechat-brief-chip-list">
{activeExpertGuide.requirementChecklist.map((item) => (
<span key={item} className="wechat-brief-chip">{item}</span>
))}
</div>
) : null}
{activeExpertGuide.workflowSteps?.length ? (
<div className="wechat-capability-list">
{activeExpertGuide.workflowSteps.map((item, index) => (
<div key={item} className="wechat-capability-item">
<span className="wechat-capability-index">{index + 1}</span>
<span className="wechat-capability-copy">{item}</span>
</div>
))}
</div>
) : null}
<div className="starter-prompt-list">
{activeExpertGuide.prompts.map((item) => (
<button
key={item}
type="button"
className="starter-prompt wechat-starter-prompt"
onClick={() => onStarterPrompt(item)}
>
<span className="starter-prompt-title">{item}</span>
</button>
))}
</div>
</div>
)
}
if (activeExpertKey === "geo") {
return (
<div className="empty-state expert-empty-state expert-empty-state-geo">
<div className="geo-empty-copy">
<strong className="geo-empty-title">{activeExpertGuide.greeting}</strong>
{activeExpertGuide.summary ? <p>{activeExpertGuide.summary}</p> : null}
</div>
{activeExpertGuide.intro ? <p className="geo-guide-intro">{activeExpertGuide.intro}</p> : null}
{activeExpertGuide.requirementChecklist?.length ? (
<div className="geo-brief-chip-list">
{activeExpertGuide.requirementChecklist.map((item) => (
<span key={item} className="geo-brief-chip">{item}</span>
))}
</div>
) : null}
{activeExpertGuide.roles && activeExpertGuide.roles.length > 0 ? (
<RoleCollaborationFlow
roles={activeExpertGuide.roles}
handoffs={[
{ fromRoleId: "diagnostician", toRoleId: "strategist", label: "诊断报告" },
{ fromRoleId: "strategist", toRoleId: "content-creator", label: "策略方案" },
{ fromRoleId: "content-creator", toRoleId: "tech-advisor", label: "优化内容" },
]}
activeRoleId={activeRoleId}
isStreaming={isStreaming}
/>
) : null}
<div className="starter-prompt-list">
{activeExpertGuide.prompts.map((item) => (
<button
key={item}
type="button"
className="starter-prompt geo-starter-prompt"
onClick={() => onStarterPrompt(item)}
>
<span className="starter-prompt-title">{item}</span>
</button>
))}
</div>
</div>
)
}
if (activeExpertKey === "zhihu") {
return (
<div className="empty-state expert-empty-state expert-empty-state-zhihu">
<div className="zhihu-empty-copy">
<strong className="zhihu-empty-title">{activeExpertGuide.greeting}</strong>
{activeExpertGuide.summary ? <p>{activeExpertGuide.summary}</p> : null}
</div>
{activeExpertGuide.intro ? <p className="zhihu-guide-intro">{activeExpertGuide.intro}</p> : null}
{activeExpertGuide.requirementChecklist?.length ? (
<div className="zhihu-brief-chip-list">
{activeExpertGuide.requirementChecklist.map((item) => (
<span key={item} className="zhihu-brief-chip">{item}</span>
))}
</div>
) : null}
{activeExpertGuide.workflowSteps?.length ? (
<div className="zhihu-strategy-list">
<span className="zhihu-strategy-label">策略三要素</span>
{activeExpertGuide.workflowSteps.map((item, index) => (
<div key={item} className="zhihu-strategy-item">
<span className="zhihu-strategy-index">{index + 1}</span>
<span className="zhihu-strategy-copy">{item}</span>
</div>
))}
</div>
) : null}
<div className="starter-prompt-list">
{activeExpertGuide.prompts.map((item) => (
<button
key={item}
type="button"
className="starter-prompt zhihu-starter-prompt"
onClick={() => onStarterPrompt(item)}
>
<span className="starter-prompt-title">{item}</span>
</button>
))}
</div>
</div>
)
}
return (
<div className="empty-state expert-empty-state">
<span className="empty-state-kicker">{activeExpertName}</span>
......
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
if (/content-account|planner|账号规划|内容账号规划|内容创作/.test(seed)) {
return "planner"
}
if (/(^|[\s-])geo($|[\s-])/.test(seed)) {
if (/\bgeo\b/.test(seed)) {
return "geo"
}
if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) {
......@@ -105,7 +105,7 @@ export function resolveExpertVisualKey(project?: ExpertProject, definition?: Exp
if (/(^|[\s-])x($|[\s-])|twitter/.test(seed)) {
return "x"
}
if (/(^|[\s-])geo($|[\s-])/.test(seed)) {
if (/\bgeo\b/.test(seed)) {
return "geo"
}
if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) {
......@@ -176,8 +176,8 @@ function getExpertGuide(project: ExpertProject | undefined): ExpertGuideContent
requirementChecklist: ["选题方向", "文章类型", "目标读者", "核心信息", "期望效果"],
workflowSteps: ["选题规划与内容日历", "标题优化(提高打开率)", "文章结构梳理与排版建议", "涨粉策略与粉丝运营"],
prompts: [
"帮我策划一篇公众号科普长文,主题是XXX",
"分析我这个公众号的选题方向,给出优化建议",
"帮我策划一篇公众号科普长文,主题是职场效率工具,目标读者是职场新人。",
"分析生活类公众号的选题方向,给出优化建议",
"给我5个能涨粉的互动活动方案"
]
}
......@@ -202,12 +202,12 @@ function getExpertGuide(project: ExpertProject | undefined): ExpertGuideContent
case "zhihu":
return {
greeting: "在知乎,专业是最好的流量",
summary: "帮你选题、写回答、建人设,把专业积累变成影响力。",
summary: "",
intro: "知乎用户看重专业深度和真实经验。告诉我你的专业领域和人设目标,我来帮你策划内容策略。",
requirementChecklist: ["专业领域", "人设定位", "目标受众", "内容类型", "更新频率"],
workflowSteps: ["人设定位:你希望被记住的专业标签是什么?", "选题策略:追踪热点问题 vs 深耕垂直领域", "回答结构:开篇抓人 → 专业拆解 → 金句收尾"],
prompts: [
"帮我找5个XX领域的高关注问题,写深度回答",
"帮我找5个职场领域的高关注问题,写深度回答",
"给我设计一个知乎个人主页的专业定位方案",
"把这篇公众号文章改写成知乎回答风格"
]
......
......@@ -127,7 +127,15 @@ const mockExpertDefinitions: ExpertDefinition[] = [
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: "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: "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" },
......
......@@ -652,6 +652,70 @@
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 {
display: inline-flex;
align-items: center;
......
......@@ -1808,3 +1808,541 @@ button.secondary {
display: none;
}
}
/* ── Expert empty-state theme variables ──────────────────────────── */
.expert-empty-state-planner {
--expert-r: 168;
--expert-g: 85;
--expert-b: 247;
--expert-dark: #7c3aed;
--expert-light: #c084fc;
--expert-soft-bg: #f5f3ff;
--expert-title-gradient: linear-gradient(90deg, #7c3aed, #a855f7, #c084fc, #a855f7, #7c3aed);
--expert-prompt-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(245, 243, 255, 0.94));
--expert-prompt-bg-hover: linear-gradient(180deg, #ffffff, #f5f3ff);
}
.expert-empty-state-wechat {
--expert-r: 34;
--expert-g: 197;
--expert-b: 94;
--expert-dark: #16a34a;
--expert-light: #4ade80;
--expert-soft-bg: #f0fdf4;
--expert-title-gradient: linear-gradient(90deg, #16a34a, #22c55e, #4ade80, #22c55e, #16a34a);
--expert-prompt-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(240, 253, 244, 0.94));
--expert-prompt-bg-hover: linear-gradient(180deg, #ffffff, #f0fdf4);
}
.expert-empty-state-geo {
--expert-r: 59;
--expert-g: 130;
--expert-b: 246;
--expert-dark: #2563eb;
--expert-light: #60a5fa;
--expert-soft-bg: #eff6ff;
--expert-title-gradient: linear-gradient(90deg, #2563eb, #3b82f6, #60a5fa, #3b82f6, #2563eb);
--expert-prompt-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(239, 246, 255, 0.94));
--expert-prompt-bg-hover: linear-gradient(180deg, #ffffff, #eff6ff);
}
.expert-empty-state-zhihu {
--expert-r: 0;
--expert-g: 102;
--expert-b: 255;
--expert-dark: #0052cc;
--expert-light: #3388ff;
--expert-soft-bg: #eef2ff;
--expert-title-gradient: linear-gradient(90deg, #0052cc, #0066ff, #3388ff, #0066ff, #0052cc);
--expert-prompt-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(238, 242, 255, 0.94));
--expert-prompt-bg-hover: linear-gradient(180deg, #ffffff, #eef2ff);
}
/* Single shared title animation */
@keyframes expert-title-flow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* ── Shared root layout ────────────────────────────────────────── */
.expert-empty-state-planner,
.expert-empty-state-wechat,
.expert-empty-state-geo,
.expert-empty-state-zhihu {
max-width: 840px;
width: 100%;
margin: 0 auto;
display: grid;
gap: 24px;
border: 0;
}
/* ── Shared copy / intro / chip / starter-prompt rules ─────────── */
/* Title (one rule via variables, selectors grouped for specificity) */
.expert-empty-state-planner .planner-empty-title,
.expert-empty-state-wechat .wechat-empty-title,
.expert-empty-state-geo .geo-empty-title,
.expert-empty-state-zhihu .zhihu-empty-title {
background: var(--expert-title-gradient);
background-size: 240% 100%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 28px;
line-height: 1.25;
font-weight: 760;
animation: expert-title-flow 4s ease-in-out infinite;
}
/* Copy section */
.expert-empty-state-planner .planner-empty-copy,
.expert-empty-state-wechat .wechat-empty-copy,
.expert-empty-state-geo .geo-empty-copy,
.expert-empty-state-zhihu .zhihu-empty-copy {
display: grid;
justify-items: center;
gap: 10px;
max-width: 620px;
margin: 0 auto;
}
.expert-empty-state-planner .planner-empty-copy p,
.expert-empty-state-wechat .wechat-empty-copy p,
.expert-empty-state-geo .geo-empty-copy p,
.expert-empty-state-zhihu .zhihu-empty-copy p {
max-width: 520px;
margin: 0;
color: var(--revamp-text-muted);
font-size: 14px;
line-height: 1.8;
}
/* Guide intro */
.expert-empty-state-planner .planner-guide-intro,
.expert-empty-state-wechat .wechat-guide-intro,
.expert-empty-state-geo .geo-guide-intro,
.expert-empty-state-zhihu .zhihu-guide-intro {
max-width: 620px;
margin: 0 auto;
color: var(--revamp-text-muted);
font-size: 13px;
line-height: 1.7;
text-align: center;
}
/* Brief chip list */
.expert-empty-state-planner .planner-brief-chip-list,
.expert-empty-state-wechat .wechat-brief-chip-list,
.expert-empty-state-geo .geo-brief-chip-list,
.expert-empty-state-zhihu .zhihu-brief-chip-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
/* Brief chip (colour via variables) */
.expert-empty-state-planner .planner-brief-chip,
.expert-empty-state-wechat .wechat-brief-chip,
.expert-empty-state-geo .geo-brief-chip,
.expert-empty-state-zhihu .zhihu-brief-chip {
display: inline-flex;
align-items: center;
padding: 6px 16px;
border-radius: 20px;
border: 1px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.28);
background: rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.06);
color: var(--expert-dark);
font-size: 13px;
font-weight: 600;
}
/* Starter prompt list */
.expert-empty-state-planner .starter-prompt-list,
.expert-empty-state-wechat .starter-prompt-list,
.expert-empty-state-geo .starter-prompt-list,
.expert-empty-state-zhihu .starter-prompt-list {
width: 100%;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
/* Starter prompt card (colour via variables) */
.expert-empty-state-planner .planner-starter-prompt,
.expert-empty-state-wechat .wechat-starter-prompt,
.expert-empty-state-geo .geo-starter-prompt,
.expert-empty-state-zhihu .zhihu-starter-prompt {
min-height: 72px;
display: grid;
align-content: start;
gap: 8px;
padding: 16px;
border-radius: 16px;
border: 1px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.22);
background: var(--expert-prompt-bg);
color: #243044;
box-shadow: 0 8px 24px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.08);
text-align: left;
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.expert-empty-state-planner .planner-starter-prompt:hover,
.expert-empty-state-wechat .wechat-starter-prompt:hover,
.expert-empty-state-geo .geo-starter-prompt:hover,
.expert-empty-state-zhihu .zhihu-starter-prompt:hover {
transform: translateY(-2px);
border-color: rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.48);
background: var(--expert-prompt-bg-hover);
box-shadow: 0 14px 32px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.14);
}
.expert-empty-state-planner .planner-starter-prompt:focus-visible,
.expert-empty-state-wechat .wechat-starter-prompt:focus-visible,
.expert-empty-state-geo .geo-starter-prompt:focus-visible,
.expert-empty-state-zhihu .zhihu-starter-prompt:focus-visible {
outline: 3px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.28);
outline-offset: 3px;
}
/* ── Wechat capability list (layout unique to wechat) ──────────── */
.expert-empty-state-wechat .wechat-capability-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
max-width: 620px;
width: 100%;
margin: 0 auto;
}
.expert-empty-state-wechat .wechat-capability-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 12px;
border: 1px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.16);
background: rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.04);
}
.expert-empty-state-wechat .wechat-capability-index {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.14);
color: var(--expert-dark);
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.expert-empty-state-wechat .wechat-capability-copy {
font-size: 13px;
font-weight: 600;
color: #1f2937;
line-height: 1.4;
}
/* ── Role Collaboration Flow (GEO) ──────────────────────────── */
.role-collaboration-flow {
max-width: 720px;
width: 100%;
margin: 0 auto;
display: grid;
gap: 14px;
}
.collab-header {
display: flex;
align-items: center;
justify-content: center;
}
.collab-label {
font-size: 13px;
font-weight: 600;
color: var(--expert-dark);
letter-spacing: 0.02em;
}
.collab-pipeline {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 0;
}
.collab-role-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px 14px 16px;
border-radius: 14px;
border: 1px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.2);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.03));
color: #243044;
text-align: center;
min-width: 130px;
flex: 1;
max-width: 165px;
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
position: relative;
}
.collab-role-card:hover {
transform: translateY(-2px);
border-color: rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.5);
box-shadow: 0 12px 28px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.12);
}
.collab-role-card:focus-visible {
outline: 3px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.28);
outline-offset: 3px;
}
.collab-role-card.collab-role-active {
border-color: var(--expert-dark);
box-shadow: 0 0 0 3px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.15), 0 12px 28px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.18);
animation: collab-glow 2s ease-in-out infinite;
}
@keyframes collab-glow {
0%, 100% {
box-shadow: 0 0 0 3px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.15), 0 12px 28px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.18);
}
50% {
box-shadow: 0 0 0 6px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.06), 0 16px 32px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.22);
}
}
.collab-role-icon {
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, var(--expert-soft-bg), rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.15));
color: var(--expert-dark);
}
.collab-role-body {
display: grid;
gap: 4px;
}
.collab-role-name {
font-size: 14px;
font-weight: 650;
color: #1f2937;
line-height: 1.3;
}
.collab-role-desc {
font-size: 11px;
color: #667794;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.collab-role-dot {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5);
animation: collab-dot-pulse 2s ease-in-out infinite;
}
@keyframes collab-dot-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);
}
}
/* Connector between cards */
.collab-connector {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 56px;
flex-shrink: 0;
padding: 30px 10px 0 10px;
gap: 6px;
overflow: visible;
z-index: 1;
}
.collab-connector-line {
width: 100%;
height: 2px;
background: linear-gradient(90deg, rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.2), rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.6), rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.2));
position: relative;
overflow: visible;
}
.collab-connector-arrow {
position: absolute;
right: -8px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 8px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.65);
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
z-index: 2;
}
.collab-handoff-label {
font-size: 10px;
font-weight: 600;
color: var(--expert-dark);
white-space: nowrap;
opacity: 0.7;
letter-spacing: 0.02em;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
.expert-empty-state-planner .planner-empty-title,
.expert-empty-state-wechat .wechat-empty-title,
.expert-empty-state-geo .geo-empty-title,
.expert-empty-state-zhihu .zhihu-empty-title {
animation: none;
}
.collab-role-card.collab-role-active {
animation: none;
box-shadow: 0 0 0 3px rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.15);
}
.collab-role-dot {
animation: none;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.3);
}
}
/* Responsive: stack vertically on narrow screens */
@media (max-width: 640px) {
.collab-pipeline {
flex-direction: column;
align-items: center;
}
.collab-connector {
flex-direction: row;
min-width: 0;
padding: 6px 0;
}
.collab-connector-line {
width: 2px;
height: 22px;
background: linear-gradient(180deg, rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.2), rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.6), rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.2));
}
.collab-connector-arrow {
right: 50%;
top: auto;
bottom: -8px;
transform: translateX(50%);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 8px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.65);
border-bottom: 0;
}
.collab-handoff-label {
font-size: 10px;
}
}
/* ── Zhihu strategy list (layout unique to zhihu) ──────────────── */
.expert-empty-state-zhihu .zhihu-strategy-list {
display: grid;
gap: 10px;
max-width: 620px;
width: 100%;
margin: 0 auto;
}
.expert-empty-state-zhihu .zhihu-strategy-label {
text-align: center;
font-size: 13px;
font-weight: 600;
color: var(--expert-dark);
letter-spacing: 0.02em;
}
.expert-empty-state-zhihu .zhihu-strategy-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 18px;
border-radius: 14px;
border: 1px solid rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.16);
background: rgba(var(--expert-r), var(--expert-g), var(--expert-b), 0.04);
}
.expert-empty-state-zhihu .zhihu-strategy-index {
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, var(--expert-dark), rgba(var(--expert-r), var(--expert-g), var(--expert-b)));
color: #ffffff;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.expert-empty-state-zhihu .zhihu-strategy-copy {
font-size: 13px;
font-weight: 600;
color: #1f2937;
line-height: 1.5;
}
/* ── Responsive: Expert empty states ───────────────────────────── */
@media (max-width: 760px) {
.expert-empty-state-planner .starter-prompt-list,
.expert-empty-state-wechat .starter-prompt-list,
.expert-empty-state-geo .starter-prompt-list,
.expert-empty-state-zhihu .starter-prompt-list {
grid-template-columns: 1fr;
}
.expert-empty-state-wechat .wechat-capability-list {
grid-template-columns: 1fr;
}
.expert-empty-state-planner .planner-empty-title,
.expert-empty-state-wechat .wechat-empty-title,
.expert-empty-state-geo .geo-empty-title,
.expert-empty-state-zhihu .zhihu-empty-title {
font-size: 22px;
}
}
......@@ -801,8 +801,10 @@
}
.conversation-shell .message-list-xiaohongshu-empty:has(.expert-empty-state-xiaohongshu) {
align-content: center;
justify-items: center;
display: grid;
place-items: center;
place-content: center;
padding: 20px 0 28px;
}
.conversation-shell .home-empty-state {
......@@ -893,124 +895,284 @@
box-shadow: 0 0 0 5px rgba(147, 197, 253, 0.28);
}
.conversation-shell.conversation-shell-experts .expert-empty-state-xiaohongshu {
width: min(100%, 760px);
justify-self: center;
align-self: center;
justify-items: center;
.conversation-shell .expert-empty-state-xiaohongshu {
width: min(720px, 100%);
margin: auto;
min-height: auto;
align-content: center;
gap: 24px;
padding: 16px 4px;
justify-items: center;
gap: 28px;
padding: 0 12px;
border: 0;
background: transparent;
box-shadow: none;
text-align: center;
}
.conversation-shell .expert-empty-state-douyin {
border: 0;
}
.conversation-shell .expert-empty-state-xiaohongshu .empty-state-kicker {
color: #e44f7a;
font-weight: 700;
}
.conversation-shell .xiaohongshu-empty-copy {
display: grid;
justify-items: center;
gap: 9px;
width: min(100%, 620px);
text-align: center;
gap: 10px;
max-width: 620px;
}
.conversation-shell .xiaohongshu-empty-title {
color: #be123c;
font-size: 24px;
line-height: 1.28;
letter-spacing: 0;
background: linear-gradient(92deg, #e11d48 0%, #fb7185 30%, #60a5fa 72%, #e11d48 100%);
background-size: 220% 100%;
-webkit-background-clip: text;
color: #e44f7a;
background: linear-gradient(90deg, #e11d48, #fb7185, #be123c, #f43f5e, #e11d48);
background-size: 240% 100%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 10px 24px rgba(225, 29, 72, 0.08);
font-size: 28px;
line-height: 1.25;
font-weight: 760;
animation: xiaohongshu-title-flow 4s ease-in-out infinite;
}
.conversation-shell .xiaohongshu-empty-copy p {
width: min(100%, 580px);
color: #64748b;
font-size: 13px;
line-height: 1.7;
@keyframes xiaohongshu-title-flow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.conversation-shell .expert-empty-state-xiaohongshu .xiaohongshu-empty-copy p {
max-width: 520px;
margin: 0;
color: var(--revamp-text-muted);
font-size: 14px;
line-height: 1.8;
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt-list {
width: min(100%, 680px);
width: 100%;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt {
min-height: 92px;
position: relative;
min-height: 90px;
display: grid;
align-content: start;
gap: 6px;
padding: 15px 16px 14px;
gap: 8px;
padding: 18px;
border-radius: 18px;
border: 1px solid rgba(244, 166, 185, 0.5);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 246, 249, 0.96));
color: #243044;
box-shadow: 0 16px 36px rgba(225, 82, 126, 0.1);
text-align: left;
transition:
transform 0.18s ease,
border-color 0.18s ease,
box-shadow 0.18s ease,
background 0.18s ease;
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt:hover {
transform: translateY(-2px);
border-color: rgba(229, 80, 121, 0.64);
background: linear-gradient(180deg, #ffffff, #fff1f5);
box-shadow: 0 20px 42px rgba(225, 82, 126, 0.16);
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt:focus-visible {
outline: 3px solid rgba(229, 80, 121, 0.28);
outline-offset: 3px;
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt-title {
color: #9f1239;
}
/* ── Role Team Panel ───────────────────────────────────────────── */
.conversation-shell .role-team-panel {
width: 100%;
max-width: 840px;
display: grid;
gap: 14px;
}
.conversation-shell .role-team-header {
display: flex;
align-items: center;
gap: 10px;
}
.conversation-shell .role-team-label {
font-size: 13px;
font-weight: 600;
color: #9f1239;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.conversation-shell .role-team-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
}
.conversation-shell .role-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 10px 14px;
border-radius: 16px;
border: 1px solid rgba(244, 166, 185, 0.4);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 246, 249, 0.9));
color: #243044;
text-align: center;
cursor: pointer;
transition:
transform 0.18s ease,
border-color 0.18s ease,
box-shadow 0.18s ease,
background 0.18s ease;
position: relative;
}
.conversation-shell .role-card:hover {
transform: translateY(-2px);
border-color: rgba(229, 80, 121, 0.6);
background: linear-gradient(180deg, #ffffff, #fff1f5);
box-shadow: 0 12px 28px rgba(225, 82, 126, 0.14);
}
.conversation-shell .role-card:focus-visible {
outline: 3px solid rgba(229, 80, 121, 0.28);
outline-offset: 3px;
}
.conversation-shell .role-card-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
border-color: rgba(251, 113, 133, 0.42);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.97), rgba(255, 247, 250, 0.9));
color: var(--revamp-text);
background: linear-gradient(135deg, rgba(255, 245, 245, 0.96), rgba(255, 228, 230, 0.9));
color: #e11d48;
}
.conversation-shell .role-card-body {
display: grid;
gap: 3px;
}
.conversation-shell .role-card-name {
font-size: 14px;
font-weight: 650;
color: #1f2937;
line-height: 1.3;
}
.conversation-shell .role-card-desc {
font-size: 11px;
color: #667794;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
cursor: pointer;
box-shadow:
var(--revamp-shadow-soft),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
transition: border-color 140ms ease, background 140ms ease, box-shadow 140ms ease;
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt::before {
content: "";
/* Status dot — base positioning, state classes control color */
.conversation-shell .role-status-dot {
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: linear-gradient(180deg, rgba(244, 63, 94, 0.84), rgba(96, 165, 250, 0.36));
opacity: 0.72;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
border-radius: 50%;
transition: background 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease;
}
/* Status dot — idle (grey, static) */
.conversation-shell .role-status-dot.idle {
background: #94a3b8;
opacity: 0.5;
box-shadow: none;
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt::after {
content: none;
/* Status dot — active (green, pulsing) */
.conversation-shell .role-status-dot.active {
background: #22c55e;
opacity: 1;
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5);
animation: role-pulse 2s ease-in-out infinite;
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt:hover {
border-color: rgba(244, 63, 94, 0.68);
background: linear-gradient(180deg, #ffffff, rgba(255, 247, 250, 0.96));
box-shadow:
var(--revamp-shadow),
inset 0 1px 0 rgba(255, 255, 255, 0.92);
@keyframes role-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);
}
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt:focus-visible {
outline: 2px solid rgba(225, 29, 72, 0.56);
outline-offset: 3px;
box-shadow: 0 0 0 5px rgba(251, 113, 133, 0.18);
@media (prefers-reduced-motion: reduce) {
.conversation-shell .xiaohongshu-empty-title {
animation: none;
}
.conversation-shell .role-status-dot.active {
animation: none;
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.3);
}
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt-title {
color: #881337;
/* Responsive: 3+2 grid on tablet */
@media (max-width: 900px) {
.conversation-shell .role-team-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt-desc {
color: #64748b;
}
/* Responsive: 2 columns on small tablet */
@media (max-width: 640px) {
.conversation-shell .role-team-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.conversation-shell .starter-prompt-title,
.conversation-shell .starter-prompt-desc {
display: block;
position: relative;
min-width: 0;
.conversation-shell .role-card {
padding: 12px 8px 10px;
}
}
.conversation-shell .starter-prompt-title {
color: #172554;
display: block;
color: #1f2937;
font-size: 14px;
line-height: 1.45;
font-weight: 700;
}
.conversation-shell .starter-prompt-desc {
color: #64748b;
display: block;
color: #6b7280;
font-size: 12px;
line-height: 1.56;
line-height: 1.65;
}
@keyframes home-title-flow {
......@@ -1172,12 +1334,15 @@
width: min(100%, 620px);
}
.conversation-shell .home-empty-state .starter-prompt-list,
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt-list {
.conversation-shell .home-empty-state .starter-prompt-list {
grid-template-columns: 1fr;
width: min(100%, 520px);
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt-list {
grid-template-columns: 1fr;
}
.conversation-shell .home-showcase-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
......@@ -1207,18 +1372,17 @@
padding: 12px 14px;
}
.conversation-shell.conversation-shell-experts .expert-empty-state-xiaohongshu {
.conversation-shell .expert-empty-state-xiaohongshu {
gap: 20px;
padding-block: 10px;
padding: 0;
}
.conversation-shell .xiaohongshu-empty-title {
font-size: 22px;
}
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt {
min-height: 84px;
padding: 12px 14px;
.conversation-shell .expert-empty-state-xiaohongshu .starter-prompt-list {
grid-template-columns: 1fr;
}
.conversation-shell .home-showcase-grid {
......
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