|
|
|
@@ -6,7 +6,7 @@ import { openBrandKit, openHome } from "@/application/route";
|
|
|
|
|
import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
|
|
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
|
|
|
import type { BrandKit } from "@/domain/brandKit";
|
|
|
|
|
import type { AgentContent, AgentMessage, AgentThreadSummary, CanvasNode, CanvasViewport, ExtractedTextLayer, ExtractNodeTextResponse, MockupModel, MockupModelMap, MockupPlacement, MockupSurface, NodeActionRequest, Project, ShareAccess } from "@/domain/design";
|
|
|
|
|
import type { AgentContent, AgentMessage, AgentStarterConfig, AgentThreadSummary, CanvasNode, CanvasViewport, ExtractedTextLayer, ExtractNodeTextResponse, MockupModel, MockupModelMap, MockupPlacement, MockupSurface, NodeActionRequest, Project, ShareAccess } from "@/domain/design";
|
|
|
|
|
import { useI18n, type Locale } from "@/i18n/i18n";
|
|
|
|
|
import { brandKitRepository, brandKitStorageScope } from "@/infrastructure/brandKitRepository";
|
|
|
|
|
import { designGateway } from "@/infrastructure/designGateway";
|
|
|
|
@@ -18,6 +18,8 @@ import { BrandKitSelector } from "@/ui/components/BrandKitSelector";
|
|
|
|
|
import { CanvasAnnotationPreview } from "@/ui/components/CanvasAnnotationPreview";
|
|
|
|
|
import { CanvasAnnotations } from "@/ui/components/CanvasAnnotations";
|
|
|
|
|
import { CanvasAgentComposer, composerImageSize, defaultComposerImageSettings, type ComposerImageSettings, type ComposerMode } from "@/ui/components/CanvasAgentComposer";
|
|
|
|
|
import { CanvasAgentEmptyState } from "@/ui/components/CanvasAgentEmptyState";
|
|
|
|
|
import { defaultAgentStarterConfig, normalizeAgentStarterConfig } from "@/ui/components/CanvasAgentEmptyState/defaults";
|
|
|
|
|
import { CanvasAgentTimeline } from "@/ui/components/CanvasAgentTimeline";
|
|
|
|
|
import { CanvasGeneratedFilesPanel, CanvasGrid, CanvasLayerPanel } from "@/ui/components/CanvasPanels";
|
|
|
|
|
import { CanvasMinimap } from "@/ui/components/CanvasMinimap";
|
|
|
|
@@ -85,8 +87,7 @@ const minCanvasZoom = 0.1;
|
|
|
|
|
const maxCanvasZoom = 2.8;
|
|
|
|
|
const defaultCanvasBackgroundColor = "#F7F7F4";
|
|
|
|
|
const canvasBackgroundSwatches = ["#F5F5F5", "#000000", "#FFFFFF", "#00F016", "#A855F7", "#E9D5FF"];
|
|
|
|
|
const generatedFilesPanelWidth = 408;
|
|
|
|
|
const layersPanelWidth = 274;
|
|
|
|
|
const canvasSidePanelWidth = 268;
|
|
|
|
|
const newAgentConversationId = "__new_agent_conversation__";
|
|
|
|
|
const canvasWheelGuardSelector = [
|
|
|
|
|
".ui-popover-content",
|
|
|
|
@@ -575,6 +576,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
|
|
|
|
const { selectedId, selectedIds, setSelectedIds, selectedNodes, selectedNode, selectOnlyNode, selectNodes, toggleNodeSelection } = useCanvasSelection(nodes);
|
|
|
|
|
const [messages, setMessages] = useState<AgentMessage[]>([]);
|
|
|
|
|
const [prompt, setPrompt] = useState("");
|
|
|
|
|
const [agentStarterConfig, setAgentStarterConfig] = useState<AgentStarterConfig>(() => defaultAgentStarterConfig(t));
|
|
|
|
|
const [agentThreads, setAgentThreads] = useState<AgentThreadSummary[]>([]);
|
|
|
|
|
const [credits, setCredits] = useState(30);
|
|
|
|
|
const [assistantPanelOpen, setAssistantPanelOpen] = useState(canViewChat);
|
|
|
|
@@ -703,6 +705,21 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
|
|
|
|
.catch(() => undefined);
|
|
|
|
|
}, [isAuthenticated]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let active = true;
|
|
|
|
|
const fallback = defaultAgentStarterConfig(t);
|
|
|
|
|
setAgentStarterConfig(fallback);
|
|
|
|
|
designGateway
|
|
|
|
|
.getAgentStarterConfig(locale)
|
|
|
|
|
.then((config) => {
|
|
|
|
|
if (active) setAgentStarterConfig(normalizeAgentStarterConfig(config, fallback));
|
|
|
|
|
})
|
|
|
|
|
.catch(() => undefined);
|
|
|
|
|
return () => {
|
|
|
|
|
active = false;
|
|
|
|
|
};
|
|
|
|
|
}, [locale, t]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let active = true;
|
|
|
|
|
if (!isAuthenticated || !canManageBrandKit) {
|
|
|
|
@@ -1083,7 +1100,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
|
|
|
|
height: selectedTextNode.height * viewport.k
|
|
|
|
|
};
|
|
|
|
|
}, [selectedTextNode, viewport]);
|
|
|
|
|
const floatingToolbarLeftInset = generatedFilesPanelOpen ? generatedFilesPanelWidth : layersPanelOpen ? layersPanelWidth : 0;
|
|
|
|
|
const floatingToolbarLeftInset = generatedFilesPanelOpen || layersPanelOpen ? canvasSidePanelWidth : 0;
|
|
|
|
|
const insertCanvasImageReference = useCallback((node: CanvasNode, options: { focus?: boolean; pending?: boolean; replacePending?: boolean } = {}) => {
|
|
|
|
|
const reference = canvasNodeToReferenceImage(node);
|
|
|
|
|
if (!reference) return;
|
|
|
|
@@ -2640,6 +2657,13 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
|
|
|
|
activeAgentThreadIdRef.current = null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectAgentStarterPrompt = (nextPrompt: string) => {
|
|
|
|
|
if (!canEdit) return;
|
|
|
|
|
setComposerMode("agent");
|
|
|
|
|
setPrompt(nextPrompt);
|
|
|
|
|
window.requestAnimationFrame(() => agentComposerRef.current?.focus());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectAgentThread = (threadId: string) => {
|
|
|
|
|
autoScrollAgentFeedRef.current = false;
|
|
|
|
|
selectedAgentThreadIdRef.current = threadId;
|
|
|
|
@@ -3477,6 +3501,8 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const displayProjectTitle = localizedCanvasTitle(project.title, t("canvasUntitled"));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={`workspace-shell ${assistantPanelOpen && canViewChat ? "" : "assistant-panel-collapsed"} ${canEdit ? "" : "is-readonly"} ${canViewChat ? "" : "is-canvas-only"}`}>
|
|
|
|
|
<header className="workspace-topbar">
|
|
|
|
@@ -3512,9 +3538,9 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
|
|
|
|
onDrop={canEdit ? handleCanvasDrop : undefined}
|
|
|
|
|
>
|
|
|
|
|
<input ref={canvasImageInputRef} type="file" accept="image/*" multiple hidden disabled={!canEdit} onChange={handleCanvasImageInput} />
|
|
|
|
|
<div className="workspace-stage-title">
|
|
|
|
|
<div className={`workspace-stage-title ${generatedFilesPanelOpen || layersPanelOpen ? "is-offset-by-canvas-panel" : ""}`}>
|
|
|
|
|
<WorkspaceTitle
|
|
|
|
|
title={project.title}
|
|
|
|
|
title={displayProjectTitle}
|
|
|
|
|
untitledLabel={t("canvasUntitled")}
|
|
|
|
|
renameLabel={t("renameCanvasTitle")}
|
|
|
|
|
placeholder={t("projectTitlePlaceholder")}
|
|
|
|
@@ -4011,7 +4037,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
|
|
|
|
<aside className="assistant-panel">
|
|
|
|
|
<div className="assistant-header">
|
|
|
|
|
<div className="assistant-header-title">
|
|
|
|
|
<strong>{agentHeaderTitle(messages, project, t("canvasUntitled"))}</strong>
|
|
|
|
|
<strong>{agentHeaderTitle(messages, project, displayProjectTitle)}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="assistant-header-actions">
|
|
|
|
|
<button className="assistant-header-icon" type="button" aria-label={t("newChat")} title={t("newChat")} onClick={beginNewAgentConversation} disabled={!canEdit || !hasCurrentConversation}>
|
|
|
|
@@ -4098,7 +4124,11 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="assistant-feed" ref={assistantFeedRef} onScroll={updateAssistantScrollButton}>
|
|
|
|
|
<CanvasAgentTimeline messages={messages} nodes={nodes} active={agentTimelineActive} />
|
|
|
|
|
{!hasCurrentConversation && !agentTimelineActive ? (
|
|
|
|
|
<CanvasAgentEmptyState config={agentStarterConfig} disabled={!canEdit} onSelectPrompt={selectAgentStarterPrompt} />
|
|
|
|
|
) : (
|
|
|
|
|
<CanvasAgentTimeline messages={messages} nodes={nodes} active={agentTimelineActive} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showAssistantScrollButton && (
|
|
|
|
@@ -5616,11 +5646,21 @@ function agentThreadTitleText(content: string) {
|
|
|
|
|
return visiblePromptText(content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function agentHeaderTitle(messages: AgentMessage[], project: Project, fallback: string) {
|
|
|
|
|
function agentHeaderTitle(messages: AgentMessage[], project: Project, displayProjectTitle: string) {
|
|
|
|
|
const firstUserMessage = messages.find((message) => message.role === "user");
|
|
|
|
|
const firstUserText = firstUserMessage ? visiblePromptText(firstUserMessage.content) : "";
|
|
|
|
|
if (firstUserText) return firstUserText;
|
|
|
|
|
return visiblePromptText(project.brief) || project.title.trim() || fallback;
|
|
|
|
|
const brief = visiblePromptText(project.brief);
|
|
|
|
|
return brief && !isDefaultCanvasTitle(brief) ? brief : displayProjectTitle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function localizedCanvasTitle(title: string, fallback: string) {
|
|
|
|
|
const trimmedTitle = title.trim();
|
|
|
|
|
return !trimmedTitle || isDefaultCanvasTitle(trimmedTitle) ? fallback : trimmedTitle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isDefaultCanvasTitle(title: string) {
|
|
|
|
|
return ["Untitled", "Untitled design project", "未命名设计项目"].includes(title.trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isProjectGenerating(project: Project) {
|
|
|
|
|