From 08b63c5a12067c0dd1b76bf733386497b34f7c51 Mon Sep 17 00:00:00 2001 From: Jeff-Emmett Date: Tue, 25 Feb 2025 15:48:29 -0500 Subject: [PATCH] LLM prompt tool operational, fixed keyboard shortcut conflicts --- src/shapes/PromptShapeUtil.tsx | 290 ++++++++++++++++++++++++++++----- src/ui/CustomContextMenu.tsx | 2 +- src/ui/overrides.tsx | 4 +- 3 files changed, 248 insertions(+), 48 deletions(-) diff --git a/src/shapes/PromptShapeUtil.tsx b/src/shapes/PromptShapeUtil.tsx index 7fe2591..9ab040f 100644 --- a/src/shapes/PromptShapeUtil.tsx +++ b/src/shapes/PromptShapeUtil.tsx @@ -8,6 +8,7 @@ import { import { getEdge } from "@/propagators/tlgraph" import { llm } from "@/utils/llmUtils" import { isShapeOfType } from "@/propagators/utils" +import React from "react" type IPrompt = TLBaseShape< "Prompt", @@ -20,11 +21,18 @@ type IPrompt = TLBaseShape< } > +// Add this SVG copy icon component at the top level of the file +const CopyIcon = () => ( + + + +) + export class PromptShape extends BaseBoxShapeUtil { static override type = "Prompt" as const - FIXED_HEIGHT = 50 as const - MIN_WIDTH = 150 as const + FIXED_HEIGHT = 500 as const + MIN_WIDTH = 200 as const PADDING = 4 as const getDefaultProps(): IPrompt["props"] { @@ -74,14 +82,42 @@ export class PromptShape extends BaseBoxShapeUtil { return acc }, {} as Record) - 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({ + 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) => { - console.log("DONE??", done) - this.editor.updateShape({ - id: shape.id, - type: "Prompt", - props: { value: partial, agentBinding: done ? null : "someone" }, - }) + 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 + + this.editor.updateShape({ + 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 { } } } - //console.log(processedPrompt) generateText(processedPrompt) + this.editor.updateShape({ + 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 ( { backgroundColor: "#efefef", overflow: "visible", display: "flex", - justifyContent: "center", - alignItems: "center", + flexDirection: "column", + justifyContent: "space-between", + alignItems: "stretch", outline: shape.props.agentBinding ? "2px solid orange" : "none", }} > - { - this.editor.updateShape({ - id: shape.id, - type: "Prompt", - props: { prompt: text.target.value }, - }) - }} - /> - + {shape.props.value ? ( + 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 ( +
+
+ {parsed.content} + +
+
+ ); + } catch { + return null; // Skip invalid JSON + } + }) + ) : ( + "Chat history will appear here..." + )} + +
+
+ { + this.editor.updateShape({ + id: shape.id, + type: "Prompt", + props: { prompt: text.target.value }, + }) + }} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handlePrompt() + } + }} + /> + +
+ +
) } diff --git a/src/ui/CustomContextMenu.tsx b/src/ui/CustomContextMenu.tsx index a5a84cf..e6c5ab5 100644 --- a/src/ui/CustomContextMenu.tsx +++ b/src/ui/CustomContextMenu.tsx @@ -185,7 +185,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) { id="MycrozineTemplate" label="Create Mycrozine Template" icon="rectangle" - kbd="m" + kbd="alt+z" disabled={hasSelection} onSelect={() => { editor.setCurrentTool("MycrozineTemplate") diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx index 60c7d1e..0e308e3 100644 --- a/src/ui/overrides.tsx +++ b/src/ui/overrides.tsx @@ -123,7 +123,7 @@ export const overrides: TLUiOverrides = { icon: "rectangle", label: "Mycrozine Template", type: "MycrozineTemplate", - kbd: "m", + kbd: "alt+z", readonlyOk: true, onSelect: () => editor.setCurrentTool("MycrozineTemplate"), }, @@ -132,7 +132,7 @@ export const overrides: TLUiOverrides = { icon: "prompt", label: "Prompt", type: "Prompt", - kdb: "p", + kbd: "alt+p", readonlyOk: true, onSelect: () => editor.setCurrentTool("Prompt"), },