1
This commit is contained in:
112
src/components/creation-agent/CreationAgentWorkspace.test.tsx
Normal file
112
src/components/creation-agent/CreationAgentWorkspace.test.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { type CreationAgentTheme,CreationAgentWorkspace } from './CreationAgentWorkspace';
|
||||
|
||||
const testTheme: CreationAgentTheme = {
|
||||
accentTextClass: 'text-emerald-100',
|
||||
accentBgClass: 'bg-emerald-300',
|
||||
accentButtonClass: 'bg-emerald-200',
|
||||
userBubbleClass: 'bg-emerald-600 text-white',
|
||||
heroClass: 'border border-emerald-100/20 bg-slate-900',
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('creation agent workspace filters duplicate recommended replies', () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: '统一共创',
|
||||
currentTurn: 2,
|
||||
progressPercent: 40,
|
||||
anchors: [],
|
||||
messages: [
|
||||
{
|
||||
id: '',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '先把方向收一下。',
|
||||
},
|
||||
],
|
||||
recommendedReplies: [
|
||||
'',
|
||||
'继续补充冲突',
|
||||
'继续补充冲突',
|
||||
' 先确定玩家身份 ',
|
||||
],
|
||||
}}
|
||||
theme={testTheme}
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '继续补充冲突' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '先确定玩家身份' })).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: /^\s*$/u })).toBeNull();
|
||||
|
||||
const duplicateKeyCalls = consoleErrorSpy.mock.calls.filter((call) =>
|
||||
call.some(
|
||||
(arg) =>
|
||||
typeof arg === 'string' &&
|
||||
arg.includes('Encountered two children with the same key'),
|
||||
),
|
||||
);
|
||||
|
||||
expect(duplicateKeyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('creation agent workspace renders streaming assistant text', () => {
|
||||
if (!Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = () => {};
|
||||
}
|
||||
|
||||
render(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: '统一共创',
|
||||
currentTurn: 1,
|
||||
progressPercent: 20,
|
||||
anchors: [],
|
||||
messages: [
|
||||
{
|
||||
id: 'message-1',
|
||||
role: 'user',
|
||||
kind: 'chat',
|
||||
text: '我想做一个潮湿压抑的海上世界。',
|
||||
},
|
||||
],
|
||||
}}
|
||||
theme={testTheme}
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
streamingReplyText="那我先顺着这个方向收一下,开场时你更想让玩家撞上什么麻烦"
|
||||
isStreamingReply
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/那我先顺着这个方向收一下/u)).toBeTruthy();
|
||||
});
|
||||
493
src/components/creation-agent/CreationAgentWorkspace.tsx
Normal file
493
src/components/creation-agent/CreationAgentWorkspace.tsx
Normal file
@@ -0,0 +1,493 @@
|
||||
import { ArrowLeft, Send, Sparkles } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
type CreationAgentProgressCopy,
|
||||
normalizeCreationAgentProgress,
|
||||
resolveCreationAgentProgressHint,
|
||||
resolveCreationAnchorStatusLabel,
|
||||
} from '../../services/creation-agent';
|
||||
|
||||
export type CreationAgentAnchorView = {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreationAgentMessageView = {
|
||||
id: string;
|
||||
role: string;
|
||||
kind?: string;
|
||||
text: string;
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
export type CreationAgentOperationView = {
|
||||
operationId?: string;
|
||||
type?: string;
|
||||
status: string;
|
||||
phaseLabel: string;
|
||||
phaseDetail?: string;
|
||||
progress: number;
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export type CreationAgentSessionView = {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
assistantSummary?: string | null;
|
||||
currentTurn: number;
|
||||
progressPercent: number;
|
||||
anchors: CreationAgentAnchorView[];
|
||||
messages: CreationAgentMessageView[];
|
||||
recommendedReplies?: string[];
|
||||
};
|
||||
|
||||
export type CreationAgentTheme = {
|
||||
accentTextClass: string;
|
||||
accentBgClass: string;
|
||||
accentButtonClass: string;
|
||||
userBubbleClass: string;
|
||||
heroClass: string;
|
||||
anchorGridClass?: string;
|
||||
};
|
||||
|
||||
export type CreationAgentQuickAction = {
|
||||
key: string;
|
||||
label: string;
|
||||
minTurn?: number;
|
||||
minProgress?: number;
|
||||
showWhenComplete?: boolean;
|
||||
};
|
||||
|
||||
type CreationAgentWorkspaceProps = {
|
||||
session: CreationAgentSessionView | null;
|
||||
theme: CreationAgentTheme;
|
||||
loadingText: string;
|
||||
composerPlaceholder: string;
|
||||
primaryActionLabel: string;
|
||||
progressCopy?: CreationAgentProgressCopy;
|
||||
activeOperation?: CreationAgentOperationView | null;
|
||||
streamingReplyText?: string;
|
||||
isStreamingReply?: boolean;
|
||||
isBusy?: boolean;
|
||||
error?: string | null;
|
||||
quickActions?: CreationAgentQuickAction[];
|
||||
onBack: () => void;
|
||||
onSubmitText: (text: string, quickActionKey?: string) => void;
|
||||
onPrimaryAction: () => void;
|
||||
onQuickAction?: (action: CreationAgentQuickAction) => void;
|
||||
};
|
||||
|
||||
function uniqueRecommendedReplies(recommendedReplies: string[] = []) {
|
||||
return [...new Set(recommendedReplies.map((reply) => reply.trim()).filter(Boolean))].slice(
|
||||
0,
|
||||
3,
|
||||
);
|
||||
}
|
||||
|
||||
function CreationAgentOperationBanner({
|
||||
operation,
|
||||
}: {
|
||||
operation: CreationAgentOperationView | null | undefined;
|
||||
}) {
|
||||
const [visibleOperation, setVisibleOperation] =
|
||||
useState<CreationAgentOperationView | null>(operation ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibleOperation(operation ?? null);
|
||||
|
||||
if (operation?.status !== 'completed') {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
setVisibleOperation((current) =>
|
||||
current?.operationId === operation.operationId ? null : current,
|
||||
);
|
||||
}, 1200);
|
||||
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [operation]);
|
||||
|
||||
if (!visibleOperation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isFailed = visibleOperation.status === 'failed';
|
||||
const isRunning =
|
||||
visibleOperation.status === 'running' ||
|
||||
visibleOperation.status === 'queued';
|
||||
const bannerToneClass = isFailed
|
||||
? 'platform-banner--danger'
|
||||
: isRunning
|
||||
? 'platform-banner--info'
|
||||
: 'platform-banner--success';
|
||||
const progress = normalizeCreationAgentProgress(visibleOperation.progress);
|
||||
const progressFillStyle = isFailed
|
||||
? { background: 'linear-gradient(90deg, #fb7185 0%, #f43f5e 100%)' }
|
||||
: isRunning
|
||||
? { background: 'var(--platform-button-primary-fill)' }
|
||||
: { background: 'linear-gradient(90deg, #86efac 0%, #34d399 100%)' };
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`platform-remap-surface platform-banner rounded-[1.4rem] px-4 py-4 ${bannerToneClass}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-semibold">
|
||||
{visibleOperation.phaseLabel}
|
||||
</div>
|
||||
<div className="text-xs opacity-80">{progress}%</div>
|
||||
</div>
|
||||
{visibleOperation.phaseDetail ? (
|
||||
<div className="mt-1 text-xs opacity-80">
|
||||
{visibleOperation.phaseDetail}
|
||||
</div>
|
||||
) : null}
|
||||
{visibleOperation.error ? (
|
||||
<div className="mt-2 text-sm opacity-90">{visibleOperation.error}</div>
|
||||
) : null}
|
||||
<div className="platform-progress-track mt-3 h-2 overflow-hidden rounded-full">
|
||||
<div
|
||||
className="h-full rounded-full transition-[width] duration-300"
|
||||
style={{
|
||||
width: `${Math.max(8, progress)}%`,
|
||||
...progressFillStyle,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreationAgentMessageBubble({
|
||||
message,
|
||||
theme,
|
||||
recommendedReplies,
|
||||
onRecommendedReply,
|
||||
}: {
|
||||
message: CreationAgentMessageView;
|
||||
theme: CreationAgentTheme;
|
||||
recommendedReplies?: string[];
|
||||
onRecommendedReply: (text: string) => void;
|
||||
}) {
|
||||
const isUser = message.role === 'user';
|
||||
const isSystem = message.role === 'system';
|
||||
const visibleRecommendedReplies = isUser
|
||||
? []
|
||||
: uniqueRecommendedReplies(recommendedReplies);
|
||||
const bubbleToneClass = isUser
|
||||
? theme.userBubbleClass
|
||||
: isSystem
|
||||
? 'border border-[var(--platform-warm-border)] bg-[var(--platform-warm-bg)] text-[var(--platform-warm-text)]'
|
||||
: 'platform-subpanel text-[var(--platform-text-strong)]';
|
||||
|
||||
return (
|
||||
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||
<div
|
||||
className={`max-w-[90%] rounded-[1.4rem] px-4 py-3 text-sm leading-7 break-words sm:max-w-[82%] ${bubbleToneClass}`}
|
||||
>
|
||||
<div className="whitespace-pre-wrap">{message.text}</div>
|
||||
{visibleRecommendedReplies.length > 0 ? (
|
||||
<div className="mt-2.5 flex flex-col gap-1.5">
|
||||
{visibleRecommendedReplies.map((reply, replyIndex) => (
|
||||
<button
|
||||
key={`recommended-reply-${replyIndex}-${reply}`}
|
||||
type="button"
|
||||
onClick={() => onRecommendedReply(reply)}
|
||||
className="platform-button platform-button--ghost min-h-0 justify-start rounded-[0.95rem] px-2.5 py-1.5 text-left text-[11px] leading-4.5 whitespace-normal"
|
||||
>
|
||||
{reply}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreationAgentAnchorChip({
|
||||
anchor,
|
||||
theme,
|
||||
}: {
|
||||
anchor: CreationAgentAnchorView;
|
||||
theme: CreationAgentTheme;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-[1.25rem] border border-white/14 bg-white/8 px-3 py-3 text-left">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span
|
||||
className={`text-xs font-semibold tracking-[0.18em] ${theme.accentTextClass}`}
|
||||
>
|
||||
{anchor.label}
|
||||
</span>
|
||||
<span className="rounded-full bg-white/12 px-2 py-1 text-[0.68rem] text-white/70">
|
||||
{resolveCreationAnchorStatusLabel(anchor.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 line-clamp-2 text-sm leading-5 text-white/86">
|
||||
{anchor.value || '等待补齐'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function shouldShowQuickAction(
|
||||
action: CreationAgentQuickAction,
|
||||
session: CreationAgentSessionView,
|
||||
progress: number,
|
||||
) {
|
||||
if (action.showWhenComplete && progress < 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!action.showWhenComplete && progress >= 100 && action.minProgress !== 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof action.minTurn === 'number' && session.currentTurn < action.minTurn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof action.minProgress === 'number' && progress < action.minProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function CreationAgentWorkspace({
|
||||
session,
|
||||
theme,
|
||||
loadingText,
|
||||
composerPlaceholder,
|
||||
primaryActionLabel,
|
||||
progressCopy,
|
||||
activeOperation = null,
|
||||
streamingReplyText = '',
|
||||
isStreamingReply = false,
|
||||
isBusy = false,
|
||||
error = null,
|
||||
quickActions = [],
|
||||
onBack,
|
||||
onSubmitText,
|
||||
onPrimaryAction,
|
||||
onQuickAction,
|
||||
}: CreationAgentWorkspaceProps) {
|
||||
const [draftText, setDraftText] = useState('');
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
});
|
||||
}, [session?.messages, streamingReplyText, isStreamingReply]);
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
|
||||
{error || loadingText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const progress = normalizeCreationAgentProgress(session.progressPercent);
|
||||
const visibleQuickActions = quickActions.filter((action) =>
|
||||
shouldShowQuickAction(action, session, progress),
|
||||
);
|
||||
const lastAssistantMessageIndex = session.messages.reduce(
|
||||
(lastIndex, message, index) =>
|
||||
message.role === 'assistant' ? index : lastIndex,
|
||||
-1,
|
||||
);
|
||||
|
||||
const submit = () => {
|
||||
const text = draftText.trim();
|
||||
if (!text || isBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmitText(text);
|
||||
setDraftText('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-6xl flex-col gap-3 overflow-hidden px-1 sm:px-0">
|
||||
<div className={`relative overflow-hidden rounded-[1.8rem] px-4 py-4 text-white shadow-[0_20px_60px_rgba(15,23,42,0.18)] sm:px-5 ${theme.heroClass}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="返回"
|
||||
onClick={onBack}
|
||||
disabled={isBusy}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/16 bg-white/10 text-white/84 disabled:opacity-45"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={onPrimaryAction}
|
||||
className={`inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-bold text-slate-950 shadow-lg disabled:cursor-not-allowed disabled:opacity-50 ${theme.accentButtonClass}`}
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{primaryActionLabel}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="text-2xl font-black leading-tight sm:text-3xl">
|
||||
{session.title}
|
||||
</div>
|
||||
{session.assistantSummary ? (
|
||||
<div className="mt-2 max-w-2xl text-sm leading-6 text-white/76">
|
||||
{session.assistantSummary}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<span className="text-xs font-semibold tracking-[0.14em] text-white/72">
|
||||
创作进度
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-white/88">
|
||||
{progress}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-white/12">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${theme.accentBgClass}`}
|
||||
style={{ width: `${Math.max(6, progress)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-xs leading-5 text-white/64">
|
||||
{resolveCreationAgentProgressHint(progress, progressCopy)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{visibleQuickActions.length > 0 ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{visibleQuickActions.map((action) => (
|
||||
<button
|
||||
key={action.key}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => onQuickAction?.(action)}
|
||||
className="rounded-full border border-white/14 bg-white/10 px-3 py-1.5 text-xs font-semibold text-white/78 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{session.anchors.length > 0 ? (
|
||||
<div className={theme.anchorGridClass || 'grid gap-2 sm:grid-cols-2 xl:grid-cols-4'}>
|
||||
{session.anchors.map((anchor) => (
|
||||
<CreationAgentAnchorChip
|
||||
key={anchor.key}
|
||||
anchor={anchor}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CreationAgentOperationBanner operation={activeOperation} />
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-hidden rounded-[1.6rem] border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)]">
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-4">
|
||||
{session.messages.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-[var(--platform-text-soft)]">
|
||||
暂无消息
|
||||
</div>
|
||||
) : (
|
||||
session.messages.map((message, index) => (
|
||||
<CreationAgentMessageBubble
|
||||
key={message.id || `message-${index}`}
|
||||
message={message}
|
||||
theme={theme}
|
||||
recommendedReplies={
|
||||
index === lastAssistantMessageIndex
|
||||
? session.recommendedReplies
|
||||
: []
|
||||
}
|
||||
onRecommendedReply={(text) => onSubmitText(text)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{isStreamingReply ? (
|
||||
<div className="flex justify-start">
|
||||
<div className="platform-subpanel max-w-[90%] rounded-[1.4rem] px-4 py-3 text-sm leading-7 text-[var(--platform-text-strong)] sm:max-w-[82%]">
|
||||
{streamingReplyText ? (
|
||||
<div className="whitespace-pre-wrap">
|
||||
{streamingReplyText}
|
||||
<span
|
||||
className={`ml-1 inline-block h-4 w-1 animate-pulse rounded-full align-[-2px] ${theme.accentBgClass}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5 py-1">
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)] [animation-delay:-0.2s]" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)] [animation-delay:-0.1s]" />
|
||||
<span className="h-2 w-2 animate-pulse rounded-full bg-[var(--platform-text-soft)]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="mx-4 mb-3 rounded-[1rem] border border-red-200/70 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="border-t border-[var(--platform-subpanel-border)] p-3">
|
||||
<div className="flex items-end gap-2 rounded-[1.25rem] bg-white/80 p-2">
|
||||
<textarea
|
||||
value={draftText}
|
||||
disabled={isBusy}
|
||||
rows={2}
|
||||
onChange={(event) => {
|
||||
setDraftText(event.target.value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}
|
||||
}}
|
||||
placeholder={composerPlaceholder}
|
||||
className="min-h-[3rem] flex-1 resize-none bg-transparent px-2 py-2 text-sm text-[var(--platform-text-strong)] outline-none placeholder:text-[var(--platform-text-soft)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="发送"
|
||||
disabled={isBusy || !draftText.trim()}
|
||||
onClick={submit}
|
||||
className={`inline-flex h-11 w-11 items-center justify-center rounded-full text-white disabled:cursor-not-allowed disabled:opacity-40 ${theme.userBubbleClass}`}
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreationAgentWorkspace;
|
||||
1
src/components/creation-agent/index.ts
Normal file
1
src/components/creation-agent/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './CreationAgentWorkspace';
|
||||
Reference in New Issue
Block a user