|
@@ -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
|