LLM prompt tool operational, fixed keyboard shortcut conflicts
This commit is contained in:
parent
59e9025336
commit
1126fc4a1c
|
|
@ -8,6 +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"
|
||||||
|
|
||||||
type IPrompt = TLBaseShape<
|
type IPrompt = TLBaseShape<
|
||||||
"Prompt",
|
"Prompt",
|
||||||
|
|
@ -20,11 +21,18 @@ type IPrompt = TLBaseShape<
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
// Add this SVG copy icon component at the top level of the file
|
||||||
|
const CopyIcon = () => (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z"/>
|
||||||
|
</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
|
||||||
|
|
||||||
FIXED_HEIGHT = 50 as const
|
FIXED_HEIGHT = 500 as const
|
||||||
MIN_WIDTH = 150 as const
|
MIN_WIDTH = 200 as const
|
||||||
PADDING = 4 as const
|
PADDING = 4 as const
|
||||||
|
|
||||||
getDefaultProps(): IPrompt["props"] {
|
getDefaultProps(): IPrompt["props"] {
|
||||||
|
|
@ -74,14 +82,42 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
return acc
|
return acc
|
||||||
}, {} 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 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
|
||||||
|
this.editor.updateShape<IPrompt>({
|
||||||
|
id: shape.id,
|
||||||
|
type: "Prompt",
|
||||||
|
props: {
|
||||||
|
value: fullPrompt,
|
||||||
|
agentBinding: "someone"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send just the new prompt to the LLM
|
||||||
|
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) => {
|
||||||
console.log("DONE??", done)
|
if (partial) {
|
||||||
this.editor.updateShape<IPrompt>({
|
fullResponse = partial // Store the complete response
|
||||||
id: shape.id,
|
// Only get the new content by slicing from the last response length
|
||||||
type: "Prompt",
|
const newContent = partial.slice(lastResponseLength)
|
||||||
props: { value: partial, agentBinding: done ? null : "someone" },
|
lastResponseLength = partial.length
|
||||||
})
|
|
||||||
|
this.editor.updateShape<IPrompt>({
|
||||||
|
id: shape.id,
|
||||||
|
type: "Prompt",
|
||||||
|
props: {
|
||||||
|
value: `${fullPrompt}${fullResponse}"}`, // Use the complete response
|
||||||
|
agentBinding: done ? null : "someone"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,10 +137,48 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//console.log(processedPrompt)
|
|
||||||
generateText(processedPrompt)
|
generateText(processedPrompt)
|
||||||
|
this.editor.updateShape<IPrompt>({
|
||||||
|
id: shape.id,
|
||||||
|
type: "Prompt",
|
||||||
|
props: { prompt: "" },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add state for copy button text
|
||||||
|
const [copyButtonText, setCopyButtonText] = React.useState("Copy Conversation")
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
// Parse and format each message
|
||||||
|
const messages = shape.props.value
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => line.trim())
|
||||||
|
.map(line => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(line);
|
||||||
|
return `${parsed.role}: ${parsed.content}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n');
|
||||||
|
|
||||||
|
await navigator.clipboard.writeText(messages);
|
||||||
|
setCopyButtonText("Copied!");
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopyButtonText("Copy Conversation");
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy text:', err);
|
||||||
|
setCopyButtonText("Failed to copy");
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopyButtonText("Copy Conversation");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HTMLContainer
|
<HTMLContainer
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -117,47 +191,173 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||||
backgroundColor: "#efefef",
|
backgroundColor: "#efefef",
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
justifyContent: "space-between",
|
||||||
|
alignItems: "stretch",
|
||||||
outline: shape.props.agentBinding ? "2px solid orange" : "none",
|
outline: shape.props.agentBinding ? "2px solid orange" : "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
padding: "4px 8px",
|
||||||
height: "100%",
|
flex: 1,
|
||||||
overflow: "visible",
|
backgroundColor: "white",
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
borderRadius: "4px",
|
||||||
border: "1px solid rgba(0, 0, 0, 0.05)",
|
marginBottom: "4px",
|
||||||
borderRadius: 6 - this.PADDING,
|
fontSize: "14px",
|
||||||
fontSize: 16,
|
overflowY: "auto",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
fontFamily: "monospace",
|
||||||
}}
|
}}
|
||||||
type="text"
|
|
||||||
placeholder="Enter prompt..."
|
|
||||||
value={shape.props.prompt}
|
|
||||||
onChange={(text) => {
|
|
||||||
this.editor.updateShape<IPrompt>({
|
|
||||||
id: shape.id,
|
|
||||||
type: "Prompt",
|
|
||||||
props: { prompt: text.target.value },
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
width: 100,
|
|
||||||
height: "100%",
|
|
||||||
marginLeft: 5,
|
|
||||||
pointerEvents: "all",
|
|
||||||
}}
|
|
||||||
onPointerDown={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
onClick={handlePrompt}
|
|
||||||
>
|
>
|
||||||
Prompt
|
{shape.props.value ? (
|
||||||
</button>
|
shape.props.value.split('\n').map((message, index) => {
|
||||||
|
if (!message.trim()) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(message);
|
||||||
|
const isUser = parsed.role === "user";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: isUser ? 'flex-end' : 'flex-start',
|
||||||
|
margin: '8px 0',
|
||||||
|
maxWidth: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '12px 16px',
|
||||||
|
maxWidth: '80%',
|
||||||
|
backgroundColor: isUser ? '#007AFF' : '#f0f0f0',
|
||||||
|
color: isUser ? 'white' : 'black',
|
||||||
|
borderRadius: isUser ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
|
||||||
|
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{parsed.content}
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '-20px',
|
||||||
|
right: isUser ? '0' : 'auto',
|
||||||
|
left: isUser ? 'auto' : '0',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '4px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666',
|
||||||
|
opacity: 0.7,
|
||||||
|
transition: 'opacity 0.2s',
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(parsed.content)
|
||||||
|
// Optional: Show a brief "Copied!" tooltip
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy text:', err)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.opacity = '1'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.opacity = '0.7'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return null; // Skip invalid JSON
|
||||||
|
}
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
"Chat history will appear here..."
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "5px",
|
||||||
|
marginTop: "auto"
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "5px"
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "40px",
|
||||||
|
overflow: "visible",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
||||||
|
border: "1px solid rgba(0, 0, 0, 0.05)",
|
||||||
|
borderRadius: 6 - this.PADDING,
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter prompt..."
|
||||||
|
value={shape.props.prompt}
|
||||||
|
onChange={(text) => {
|
||||||
|
this.editor.updateShape<IPrompt>({
|
||||||
|
id: shape.id,
|
||||||
|
type: "Prompt",
|
||||||
|
props: { prompt: text.target.value },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
handlePrompt()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
width: 100,
|
||||||
|
height: "40px",
|
||||||
|
pointerEvents: "all",
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
onClick={handlePrompt}
|
||||||
|
>
|
||||||
|
Prompt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "30px",
|
||||||
|
pointerEvents: "all",
|
||||||
|
backgroundColor: "#f0f0f0",
|
||||||
|
border: "1px solid #ddd",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
onClick={handleCopy}
|
||||||
|
>
|
||||||
|
{copyButtonText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</HTMLContainer>
|
</HTMLContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
id="MycrozineTemplate"
|
id="MycrozineTemplate"
|
||||||
label="Create Mycrozine Template"
|
label="Create Mycrozine Template"
|
||||||
icon="rectangle"
|
icon="rectangle"
|
||||||
kbd="m"
|
kbd="alt+z"
|
||||||
disabled={hasSelection}
|
disabled={hasSelection}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
editor.setCurrentTool("MycrozineTemplate")
|
editor.setCurrentTool("MycrozineTemplate")
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export const overrides: TLUiOverrides = {
|
||||||
icon: "rectangle",
|
icon: "rectangle",
|
||||||
label: "Mycrozine Template",
|
label: "Mycrozine Template",
|
||||||
type: "MycrozineTemplate",
|
type: "MycrozineTemplate",
|
||||||
kbd: "m",
|
kbd: "alt+z",
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => editor.setCurrentTool("MycrozineTemplate"),
|
onSelect: () => editor.setCurrentTool("MycrozineTemplate"),
|
||||||
},
|
},
|
||||||
|
|
@ -132,7 +132,7 @@ export const overrides: TLUiOverrides = {
|
||||||
icon: "prompt",
|
icon: "prompt",
|
||||||
label: "Prompt",
|
label: "Prompt",
|
||||||
type: "Prompt",
|
type: "Prompt",
|
||||||
kdb: "p",
|
kbd: "alt+p",
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => editor.setCurrentTool("Prompt"),
|
onSelect: () => editor.setCurrentTool("Prompt"),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue