Skip to content

Commit 0409b2d

Browse files
huntertransmissions11
hunter
andauthored
feat: adds code blocks (#19)
* adds code blocks, copy code button * improve code blocks, fix wrapping, break out into component for readability * remove copycodebutton file * prettier formatting * add titlebar to code blocks, move copy button to title bar * utilize chakra instead of vanilla divs * again * address some comments * remove unused prop * edit button change * edit button improvement * more comments * margin update * edit button for user nodes * copyMessagesToClipboard into utils * rename fn * remove extra useEffect hook * remove height useeffect as not really necessary * remove extra lines * fix word break issue * nit: randomc leanup * nit: style nits * nit: more style nits * fix: timeout before focusing promptbox * nit: cleanup * feat: allow clicking on a node in view mode to edit it * nit: imports * perf: memo TextAndCodeBlock * nit: hover color * fix: only show margin if before text * nit: make system nodes edit by default * nit: stale whiteSpace="pre-wrap" * feat: always set is editing if clicking * docs: note * nit: numbers * fix: get rid of timeout * nit: cleanup * nit: more tweaks * fix: copyMessagesToClipboard * nit: spaces * refactor: iterative instead of recursive --------- Co-authored-by: t11s <[email protected]>
1 parent 1ccbfd3 commit 0409b2d

10 files changed

+692
-53
lines changed

package-lock.json

+432
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"react-beforeunload": "^2.5.3",
2424
"react-dom": "^18.2.0",
2525
"react-hotkeys-hook": "^4.3.7",
26+
"react-syntax-highlighter": "^15.5.0",
2627
"react-textarea-autosize": "^8.4.0",
2728
"reactflow": "^11.5.6",
2829
"yield-stream": "^2.3.0"
@@ -33,6 +34,7 @@
3334
"@types/react": "^18.0.27",
3435
"@types/react-beforeunload": "^2.1.1",
3536
"@types/react-dom": "^18.0.10",
37+
"@types/react-syntax-highlighter": "^15.5.6",
3638
"@vitejs/plugin-react-swc": "^3.0.0",
3739
"typescript": "^4.9.3",
3840
"vite": "^4.1.0"

src/components/App.tsx

+18-9
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import ReactFlow, {
1212
ReactFlowInstance,
1313
ReactFlowJsonObject,
1414
useReactFlow,
15-
Controls,
1615
} from "reactflow";
1716

1817
import "reactflow/dist/style.css";
@@ -75,6 +74,7 @@ import {
7574
NEW_TREE_CONTENT_QUERY_PARAM,
7675
OVERLAP_RANDOMNESS_MAX,
7776
REACT_FLOW_LOCAL_STORAGE_KEY,
77+
TOAST_CONFIG,
7878
UNDEFINED_RESPONSE_STRING,
7979
} from "../utils/constants";
8080
import { mod } from "../utils/mod";
@@ -87,6 +87,7 @@ import { NavigationBar } from "./utils/NavigationBar";
8787
import { useDebouncedEffect } from "../utils/debounce";
8888
import { useDebouncedWindowResize } from "../utils/resize";
8989
import { getQueryParam, resetURL } from "../utils/qparams";
90+
import { copySnippetToClipboard } from "../utils/clipboard";
9091
import { messagesFromLineage, promptFromLineage } from "../utils/prompt";
9192
import { newFluxEdge, modifyFluxEdge, addFluxEdge } from "../utils/fluxEdge";
9293
import { getFluxNodeTypeColor, getFluxNodeTypeDarkColor } from "../utils/color";
@@ -406,9 +407,7 @@ function App() {
406407
toast({
407408
title: err.toString(),
408409
status: "error",
409-
isClosable: true,
410-
variant: "left-accent",
411-
position: "bottom-left",
410+
...TOAST_CONFIG,
412411
})
413412
);
414413

@@ -756,10 +755,22 @@ function App() {
756755
COPY MESSAGES LOGIC
757756
//////////////////////////////////////////////////////////////*/
758757

759-
const copyMessagesToClipboard = () => {
758+
const copyMessagesToClipboard = async () => {
760759
const messages = promptFromLineage(selectedNodeLineage, settings);
761760

762-
if (messages) navigator.clipboard.writeText(messages);
761+
if (await copySnippetToClipboard(messages)) {
762+
toast({
763+
title: "Copied messages to clipboard!",
764+
status: "success",
765+
...TOAST_CONFIG,
766+
});
767+
} else {
768+
toast({
769+
title: "Failed to copy messages to clipboard!",
770+
status: "error",
771+
...TOAST_CONFIG,
772+
});
773+
}
763774
};
764775

765776
/*//////////////////////////////////////////////////////////////
@@ -806,7 +817,6 @@ function App() {
806817
);
807818
useHotkeys("meta+k", completeNextWords, HOTKEY_CONFIG);
808819
useHotkeys("meta+backspace", deleteSelectedNodes, HOTKEY_CONFIG);
809-
810820
useHotkeys("ctrl+c", copyMessagesToClipboard, HOTKEY_CONFIG);
811821

812822
/*//////////////////////////////////////////////////////////////
@@ -834,7 +844,7 @@ function App() {
834844
<Row mainAxisAlignment="flex-start" crossAxisAlignment="stretch" expand>
835845
<Resizable
836846
maxWidth="75%"
837-
minWidth="20%"
847+
minWidth="15%"
838848
defaultSize={{
839849
width: "50%",
840850
height: "auto",
@@ -930,7 +940,6 @@ function App() {
930940
}}
931941
>
932942
<Background />
933-
<Controls position="top-right" showInteractive={false} />
934943
</ReactFlow>
935944
</Column>
936945
</Resizable>

src/components/Prompt.tsx

+103-42
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import { useEffect, useRef } from "react";
2-
31
import { Node } from "reactflow";
42

5-
import { Spinner, Text } from "@chakra-ui/react";
3+
import { useState, useEffect, useRef } from "react";
4+
5+
import { Spinner, Text, Button } from "@chakra-ui/react";
6+
7+
import { EditIcon, ViewIcon } from "@chakra-ui/icons";
68

79
import TextareaAutosize from "react-textarea-autosize";
810

911
import { getFluxNodeTypeColor, getFluxNodeTypeDarkColor } from "../utils/color";
12+
import { TextAndCodeBlock } from "./utils/TextAndCodeBlock";
1013
import { FluxNodeData, FluxNodeType, Settings } from "../utils/types";
1114
import { displayNameFromFluxNodeType } from "../utils/fluxNode";
1215
import { LabeledSlider } from "./utils/LabeledInputs";
13-
import { Row, Center } from "../utils/chakra";
16+
import { Row, Center, Column } from "../utils/chakra";
1417
import { BigButton } from "./utils/BigButton";
1518

1619
export function Prompt({
@@ -44,6 +47,15 @@ export function Prompt({
4447
}
4548
};
4649

50+
/*//////////////////////////////////////////////////////////////
51+
STATE
52+
//////////////////////////////////////////////////////////////*/
53+
54+
const [isEditing, setIsEditing] = useState(
55+
promptNodeType === FluxNodeType.User || promptNodeType === FluxNodeType.System
56+
);
57+
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
58+
4759
/*//////////////////////////////////////////////////////////////
4860
EFFECTS
4961
//////////////////////////////////////////////////////////////*/
@@ -57,17 +69,30 @@ export function Prompt({
5769
.getElementById("promptButtons")
5870
?.scrollIntoView(/* { behavior: "smooth" } */);
5971

60-
const promptBox = window.document.getElementById(
61-
"promptBox"
62-
) as HTMLTextAreaElement | null;
72+
// If the user clicked on the node, we assume they want to edit it.
73+
// Otherwise, we only put them in edit mode if its a user or system node.
74+
setIsEditing(
75+
textOffsetRef.current !== -1 ||
76+
promptNodeType === FluxNodeType.User ||
77+
promptNodeType === FluxNodeType.System
78+
);
79+
}, [promptNode.id]);
6380

64-
// Focus the text box and move the cursor to chosen offset (defaults to end).
65-
promptBox?.setSelectionRange(textOffsetRef.current, textOffsetRef.current);
66-
promptBox?.focus();
81+
// Focus the textbox when the user changes into edit mode.
82+
useEffect(() => {
83+
if (isEditing) {
84+
const promptBox = window.document.getElementById(
85+
"promptBox"
86+
) as HTMLTextAreaElement | null;
6787

68-
// Default to moving to the start of the text.
69-
textOffsetRef.current = -1;
70-
}, [promptNode.id]);
88+
// Focus the text box and move the cursor to chosen offset (defaults to end).
89+
promptBox?.setSelectionRange(textOffsetRef.current, textOffsetRef.current);
90+
promptBox?.focus();
91+
92+
// Default to moving to the end of the text.
93+
textOffsetRef.current = -1;
94+
}
95+
}, [promptNode.id, isEditing]);
7196

7297
/*//////////////////////////////////////////////////////////////
7398
APP
@@ -87,62 +112,98 @@ export function Prompt({
87112
<Row
88113
mb={2}
89114
p={3}
90-
whiteSpace="pre-wrap" // Preserve newlines.
91115
mainAxisAlignment="flex-start"
92116
crossAxisAlignment="flex-start"
93117
borderRadius="6px"
94118
borderLeftWidth={isLast ? "4px" : "0px"}
119+
_hover={{
120+
boxShadow: isLast ? "none" : "0 0 0 0.5px #1a192b",
121+
}}
95122
borderColor={getFluxNodeTypeDarkColor(data.fluxNodeType)}
123+
position="relative"
124+
onMouseEnter={() => setHoveredNodeId(node.id)}
125+
onMouseLeave={() => setHoveredNodeId(null)}
96126
bg={getFluxNodeTypeColor(data.fluxNodeType)}
97127
key={node.id}
98-
{...(!isLast
99-
? {
100-
onClick: () => {
128+
onClick={
129+
isLast
130+
? undefined
131+
: () => {
101132
const selection = window.getSelection();
102133

103134
// We don't want to trigger the selection
104135
// if they're just selecting/copying text.
105136
if (selection?.isCollapsed) {
137+
// TODO: Note this is basically broken because of codeblocks.
106138
textOffsetRef.current = selection.anchorOffset ?? 0;
107139

108140
selectNode(node.id);
141+
setIsEditing(true);
109142
}
110-
},
111-
cursor: "pointer",
112-
}
113-
: {})}
143+
}
144+
}
145+
cursor={isLast && isEditing ? "text" : "pointer"}
114146
>
115147
{data.generating && data.text === "" ? (
116148
<Center expand>
117149
<Spinner />
118150
</Center>
119151
) : (
120152
<>
153+
<Button
154+
display={
155+
hoveredNodeId === promptNode.id && promptNode.id === node.id
156+
? "block"
157+
: "none"
158+
}
159+
onClick={() => setIsEditing(!isEditing)}
160+
position="absolute"
161+
top={1}
162+
right={1}
163+
zIndex={10}
164+
variant="outline"
165+
border="0px"
166+
_hover={{ background: "none" }}
167+
p={1}
168+
>
169+
{isEditing ? <ViewIcon boxSize={4} /> : <EditIcon boxSize={4} />}
170+
</Button>
121171
<Text fontWeight="bold" width="auto" whiteSpace="nowrap">
122172
{displayNameFromFluxNodeType(data.fluxNodeType)}
123173
:&nbsp;
124174
</Text>
125-
{isLast ? (
126-
<TextareaAutosize
127-
id="promptBox"
128-
style={{
129-
width: "100%",
130-
backgroundColor: "transparent",
131-
outline: "none",
132-
}}
133-
value={data.text ?? ""}
134-
onChange={(e) => onType(e.target.value)}
135-
placeholder={
136-
data.fluxNodeType === FluxNodeType.User
137-
? "Write a poem about..."
138-
: data.fluxNodeType === FluxNodeType.System
139-
? "You are ChatGPT..."
140-
: undefined
141-
}
142-
/>
143-
) : (
144-
data.text
145-
)}
175+
<Column
176+
width="100%"
177+
marginRight="30px"
178+
whiteSpace="pre-wrap" // Preserve newlines.
179+
mainAxisAlignment="flex-start"
180+
crossAxisAlignment="flex-start"
181+
borderRadius="6px"
182+
wordBreak="break-word"
183+
onClick={isEditing ? undefined : () => setIsEditing(true)}
184+
>
185+
{isLast && isEditing ? (
186+
<TextareaAutosize
187+
id="promptBox"
188+
style={{
189+
width: "100%",
190+
backgroundColor: "transparent",
191+
outline: "none",
192+
}}
193+
value={data.text ?? ""}
194+
onChange={(e) => onType(e.target.value)}
195+
placeholder={
196+
data.fluxNodeType === FluxNodeType.User
197+
? "Write a poem about..."
198+
: data.fluxNodeType === FluxNodeType.System
199+
? "You are ChatGPT..."
200+
: undefined
201+
}
202+
/>
203+
) : (
204+
<TextAndCodeBlock text={data.text} />
205+
)}
206+
</Column>
146207
</>
147208
)}
148209
</Row>

src/components/utils/NavigationBar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export function NavigationBar({
190190

191191
<MenuGroup title="Copy">
192192
<MenuItem command="Ctrl+C" onClick={copyMessagesToClipboard}>
193-
Copy tree to clipboard
193+
Copy messages to clipboard
194194
</MenuItem>
195195
</MenuGroup>
196196
</MenuList>

0 commit comments

Comments
 (0)