Browse Source

feat: drag and drop in contextual prompts

legao 1 year ago
parent
commit
fb98050d9f
1 changed files with 101 additions and 53 deletions
  1. 101 53
      app/components/mask.tsx

+ 101 - 53
app/components/mask.tsx

@@ -42,6 +42,20 @@ import { ModelConfigList } from "./model-config";
 import { FileName, Path } from "../constant";
 import { BUILTIN_MASK_STORE } from "../masks";
 import { nanoid } from "nanoid";
+import {
+  DragDropContext,
+  Droppable,
+  Draggable,
+  OnDragEndResponder,
+} from "@hello-pangea/dnd";
+
+// drag and drop helper function
+function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
+  const result = [...list];
+  const [removed] = result.splice(startIndex, 1);
+  result.splice(endIndex, 0, removed);
+  return result;
+}
 
 export function MaskAvatar(props: { mask: Mask }) {
   return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
@@ -192,6 +206,7 @@ export function MaskConfig(props: {
 }
 
 function ContextPromptItem(props: {
+  index: number;
   prompt: ChatMessage;
   update: (prompt: ChatMessage) => void;
   remove: () => void;
@@ -199,53 +214,62 @@ function ContextPromptItem(props: {
   const [focusingInput, setFocusingInput] = useState(false);
 
   return (
-    <div className={chatStyle["context-prompt-row"]}>
-      {!focusingInput && (
-        <Select
-          value={props.prompt.role}
-          className={chatStyle["context-role"]}
-          onChange={(e) =>
-            props.update({
-              ...props.prompt,
-              role: e.target.value as any,
-            })
-          }
+    <Draggable draggableId={props.prompt.id} index={props.index}>
+      {(provided) => (
+        <div
+          className={chatStyle["context-prompt-row"]}
+          ref={provided.innerRef}
+          {...provided.draggableProps}
+          {...provided.dragHandleProps}
         >
-          {ROLES.map((r) => (
-            <option key={r} value={r}>
-              {r}
-            </option>
-          ))}
-        </Select>
-      )}
-      <Input
-        value={props.prompt.content}
-        type="text"
-        className={chatStyle["context-content"]}
-        rows={focusingInput ? 5 : 1}
-        onFocus={() => setFocusingInput(true)}
-        onBlur={() => {
-          setFocusingInput(false);
-          // If the selection is not removed when the user loses focus, some
-          // extensions like "Translate" will always display a floating bar
-          window?.getSelection()?.removeAllRanges();
-        }}
-        onInput={(e) =>
-          props.update({
-            ...props.prompt,
-            content: e.currentTarget.value as any,
-          })
-        }
-      />
-      {!focusingInput && (
-        <IconButton
-          icon={<DeleteIcon />}
-          className={chatStyle["context-delete-button"]}
-          onClick={() => props.remove()}
-          bordered
-        />
+          {!focusingInput && (
+            <Select
+              value={props.prompt.role}
+              className={chatStyle["context-role"]}
+              onChange={(e) =>
+                props.update({
+                  ...props.prompt,
+                  role: e.target.value as any,
+                })
+              }
+            >
+              {ROLES.map((r) => (
+                <option key={r} value={r}>
+                  {r}
+                </option>
+              ))}
+            </Select>
+          )}
+          <Input
+            value={props.prompt.content}
+            type="text"
+            className={chatStyle["context-content"]}
+            rows={focusingInput ? 5 : 1}
+            onFocus={() => setFocusingInput(true)}
+            onBlur={() => {
+              setFocusingInput(false);
+              // If the selection is not removed when the user loses focus, some
+              // extensions like "Translate" will always display a floating bar
+              window?.getSelection()?.removeAllRanges();
+            }}
+            onInput={(e) =>
+              props.update({
+                ...props.prompt,
+                content: e.currentTarget.value as any,
+              })
+            }
+          />
+          {!focusingInput && (
+            <IconButton
+              icon={<DeleteIcon />}
+              className={chatStyle["context-delete-button"]}
+              onClick={() => props.remove()}
+              bordered
+            />
+          )}
+        </div>
       )}
-    </div>
+    </Draggable>
   );
 }
 
@@ -267,17 +291,41 @@ export function ContextPrompts(props: {
     props.updateContext((context) => (context[i] = prompt));
   };
 
+  const onDragEnd: OnDragEndResponder = (result) => {
+    if (!result.destination) {
+      return;
+    }
+    const newContext = reorder(
+      context,
+      result.source.index,
+      result.destination.index,
+    );
+    props.updateContext((context) => {
+      context.splice(0, context.length, ...newContext);
+    });
+  };
+
   return (
     <>
       <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
-        {context.map((c, i) => (
-          <ContextPromptItem
-            key={i}
-            prompt={c}
-            update={(prompt) => updateContextPrompt(i, prompt)}
-            remove={() => removeContextPrompt(i)}
-          />
-        ))}
+        <DragDropContext onDragEnd={onDragEnd}>
+          <Droppable droppableId="context-prompt-list">
+            {(provided) => (
+              <div ref={provided.innerRef} {...provided.droppableProps}>
+                {context.map((c, i) => (
+                  <ContextPromptItem
+                    index={i}
+                    key={c.id}
+                    prompt={c}
+                    update={(prompt) => updateContextPrompt(i, prompt)}
+                    remove={() => removeContextPrompt(i)}
+                  />
+                ))}
+                {provided.placeholder}
+              </div>
+            )}
+          </Droppable>
+        </DragDropContext>
 
         <div className={chatStyle["context-prompt-row"]}>
           <IconButton