Files
Genarrative/src/components/creative-agent/CreativeAgentHome.tsx
2026-05-08 11:44:42 +08:00

424 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
Bell,
Bookmark,
ChevronRight,
Gamepad2,
Menu,
MessageCircle,
Moon,
Music,
PanelLeftClose,
Settings,
Sparkles,
UserRound,
} from 'lucide-react';
import { useMemo, useState } from 'react';
import type { CreativeAgentInputPart } from '../../../packages/shared/src/contracts/creativeAgent';
import type { CreationWorkShelfItem } from '../custom-world-home/creationWorkShelf';
import { RpgEntryBrandLogo } from '../rpg-entry/RpgEntryBrandLogo';
import { CreativeAgentInputComposer } from './CreativeAgentInputComposer';
import { createCreativeAgentClientMessageId } from './creativeAgentViewModel';
type CreativeAgentHomePrompt = {
id: string;
label: string;
prompt: string;
icon: typeof Sparkles;
tone: 'cool' | 'green' | 'warm' | 'purple' | 'rose';
badge?: string;
};
export type CreativeAgentHistoryItem = {
id: string;
title: string;
groupLabel: string;
source: CreationWorkShelfItem;
};
type CreativeAgentHomeProps = {
recentItems: CreativeAgentHistoryItem[];
isBusy: boolean;
error: string | null;
onStartNewChat: () => void;
onOpenHistoryItem: (item: CreationWorkShelfItem) => void;
onOpenDrafts: () => void;
onOpenAccount: () => void;
onOpenSettings: () => void;
onSubmitMessage: (payload: {
clientMessageId: string;
content: CreativeAgentInputPart[];
}) => void;
};
const PROMPT_SUGGESTIONS: CreativeAgentHomePrompt[] = [
{
id: 'identity',
label: '你是谁',
prompt: '介绍一下你能帮我创作什么。',
icon: Sparkles,
tone: 'cool',
},
{
id: 'flash-app',
label: '一句话生成闪应用',
prompt: '帮我把一个灵感做成可互动的小应用。',
icon: Moon,
tone: 'green',
},
{
id: 'mini-game',
label: '捏个小游戏',
prompt: '帮我做一个适合马上玩的创意小游戏。',
icon: Gamepad2,
tone: 'warm',
},
{
id: 'world-model',
label: '体验世界模型',
prompt: '用一个世界设定帮我生成可体验的互动内容。',
icon: Bookmark,
tone: 'purple',
badge: 'Beta',
},
{
id: 'music',
label: '音乐扭蛋',
prompt: '把一段音乐灵感做成互动拼图。',
icon: Music,
tone: 'rose',
},
];
function buildCreativeHomeInputParts(payload: {
text: string;
image: { imageUrl: string; thumbnailUrl: string } | null;
}): CreativeAgentInputPart[] {
const content: CreativeAgentInputPart[] = [];
if (payload.text) {
content.push({
type: 'input_text',
text: payload.text,
});
}
if (payload.image) {
content.push({
type: 'input_image',
imageUrl: payload.image.imageUrl,
thumbnailUrl: payload.image.thumbnailUrl,
assetId: null,
});
}
return content;
}
function groupRecentItemsByLabel(items: CreativeAgentHistoryItem[]) {
const groups: Array<{ label: string; items: CreativeAgentHistoryItem[] }> = [];
for (const item of items) {
const lastGroup = groups[groups.length - 1];
if (lastGroup?.label === item.groupLabel) {
lastGroup.items.push(item);
continue;
}
groups.push({
label: item.groupLabel,
items: [item],
});
}
return groups;
}
function CreativeAgentPromptButton({
item,
disabled,
onClick,
}: {
item: CreativeAgentHomePrompt;
disabled: boolean;
onClick: () => void;
}) {
const Icon = item.icon;
return (
<button
type="button"
disabled={disabled}
onClick={onClick}
className={`creative-agent-home__prompt creative-agent-home__prompt--${item.tone}`}
>
<Icon className="h-5 w-5 shrink-0" />
<span className="truncate">{item.label}</span>
{item.badge ? (
<span className="creative-agent-home__prompt-badge">{item.badge}</span>
) : null}
</button>
);
}
function CreativeAgentDrawer({
open,
recentItems,
onClose,
onStartNewChat,
onOpenHistoryItem,
onOpenDrafts,
onOpenAccount,
onOpenSettings,
}: {
open: boolean;
recentItems: CreativeAgentHistoryItem[];
onClose: () => void;
onStartNewChat: () => void;
onOpenHistoryItem: (item: CreationWorkShelfItem) => void;
onOpenDrafts: () => void;
onOpenAccount: () => void;
onOpenSettings: () => void;
}) {
const groupedItems = useMemo(
() => groupRecentItemsByLabel(recentItems),
[recentItems],
);
return (
<>
<div
className={`creative-agent-drawer-backdrop ${open ? 'creative-agent-drawer-backdrop--open' : ''}`}
onClick={onClose}
/>
<aside
className={`creative-agent-drawer ${open ? 'creative-agent-drawer--open' : ''}`}
aria-hidden={!open}
>
<div className="flex h-full min-h-0 flex-col">
<header className="flex shrink-0 items-center justify-between gap-3 px-5 pb-5 pt-[max(1.1rem,env(safe-area-inset-top))]">
<RpgEntryBrandLogo decorative />
<button
type="button"
onClick={onClose}
className="platform-icon-button"
aria-label="关闭侧边栏"
title="关闭"
>
<PanelLeftClose className="h-4 w-4" />
</button>
</header>
<div className="shrink-0 space-y-3 px-5">
<button
type="button"
onClick={() => {
onStartNewChat();
onClose();
}}
className="creative-agent-drawer__new-chat"
>
<MessageCircle className="h-5 w-5" />
<span></span>
</button>
<button
type="button"
onClick={() => {
onOpenDrafts();
onClose();
}}
className="creative-agent-drawer__nav-row"
>
<Bookmark className="h-5 w-5" />
<span></span>
<ChevronRight className="ml-auto h-4 w-4 opacity-55" />
</button>
</div>
<div className="mt-5 min-h-0 flex-1 overflow-y-auto px-5 pb-5">
{groupedItems.length > 0 ? (
groupedItems.map((group) => (
<section key={group.label} className="mb-6">
<div className="creative-agent-drawer__group-label">
{group.label}
</div>
<div className="mt-3 space-y-3">
{group.items.map((item) => (
<button
key={item.id}
type="button"
onClick={() => {
onOpenHistoryItem(item.source);
onClose();
}}
className="creative-agent-drawer__history-item"
>
{item.title}
</button>
))}
</div>
</section>
))
) : (
<div className="creative-agent-drawer__empty"></div>
)}
</div>
<footer className="flex shrink-0 items-center justify-between gap-3 px-5 py-5 pb-[max(1.15rem,env(safe-area-inset-bottom))]">
<button
type="button"
onClick={() => {
onOpenAccount();
onClose();
}}
className="creative-agent-drawer__avatar"
aria-label="账号"
>
<UserRound className="h-5 w-5" />
</button>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => {
onOpenSettings();
onClose();
}}
className="platform-icon-button"
aria-label="外观"
title="外观"
>
<Moon className="h-4 w-4" />
</button>
<button
type="button"
onClick={() => {
onOpenSettings();
onClose();
}}
className="platform-icon-button"
aria-label="设置"
title="设置"
>
<Settings className="h-4 w-4" />
</button>
</div>
</footer>
</div>
</aside>
</>
);
}
export function CreativeAgentHome({
recentItems,
isBusy,
error,
onStartNewChat,
onOpenHistoryItem,
onOpenDrafts,
onOpenAccount,
onOpenSettings,
onSubmitMessage,
}: CreativeAgentHomeProps) {
const [drawerOpen, setDrawerOpen] = useState(false);
const submitText = (text: string) => {
const trimmedText = text.trim();
if (!trimmedText || isBusy) {
return;
}
onSubmitMessage({
clientMessageId: createCreativeAgentClientMessageId(),
content: [
{
type: 'input_text',
text: trimmedText,
},
],
});
};
return (
<div className="creative-agent-home platform-remap-surface">
<div className="creative-agent-home__backdrop" />
<header className="creative-agent-home__topbar">
<button
type="button"
onClick={() => setDrawerOpen(true)}
className="creative-agent-home__topbar-button"
aria-label="打开侧边栏"
title="菜单"
>
<Menu className="h-6 w-6" />
</button>
<RpgEntryBrandLogo className="creative-agent-home__brand" decorative />
<button
type="button"
onClick={onOpenAccount}
className="creative-agent-home__topbar-button"
aria-label="通知与账户"
title="通知与账户"
>
<Bell className="h-5 w-5" />
</button>
</header>
<main className="creative-agent-home__main">
<div className="creative-agent-home__hero">
<h1>Hi, </h1>
<p></p>
</div>
<div className="creative-agent-home__prompt-grid">
{PROMPT_SUGGESTIONS.map((item) => (
<CreativeAgentPromptButton
key={item.id}
item={item}
disabled={isBusy}
onClick={() => submitText(item.prompt)}
/>
))}
<button
type="button"
className="creative-agent-home__reward"
disabled={isBusy}
onClick={() => submitText('帮我做一个能马上分享的创意拼图。')}
>
<Sparkles className="h-6 w-6" />
<span> 1亿</span>
</button>
</div>
{error ? (
<div className="creative-agent-home__error">{error}</div>
) : null}
</main>
<div className="creative-agent-home__composer">
<CreativeAgentInputComposer
variant="floating"
isBusy={isBusy}
placeholder="问一问百梦"
onSubmit={(payload) => {
const content = buildCreativeHomeInputParts(payload);
if (content.length === 0) {
return;
}
onSubmitMessage({
clientMessageId: createCreativeAgentClientMessageId(),
content,
});
}}
/>
</div>
<CreativeAgentDrawer
open={drawerOpen}
recentItems={recentItems}
onClose={() => setDrawerOpen(false)}
onStartNewChat={onStartNewChat}
onOpenHistoryItem={onOpenHistoryItem}
onOpenDrafts={onOpenDrafts}
onOpenAccount={onOpenAccount}
onOpenSettings={onOpenSettings}
/>
</div>
);
}
export default CreativeAgentHome;