|
|
|
@@ -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>
|
|
|
|
|