Compare commits
2 Commits
e9cfadfb90
...
a339e09859
| Author | SHA1 | Date | |
|---|---|---|---|
| a339e09859 | |||
| b842c34fca |
@@ -117,6 +117,7 @@
|
||||
"conversation": "Chat",
|
||||
"newChat": "New chat",
|
||||
"conversationHistory": "History",
|
||||
"scrollToLatest": "Scroll to latest",
|
||||
"searchConversationPlaceholder": "Search conversations",
|
||||
"deleteConversation": "Delete conversation",
|
||||
"noConversationHistory": "No history yet",
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
"conversation": "对话",
|
||||
"newChat": "新对话",
|
||||
"conversationHistory": "历史对话",
|
||||
"scrollToLatest": "回到底部",
|
||||
"searchConversationPlaceholder": "请输入搜索关键词",
|
||||
"deleteConversation": "删除对话",
|
||||
"noConversationHistory": "暂无历史对话",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { BookOpen, ChevronDown, Circle, Folder, Frame, Globe2, Image as ImageIcon, Layers, Lock, LogOut, Map as MapIcon, Loader2, MessageCircle, MessageSquarePlus, PanelRightClose, PenLine, Search, Share2, Trash2, Type, User, Zap } from "lucide-react";
|
||||
import { ArrowDown, BookOpen, ChevronDown, Circle, Folder, Frame, Globe2, Image as ImageIcon, Layers, Lock, LogOut, Map as MapIcon, Loader2, MessageCircle, MessageSquarePlus, PanelRightClose, PenLine, Search, Share2, Trash2, Type, User, Zap } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
||||
import { openHome } from "@/application/route";
|
||||
import { ContextMenu, ContextMenuTrigger } from "@/components/ui/context-menu";
|
||||
@@ -550,6 +550,9 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasImageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const agentComposerRef = useRef<PromptComposerHandle | null>(null);
|
||||
const assistantFeedRef = useRef<HTMLDivElement | null>(null);
|
||||
const assistantFooterRef = useRef<HTMLDivElement | null>(null);
|
||||
const autoScrollAgentFeedRef = useRef(false);
|
||||
const referenceImagesRef = useRef<UploadedReferenceImage[]>([]);
|
||||
const annotationsRef = useRef<CanvasAnnotation[]>([]);
|
||||
const imageGeneratorThreadIdsRef = useRef<Set<string>>(new Set());
|
||||
@@ -572,6 +575,8 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
const [agentHistoryOpen, setAgentHistoryOpen] = useState(false);
|
||||
const [agentHistorySearch, setAgentHistorySearch] = useState("");
|
||||
const [selectedAgentThreadId, setSelectedAgentThreadId] = useState<string | null>(null);
|
||||
const [assistantFooterHeight, setAssistantFooterHeight] = useState(0);
|
||||
const [showAssistantScrollButton, setShowAssistantScrollButton] = useState(false);
|
||||
const [referenceImages, setReferenceImages] = useState<UploadedReferenceImage[]>([]);
|
||||
const [annotations, setAnnotations] = useState<CanvasAnnotation[]>([]);
|
||||
const [annotationPreview, setAnnotationPreview] = useState<AnnotationPreviewRequest | null>(null);
|
||||
@@ -650,6 +655,29 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
const agentTimelineActive = agentGenerating || Boolean(project && !projectHasCanvasActionGeneration(project) && nodes.some(isAgentVisibleGeneratingNode));
|
||||
const mockupPreparing = Boolean(mockupPendingTargetId);
|
||||
const anyGenerating = agentGenerating || imageGeneratorGenerating || canvasActionWorkActive || mockupPreparing;
|
||||
const updateAssistantScrollButton = useCallback(() => {
|
||||
const feed = assistantFeedRef.current;
|
||||
if (!feed || feed.scrollHeight <= feed.clientHeight) {
|
||||
setShowAssistantScrollButton(false);
|
||||
return;
|
||||
}
|
||||
const distanceToEnd = feed.scrollHeight - feed.scrollTop - feed.clientHeight;
|
||||
const shouldShow = distanceToEnd > 24;
|
||||
setShowAssistantScrollButton((current) => (current === shouldShow ? current : shouldShow));
|
||||
}, []);
|
||||
const scrollAssistantFeedToEnd = useCallback((behavior: ScrollBehavior = "auto") => {
|
||||
window.requestAnimationFrame(() => {
|
||||
const feed = assistantFeedRef.current;
|
||||
if (!feed) return;
|
||||
if (behavior === "auto") {
|
||||
feed.scrollTop = feed.scrollHeight;
|
||||
updateAssistantScrollButton();
|
||||
return;
|
||||
}
|
||||
feed.scrollTo({ top: feed.scrollHeight, behavior });
|
||||
window.setTimeout(updateAssistantScrollButton, 220);
|
||||
});
|
||||
}, [updateAssistantScrollButton]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem("fluxo:show-minimap", showMinimap ? "true" : "false");
|
||||
@@ -754,12 +782,47 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
}
|
||||
}, [agentGenerating]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!assistantPanelOpen || !autoScrollAgentFeedRef.current) return;
|
||||
scrollAssistantFeedToEnd("auto");
|
||||
if (agentTimelineActive) return;
|
||||
const timer = window.setTimeout(() => {
|
||||
autoScrollAgentFeedRef.current = false;
|
||||
}, 250);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [agentTimelineActive, assistantPanelOpen, messages, scrollAssistantFeedToEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!assistantPanelOpen) {
|
||||
setShowAssistantScrollButton(false);
|
||||
return;
|
||||
}
|
||||
const frame = window.requestAnimationFrame(updateAssistantScrollButton);
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [agentTimelineActive, assistantPanelOpen, messages, updateAssistantScrollButton]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!assistantPanelOpen) return;
|
||||
const footer = assistantFooterRef.current;
|
||||
if (!footer) return;
|
||||
const updateFooterHeight = () => {
|
||||
setAssistantFooterHeight(Math.ceil(footer.getBoundingClientRect().height));
|
||||
updateAssistantScrollButton();
|
||||
};
|
||||
updateFooterHeight();
|
||||
if (typeof ResizeObserver === "undefined") return;
|
||||
const observer = new ResizeObserver(updateFooterHeight);
|
||||
observer.observe(footer);
|
||||
return () => observer.disconnect();
|
||||
}, [assistantPanelOpen, error, updateAssistantScrollButton]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedAgentThreadIdRef.current = null;
|
||||
setSelectedAgentThreadId(null);
|
||||
setAgentThreads([]);
|
||||
setAgentHistorySearch("");
|
||||
setAgentHistoryOpen(false);
|
||||
autoScrollAgentFeedRef.current = false;
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -2435,6 +2498,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
};
|
||||
|
||||
const beginNewAgentConversation = () => {
|
||||
autoScrollAgentFeedRef.current = false;
|
||||
selectedAgentThreadIdRef.current = newAgentConversationId;
|
||||
setSelectedAgentThreadId(newAgentConversationId);
|
||||
setAgentHistoryOpen(false);
|
||||
@@ -2446,6 +2510,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
};
|
||||
|
||||
const selectAgentThread = (threadId: string) => {
|
||||
autoScrollAgentFeedRef.current = false;
|
||||
selectedAgentThreadIdRef.current = threadId;
|
||||
setSelectedAgentThreadId(threadId);
|
||||
setAgentHistoryOpen(false);
|
||||
@@ -2500,6 +2565,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
selectedAgentThreadIdRef.current = threadId;
|
||||
setSelectedAgentThreadId(threadId);
|
||||
setAgentGenerating(true);
|
||||
autoScrollAgentFeedRef.current = true;
|
||||
}
|
||||
setError("");
|
||||
if (source === "image-generator") {
|
||||
@@ -2551,6 +2617,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
const stopAgentTask = async () => {
|
||||
if (!project) return;
|
||||
const threadId = activeAgentThreadIdRef.current || project.lastThreadId;
|
||||
autoScrollAgentFeedRef.current = false;
|
||||
stopGenerationStream("agent");
|
||||
activeAgentThreadIdRef.current = null;
|
||||
setAgentGenerating(false);
|
||||
@@ -3858,37 +3925,52 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="assistant-feed">
|
||||
<div className="assistant-feed" ref={assistantFeedRef} onScroll={updateAssistantScrollButton}>
|
||||
<CanvasAgentTimeline messages={messages} nodes={nodes} active={agentTimelineActive} />
|
||||
</div>
|
||||
|
||||
{error && <p className="panel-error">{error}</p>}
|
||||
{showAssistantScrollButton && (
|
||||
<div className="assistant-scroll-to-end" style={{ bottom: assistantFooterHeight > 0 ? assistantFooterHeight + 12 : 178 }}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("scrollToLatest")}
|
||||
title={t("scrollToLatest")}
|
||||
onClick={() => scrollAssistantFeedToEnd("smooth")}
|
||||
>
|
||||
<ArrowDown size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CanvasAgentComposer
|
||||
ref={agentComposerRef}
|
||||
text={prompt}
|
||||
mode={composerMode}
|
||||
models={imageModels}
|
||||
selectedModel={selectedCanvasModel}
|
||||
settings={composerImageSettings}
|
||||
references={referenceImages}
|
||||
annotations={annotations}
|
||||
webSearchEnabled={useAgentWebSearch}
|
||||
generating={agentGenerating}
|
||||
previewSlot={<CanvasAnnotationPreview preview={annotationPreview} nodes={nodes} />}
|
||||
onTextChange={setPrompt}
|
||||
onModeChange={setComposerMode}
|
||||
onSettingsChange={updateComposerImageSettings}
|
||||
onModelChange={setSelectedCanvasModelId}
|
||||
onWebSearchChange={setUseAgentWebSearch}
|
||||
onUploadFile={attachImage}
|
||||
onReferencesChange={syncReferenceImages}
|
||||
onReferenceRemoved={handleReferenceRemoved}
|
||||
onAnnotationsChange={syncAnnotations}
|
||||
onAnnotationPreviewChange={setAnnotationPreview}
|
||||
onSubmit={() => void generate("agent")}
|
||||
onStop={() => void stopAgentTask()}
|
||||
/>
|
||||
<div className="assistant-footer-dock" ref={assistantFooterRef}>
|
||||
{error && <p className="panel-error">{error}</p>}
|
||||
|
||||
<CanvasAgentComposer
|
||||
ref={agentComposerRef}
|
||||
text={prompt}
|
||||
mode={composerMode}
|
||||
models={imageModels}
|
||||
selectedModel={selectedCanvasModel}
|
||||
settings={composerImageSettings}
|
||||
references={referenceImages}
|
||||
annotations={annotations}
|
||||
webSearchEnabled={useAgentWebSearch}
|
||||
generating={agentGenerating}
|
||||
previewSlot={<CanvasAnnotationPreview preview={annotationPreview} nodes={nodes} />}
|
||||
onTextChange={setPrompt}
|
||||
onModeChange={setComposerMode}
|
||||
onSettingsChange={updateComposerImageSettings}
|
||||
onModelChange={setSelectedCanvasModelId}
|
||||
onWebSearchChange={setUseAgentWebSearch}
|
||||
onUploadFile={attachImage}
|
||||
onReferencesChange={syncReferenceImages}
|
||||
onReferenceRemoved={handleReferenceRemoved}
|
||||
onAnnotationsChange={syncAnnotations}
|
||||
onAnnotationPreviewChange={setAnnotationPreview}
|
||||
onSubmit={() => void generate("agent")}
|
||||
onStop={() => void stopAgentTask()}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -3602,6 +3602,11 @@ button:disabled {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.assistant-footer-dock {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.assistant-feed {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -3637,6 +3642,40 @@ button:disabled {
|
||||
background: #dfe0e2;
|
||||
}
|
||||
|
||||
.assistant-scroll-to-end {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
z-index: 12;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.assistant-scroll-to-end button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid #e1e3e8;
|
||||
border-radius: 999px;
|
||||
color: #373b42;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgba(17, 24, 39, 0.12);
|
||||
pointer-events: auto;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.assistant-scroll-to-end button:hover {
|
||||
border-color: #d4d8df;
|
||||
background: #f8f9fa;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.assistant-scroll-to-end button:active {
|
||||
background: #eef0f3;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.generated-preview-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
@@ -94,6 +94,8 @@ func (c *CreativeAgent) Respond(ctx context.Context, req design.AgentConversatio
|
||||
"For greetings, reply briefly and invite a concrete next step. For capability questions, answer with image-only capabilities: brand/site visuals, ecommerce product images, posters/banners/social images, reference-based generation, image edits, background removal, vectorization, mockup placement, and multi-round canvas iteration.",
|
||||
"When a current project exists, connect capabilities to the canvas instead of giving a generic product brochure.",
|
||||
"When the task plan indicates missing requirements or contains user response questions, ask those questions directly and stop; do not pretend generation has started.",
|
||||
"Use a few purposeful emoji in visible replies to make scanning warmer: put a relevant emoji at the start of capability or next-step bullets, for example 🎨 design direction, 📦 packaging, 🖼️ canvas. Keep the tone professional and avoid empty bullet labels such as '• :'.",
|
||||
"Format emoji lists as short separate lines: intro sentence, then one emoji-led item per line, for example 🎭角色设定、\\n📦系列包装、\\n🧩展示台/贴纸、\\n📣发售海报。 Do not put a hyphen after the emoji. Do not chain items like 🎨-设计...-生成..., do not start continuation lines with bare hyphens, and never end with empty choices such as 1. 2. 3.",
|
||||
}, " "),
|
||||
},
|
||||
{
|
||||
@@ -116,6 +118,8 @@ func (c *CreativeAgent) StreamRespond(ctx context.Context, req design.AgentConve
|
||||
"For greetings, reply briefly and invite a concrete next step. For capability questions, answer with image-only capabilities: brand/site visuals, ecommerce product images, posters/banners/social images, reference-based generation, image edits, background removal, vectorization, mockup placement, and multi-round canvas iteration.",
|
||||
"When a current project exists, connect capabilities to the canvas instead of giving a generic product brochure.",
|
||||
"When the task plan indicates missing requirements or contains user response questions, ask those questions directly and stop; do not pretend generation has started.",
|
||||
"Use a few purposeful emoji in visible replies to make scanning warmer: put a relevant emoji at the start of capability or next-step bullets, for example 🎨 design direction, 📦 packaging, 🖼️ canvas. Keep the tone professional and avoid empty bullet labels such as '• :'.",
|
||||
"Format emoji lists as short separate lines: intro sentence, then one emoji-led item per line, for example 🎭角色设定、\\n📦系列包装、\\n🧩展示台/贴纸、\\n📣发售海报。 Do not put a hyphen after the emoji. Do not chain items like 🎨-设计...-生成..., do not start continuation lines with bare hyphens, and never end with empty choices such as 1. 2. 3.",
|
||||
}, " "),
|
||||
},
|
||||
{
|
||||
@@ -135,6 +139,7 @@ func (c *CreativeAgent) StreamPlanNarration(ctx context.Context, req design.Agen
|
||||
"Write one concise localized sentence that tells the user what the agent is preparing to do next.",
|
||||
"If web research results are present, mention that references have been collected and how they will guide the visual direction.",
|
||||
"Do not use markdown, numbering, or generic canned wording. Use the user's language and locale automatically.",
|
||||
"A single fitting emoji at the beginning is welcome when it improves the live status; do not overuse emoji.",
|
||||
}, " "),
|
||||
},
|
||||
{
|
||||
@@ -155,7 +160,7 @@ func (c *CreativeAgent) PlanAgentTask(ctx context.Context, req design.AgentTaskP
|
||||
"content": strings.Join([]string{
|
||||
"You are the automatic planner for a Moteva-style creative agent.",
|
||||
"Read the user's request, canvas, inline references, short memory, and long memory, then decide the next workflow.",
|
||||
"Return strict JSON only: {\"intent\":\"image\"|\"chat\",\"reason\":\"short internal reason\",\"userFacingResponse\":\"one concise localized sentence explaining what you will do\",\"shouldSearch\":true|false,\"searchQuery\":\"query or empty\",\"searchReason\":\"short reason\",\"imageCount\":0-10,\"imageTasks\":[{\"title\":\"localized short title\",\"brief\":\"specific output brief\"}]}",
|
||||
"Return strict JSON only: {\"intent\":\"image\"|\"chat\",\"reason\":\"short internal reason\",\"userFacingResponse\":\"short localized visible response explaining what you will do\",\"shouldSearch\":true|false,\"searchQuery\":\"query or empty\",\"searchReason\":\"short reason\",\"imageCount\":0-10,\"imageTasks\":[{\"title\":\"localized short title\",\"brief\":\"specific output brief\"}]}",
|
||||
"Intent image means the user wants visual production, image editing, ecommerce/page/poster/product/character/illustration/output generation, or concrete canvas creation.",
|
||||
"Intent chat means greetings, thanks, vague discussion, capability questions, or missing critical information.",
|
||||
"For open-ended brand, ecommerce, homepage, logo, or campaign requests with missing essentials, choose intent chat and ask 2-4 targeted questions before generation. Essentials can include brand/logo assets, visual direction, product/category scope, audience, key sections, slogan/copy, deliverable format, and must-keep constraints.",
|
||||
@@ -175,6 +180,7 @@ func (c *CreativeAgent) PlanAgentTask(ctx context.Context, req design.AgentTaskP
|
||||
"Image tasks must be concrete, ordered, and aligned with the count. For character/design work, separate views, detail sheets, palette/style sheets, and final composite boards when requested.",
|
||||
"Do not expose hidden chain-of-thought. The userFacingResponse is a short planning summary, not a long explanation.",
|
||||
"If intent is chat for clarification, userFacingResponse should be the exact concise questions to ask and imageCount must be 0.",
|
||||
"For userFacingResponse, use a few purposeful emoji in visible questions or summaries, especially as list markers. Use newline-separated emoji items, for example 🎭角色设定、\\n📦系列包装、\\n🧩展示台/贴纸、\\n📣发售海报。 Do not put a hyphen after the emoji, do not chain items like 🎨-设计...-生成..., do not use bare continuation hyphens, and never output empty numbered choices such as 1. 2. 3. Do not put emoji in image task titles or briefs.",
|
||||
"Use the user's language and locale automatically.",
|
||||
}, " "),
|
||||
},
|
||||
@@ -235,6 +241,7 @@ func (c *CreativeAgent) SummarizeImage(ctx context.Context, req design.AgentImag
|
||||
"Do not invent exact visual details you cannot see. Tie the response to the prompt and canvas goal.",
|
||||
"When the user authorized your judgment or asked for an iteration, state the main change you attempted and what constraint it preserves. Mention a next step only when it is useful or required by the workflow.",
|
||||
"Sound like a professional art director, not a template.",
|
||||
"Use one fitting emoji or a short emoji-led phrase when it improves the completion message; keep it sparse.",
|
||||
}, " "),
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user