From e7e911c5bb88e086473dc75ff9666f962e57e1c0 Mon Sep 17 00:00:00 2001 From: Jeff-Emmett Date: Tue, 25 Feb 2025 17:53:36 -0500 Subject: [PATCH] prompt shape working, fix indicator & scroll later --- src/shapes/PromptShapeUtil.tsx | 137 +++++++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 24 deletions(-) diff --git a/src/shapes/PromptShapeUtil.tsx b/src/shapes/PromptShapeUtil.tsx index 9ab040f..0ca554f 100644 --- a/src/shapes/PromptShapeUtil.tsx +++ b/src/shapes/PromptShapeUtil.tsx @@ -8,7 +8,7 @@ import { import { getEdge } from "@/propagators/tlgraph" import { llm } from "@/utils/llmUtils" import { isShapeOfType } from "@/propagators/utils" -import React from "react" +import React, { useState } from "react" type IPrompt = TLBaseShape< "Prompt", @@ -28,6 +28,12 @@ const CopyIcon = () => ( ) +const CheckIcon = () => ( + + + +) + export class PromptShape extends BaseBoxShapeUtil { static override type = "Prompt" as const @@ -83,42 +89,69 @@ export class PromptShape extends BaseBoxShapeUtil { }, {} as Record) const generateText = async (prompt: string) => { - // Get existing conversation history, ensure it ends with a newline if not empty const conversationHistory = shape.props.value ? shape.props.value + '\n' : '' - const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n') - const newPrompt = `{"role": "user", "content": "${escapedPrompt}"}\n{"role": "assistant", "content": "` - const fullPrompt = `${conversationHistory}${newPrompt}` - - // First update: Show the user's prompt while preserving history + const escapedPrompt = prompt.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n') + const userMessage = `{"role": "user", "content": "${escapedPrompt}"}` + + // Update with user message and trigger scroll this.editor.updateShape({ id: shape.id, type: "Prompt", props: { - value: fullPrompt, + value: conversationHistory + userMessage, agentBinding: "someone" }, }) - // Send just the new prompt to the LLM - let lastResponseLength = 0 - let fullResponse = '' // Track the complete response + let fullResponse = '' + await llm(prompt, localStorage.getItem("openai_api_key") || "", (partial: string, done: boolean) => { if (partial) { - fullResponse = partial // Store the complete response - // Only get the new content by slicing from the last response length - const newContent = partial.slice(lastResponseLength) - lastResponseLength = partial.length + fullResponse = partial + const escapedResponse = partial.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n') + const assistantMessage = `{"role": "assistant", "content": "${escapedResponse}"}` + + try { + JSON.parse(assistantMessage) + + // Use requestAnimationFrame to ensure smooth scrolling during streaming + requestAnimationFrame(() => { + this.editor.updateShape({ + id: shape.id, + type: "Prompt", + props: { + value: conversationHistory + userMessage + '\n' + assistantMessage, + agentBinding: done ? null : "someone" + }, + }) + }) + } catch (error) { + console.error('Invalid JSON message:', error) + } + } + }) + + // Ensure the final message is saved after streaming is complete + if (fullResponse) { + const escapedResponse = fullResponse.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n') + const assistantMessage = `{"role": "assistant", "content": "${escapedResponse}"}` + + try { + // Verify the final message is valid JSON before updating + JSON.parse(assistantMessage) this.editor.updateShape({ id: shape.id, type: "Prompt", props: { - value: `${fullPrompt}${fullResponse}"}`, // Use the complete response - agentBinding: done ? null : "someone" + value: conversationHistory + userMessage + '\n' + assistantMessage, + agentBinding: null }, }) + } catch (error) { + console.error('Invalid JSON in final message:', error) } - }) + } } const handlePrompt = () => { @@ -148,6 +181,32 @@ export class PromptShape extends BaseBoxShapeUtil { // Add state for copy button text const [copyButtonText, setCopyButtonText] = React.useState("Copy Conversation") + // In the component function, add state for tracking copy success + const [isCopied, setIsCopied] = React.useState(false) + + // In the component function, update the state to track which message was copied + const [copiedIndex, setCopiedIndex] = React.useState(null) + + // Add ref for the chat container + const chatContainerRef = React.useRef(null) + + // Add function to scroll to bottom + const scrollToBottom = () => { + if (chatContainerRef.current) { + // Use requestAnimationFrame for smooth scrolling + requestAnimationFrame(() => { + if (chatContainerRef.current) { + chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight + } + }) + } + } + + // Use both value and agentBinding as dependencies to catch all updates + React.useEffect(() => { + scrollToBottom() + }, [shape.props.value, shape.props.agentBinding]) + const handleCopy = async () => { try { // Parse and format each message @@ -179,6 +238,9 @@ export class PromptShape extends BaseBoxShapeUtil { } }; + const isSelected = this.editor.getSelectedShapeIds().includes(shape.id) + const [isHovering, setIsHovering] = useState(false) + return ( { padding: this.PADDING, height: this.FIXED_HEIGHT, width: shape.props.w, - pointerEvents: "all", + pointerEvents: isSelected || isHovering ? "all" : "none", backgroundColor: "#efefef", overflow: "visible", display: "flex", @@ -196,8 +258,22 @@ export class PromptShape extends BaseBoxShapeUtil { alignItems: "stretch", outline: shape.props.agentBinding ? "2px solid orange" : "none", }} + //TODO: FIX SCROLL IN PROMPT CHAT WHEN HOVERING OVER ELEMENT + onPointerEnter={() => setIsHovering(true)} + onPointerLeave={() => setIsHovering(false)} + onWheel={(e) => { + if (isSelected || isHovering) { + e.preventDefault() + e.stopPropagation() + + if (chatContainerRef.current) { + chatContainerRef.current.scrollTop += e.deltaY + } + } + }} >
{ overflowY: "auto", whiteSpace: "pre-wrap", fontFamily: "monospace", + pointerEvents: isSelected || isHovering ? "all" : "none", }} > {shape.props.value ? ( @@ -263,7 +340,10 @@ export class PromptShape extends BaseBoxShapeUtil { onClick={async () => { try { await navigator.clipboard.writeText(parsed.content) - // Optional: Show a brief "Copied!" tooltip + setCopiedIndex(index) + setTimeout(() => { + setCopiedIndex(null) + }, 2000) } catch (err) { console.error('Failed to copy text:', err) } @@ -275,7 +355,7 @@ export class PromptShape extends BaseBoxShapeUtil { e.currentTarget.style.opacity = '0.7' }} > - + {copiedIndex === index ? : }
@@ -292,7 +372,8 @@ export class PromptShape extends BaseBoxShapeUtil { display: "flex", flexDirection: "column", gap: "5px", - marginTop: "auto" + marginTop: "auto", + pointerEvents: isSelected || isHovering ? "all" : "none", }}>
{ ) } - indicator(shape: IPrompt) { - return + // Override the default indicator behavior + // TODO: FIX SECOND INDICATOR UX GLITCH + override indicator(shape: IPrompt) { + return ( + + ) } }