prompt shape working, fix indicator & scroll later
This commit is contained in:
parent
1126fc4a1c
commit
e7e911c5bb
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { getEdge } from "@/propagators/tlgraph"
|
import { getEdge } from "@/propagators/tlgraph"
|
||||||
import { llm } from "@/utils/llmUtils"
|
import { llm } from "@/utils/llmUtils"
|
||||||
import { isShapeOfType } from "@/propagators/utils"
|
import { isShapeOfType } from "@/propagators/utils"
|
||||||
import React from "react"
|
import React, { useState } from "react"
|
||||||
|
|
||||||
type IPrompt = TLBaseShape<
|
type IPrompt = TLBaseShape<
|
||||||
"Prompt",
|
"Prompt",
|
||||||
|
|
@ -28,6 +28,12 @@ const CopyIcon = () => (
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CheckIcon = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
static override type = "Prompt" as const
|
static override type = "Prompt" as const
|
||||||
|
|
||||||
|
|
@ -83,42 +89,69 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
}, {} as Record<string, TLShape>)
|
}, {} as Record<string, TLShape>)
|
||||||
|
|
||||||
const generateText = async (prompt: string) => {
|
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 conversationHistory = shape.props.value ? shape.props.value + '\n' : ''
|
||||||
const escapedPrompt = prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n')
|
const escapedPrompt = prompt.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n')
|
||||||
const newPrompt = `{"role": "user", "content": "${escapedPrompt}"}\n{"role": "assistant", "content": "`
|
const userMessage = `{"role": "user", "content": "${escapedPrompt}"}`
|
||||||
const fullPrompt = `${conversationHistory}${newPrompt}`
|
|
||||||
|
|
||||||
// First update: Show the user's prompt while preserving history
|
// Update with user message and trigger scroll
|
||||||
this.editor.updateShape<IPrompt>({
|
this.editor.updateShape<IPrompt>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: "Prompt",
|
type: "Prompt",
|
||||||
props: {
|
props: {
|
||||||
value: fullPrompt,
|
value: conversationHistory + userMessage,
|
||||||
agentBinding: "someone"
|
agentBinding: "someone"
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Send just the new prompt to the LLM
|
let fullResponse = ''
|
||||||
let lastResponseLength = 0
|
|
||||||
let fullResponse = '' // Track the complete response
|
|
||||||
await llm(prompt, localStorage.getItem("openai_api_key") || "", (partial: string, done: boolean) => {
|
await llm(prompt, localStorage.getItem("openai_api_key") || "", (partial: string, done: boolean) => {
|
||||||
if (partial) {
|
if (partial) {
|
||||||
fullResponse = partial // Store the complete response
|
fullResponse = partial
|
||||||
// Only get the new content by slicing from the last response length
|
const escapedResponse = partial.replace(/[\\"]/g, '\\$&').replace(/\n/g, '\\n')
|
||||||
const newContent = partial.slice(lastResponseLength)
|
const assistantMessage = `{"role": "assistant", "content": "${escapedResponse}"}`
|
||||||
lastResponseLength = partial.length
|
|
||||||
|
try {
|
||||||
|
JSON.parse(assistantMessage)
|
||||||
|
|
||||||
|
// Use requestAnimationFrame to ensure smooth scrolling during streaming
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.editor.updateShape<IPrompt>({
|
||||||
|
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<IPrompt>({
|
this.editor.updateShape<IPrompt>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: "Prompt",
|
type: "Prompt",
|
||||||
props: {
|
props: {
|
||||||
value: `${fullPrompt}${fullResponse}"}`, // Use the complete response
|
value: conversationHistory + userMessage + '\n' + assistantMessage,
|
||||||
agentBinding: done ? null : "someone"
|
agentBinding: null
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Invalid JSON in final message:', error)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePrompt = () => {
|
const handlePrompt = () => {
|
||||||
|
|
@ -148,6 +181,32 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
// Add state for copy button text
|
// Add state for copy button text
|
||||||
const [copyButtonText, setCopyButtonText] = React.useState("Copy Conversation")
|
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<number | null>(null)
|
||||||
|
|
||||||
|
// Add ref for the chat container
|
||||||
|
const chatContainerRef = React.useRef<HTMLDivElement>(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 () => {
|
const handleCopy = async () => {
|
||||||
try {
|
try {
|
||||||
// Parse and format each message
|
// Parse and format each message
|
||||||
|
|
@ -179,6 +238,9 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSelected = this.editor.getSelectedShapeIds().includes(shape.id)
|
||||||
|
const [isHovering, setIsHovering] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HTMLContainer
|
<HTMLContainer
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -187,7 +249,7 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
padding: this.PADDING,
|
padding: this.PADDING,
|
||||||
height: this.FIXED_HEIGHT,
|
height: this.FIXED_HEIGHT,
|
||||||
width: shape.props.w,
|
width: shape.props.w,
|
||||||
pointerEvents: "all",
|
pointerEvents: isSelected || isHovering ? "all" : "none",
|
||||||
backgroundColor: "#efefef",
|
backgroundColor: "#efefef",
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -196,8 +258,22 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
alignItems: "stretch",
|
alignItems: "stretch",
|
||||||
outline: shape.props.agentBinding ? "2px solid orange" : "none",
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
ref={chatContainerRef}
|
||||||
style={{
|
style={{
|
||||||
padding: "4px 8px",
|
padding: "4px 8px",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -208,6 +284,7 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
|
pointerEvents: isSelected || isHovering ? "all" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{shape.props.value ? (
|
{shape.props.value ? (
|
||||||
|
|
@ -263,7 +340,10 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(parsed.content)
|
await navigator.clipboard.writeText(parsed.content)
|
||||||
// Optional: Show a brief "Copied!" tooltip
|
setCopiedIndex(index)
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopiedIndex(null)
|
||||||
|
}, 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy text:', err)
|
console.error('Failed to copy text:', err)
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +355,7 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
e.currentTarget.style.opacity = '0.7'
|
e.currentTarget.style.opacity = '0.7'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon />
|
{copiedIndex === index ? <CheckIcon /> : <CopyIcon />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -292,7 +372,8 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "5px",
|
gap: "5px",
|
||||||
marginTop: "auto"
|
marginTop: "auto",
|
||||||
|
pointerEvents: isSelected || isHovering ? "all" : "none",
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -362,7 +443,15 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: IPrompt) {
|
// Override the default indicator behavior
|
||||||
return <rect width={shape.props.w} height={shape.props.h} rx={5} />
|
// TODO: FIX SECOND INDICATOR UX GLITCH
|
||||||
|
override indicator(shape: IPrompt) {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
width={shape.props.w}
|
||||||
|
height={this.FIXED_HEIGHT}
|
||||||
|
rx={6}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue