diff --git a/package.json b/package.json index 1e6388a..ff6ff0f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "react-dropzone": "^14.2.3", "react-redux": "^9.1.0", "react-scripts": "5.0.1", + "react-sortablejs": "^6.1.4", "sortable": "^2.0.0", + "sortablejs": "^1.15.2", "typescript": "^4.9.5", "uuid": "^9.0.1", "vite": "^5.1.4", @@ -61,6 +63,7 @@ "devDependencies": { "@testing-library/jest-dom": "^4.2.4", "@types/jest": "^27.5.2", + "@types/sortablejs": "^1.15.8", "@types/uuid": "^9.0.8", "jest-without-globals": "^0.0.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e88747c..90110c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,9 +83,15 @@ dependencies: react-scripts: specifier: 5.0.1 version: 5.0.1(@babel/plugin-syntax-flow@7.23.3)(@babel/plugin-transform-react-jsx@7.23.4)(eslint@8.56.0)(react@18.2.0)(typescript@4.9.5) + react-sortablejs: + specifier: ^6.1.4 + version: 6.1.4(@types/sortablejs@1.15.8)(react-dom@18.2.0)(react@18.2.0)(sortablejs@1.15.2) sortable: specifier: ^2.0.0 version: 2.0.0 + sortablejs: + specifier: ^1.15.2 + version: 1.15.2 typescript: specifier: ^4.9.5 version: 4.9.5 @@ -106,6 +112,9 @@ devDependencies: '@types/jest': specifier: ^27.5.2 version: 27.5.2 + '@types/sortablejs': + specifier: ^1.15.8 + version: 1.15.8 '@types/uuid': specifier: ^9.0.8 version: 9.0.8 @@ -3838,6 +3847,9 @@ packages: '@types/node': 16.18.82 dev: false + /@types/sortablejs@1.15.8: + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + /@types/stack-utils@1.0.1: resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==} dev: true @@ -5130,6 +5142,10 @@ packages: static-extend: 0.1.2 dev: true + /classnames@2.3.1: + resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==} + dev: false + /clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -11299,6 +11315,22 @@ packages: - webpack-plugin-serve dev: false + /react-sortablejs@6.1.4(@types/sortablejs@1.15.8)(react-dom@18.2.0)(react@18.2.0)(sortablejs@1.15.2): + resolution: {integrity: sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==} + peerDependencies: + '@types/sortablejs': '1' + react: '>=16.9.0' + react-dom: '>=16.9.0' + sortablejs: '1' + dependencies: + '@types/sortablejs': 1.15.8 + classnames: 2.3.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + sortablejs: 1.15.2 + tiny-invariant: 1.2.0 + dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -12064,6 +12096,10 @@ packages: observable: 1.3.1 dev: false + /sortablejs@1.15.2: + resolution: {integrity: sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==} + dev: false + /source-list-map@2.0.1: resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} dev: false @@ -12647,6 +12683,10 @@ packages: resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} dev: false + /tiny-invariant@1.2.0: + resolution: {integrity: sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==} + dev: false + /tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false diff --git a/src/App.tsx b/src/App.tsx index e528ca3..afca82c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import React, { ChangeEvent, useEffect } from 'react'; import './App.css'; import PromptComposer from './components/PromptComposer'; -import { Box, Tabs, Tab, Typography, Container } from '@material-ui/core'; +import { Box, Tabs, Tab, Typography, Container, Paper } from '@material-ui/core'; import { $composition, $library, $textComposition, Category, Composition, Library, LibraryItem, addItemToLibrary, insertIntoComposition, lassoNuggets } from './lib/prompt'; import { TextPrompt } from './components/TextPrompt'; import { useStore } from '@nanostores/react'; @@ -72,7 +72,7 @@ function App() { setValue(newValue); }; return ( - + @@ -85,7 +85,7 @@ function App() { - + ); } diff --git a/src/components/NewLibraryItem.tsx b/src/components/NewLibraryItem.tsx index b4f74d8..acaa970 100644 --- a/src/components/NewLibraryItem.tsx +++ b/src/components/NewLibraryItem.tsx @@ -64,16 +64,18 @@ export function NewLibraryItem(props: NewLibraryItemProps) { ))} - - Name - + {categoryHasName(category) && ( + + Name + + + ) + } Prompt .text, .nugget > .score, .nugget.buttons { - padding: 4pt; - display: inline-block; +.nugget.child > .text, .nugget.child > .score, .nugget.child.buttons { + padding: 4pt 2pt; + display: inline-flex; +} + +.nugget.toplevel { + align-items: center; } .nugget .buttons button { - max-height: 12pt; + max-height: 14pt; } \ No newline at end of file diff --git a/src/components/Nugget.test.tsx b/src/components/Nugget.test.tsx index ac4c9e1..753a649 100644 --- a/src/components/Nugget.test.tsx +++ b/src/components/Nugget.test.tsx @@ -14,7 +14,8 @@ const nugget: NuggetType = { }; test('renders Nugget component', () => { - render(); + render( {}} />); const textElement = screen.getByText(nugget.item.prompt); expect(textElement).toBeInTheDocument(); }); @@ -25,6 +26,7 @@ test('increases score when button is clicked', () => { const { rerender } = render( {}} /> ); const increaseButton = screen.getByLabelText('incScore'); @@ -32,6 +34,7 @@ test('increases score when button is clicked', () => { rerender( {}} /> ); // expect(increaseScore).toHaveBeenCalledTimes(1); @@ -44,6 +47,7 @@ test('decreases score when button is clicked', () => { const { rerender } = render( {}} /> ); const decreaseButton = screen.getByLabelText('decScore'); @@ -51,6 +55,7 @@ test('decreases score when button is clicked', () => { rerender( {}} /> ); // expect(decreaseScore).toHaveBeenCalledTimes(1); diff --git a/src/components/Nugget.tsx b/src/components/Nugget.tsx index bb78d17..89bf2fc 100644 --- a/src/components/Nugget.tsx +++ b/src/components/Nugget.tsx @@ -1,14 +1,13 @@ import { Button, ButtonGroup, Chip, Divider } from '@material-ui/core'; import React, { Component, DragEvent, useEffect, useState } from 'react'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowUp'; +import {KeyboardArrowUp, KeyboardArrowDown} from '@mui/icons-material'; import { $composition, Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore, togglePromptItemMute } from '../lib/prompt'; import "./Nugget.css"; import "./PromptItem.css" import { $sourceItem, cancelDrop, completeDrop, isPromptItemDropTarget, startDrag, startHoverOver } from '../store/prompt-dnd'; import { PromptItemProps } from './PromptItem'; import { useStore } from '@nanostores/react'; -import { VolumeMute, VolumeOff, VolumeUp } from '@mui/icons-material'; +import { ArrowDownward, Delete, TextDecrease, VolumeMute, VolumeOff, VolumeUp } from '@mui/icons-material'; export interface NuggetProps extends PromptItemProps { nugget: NuggetType, @@ -19,8 +18,10 @@ export default function Nugget(props: NuggetProps) { const { nugget, onDragStart, + onDragEnd, onMouseLeave, isTopLevel, + onDelete, } = props; const scoreDisp = nugget.score > 0 ? "+" + nugget.score : nugget.score; @@ -37,7 +38,12 @@ export default function Nugget(props: NuggetProps) { console.log("nugget classname: %s", className); - const handleOnDragStart = () => { + const handleOnDragStart = (e: DragEvent) => { + if ("checkForDrag" in window) { + if (Math.abs((window.checkForDrag as number) - e.clientX) < 5) { + return e.stopPropagation(); + } + } onDragStart ? onDragStart(nugget) : null; } @@ -68,11 +74,29 @@ export default function Nugget(props: NuggetProps) { } const handleOnDragEnd = () => { - completeDrop(); + if (onDragEnd) onDragEnd(); + } + + const handleDelete = () => { + onDelete(nugget); + } + + const mouseDownCoords = (e: MouseEvent) => { + (window as any).checkForDrag = e.clientX; + } + + const handleIncClick = () => { + console.log("decrease %s", nugget.id); + increaseNuggetScore(nugget.id); + } + + const handleDecClick = () => { + console.log("increase %s", nugget.id); + decreaseNuggetScore(nugget.id); } return ( -
+ {isTopLevel && ( + + ) + } {nugget.item.name || nugget.item.prompt} {scoreDisp} - - {isTopLevel && } -
+ ); } \ No newline at end of file diff --git a/src/components/Operation.css b/src/components/Operation.css index de0838a..6406e24 100644 --- a/src/components/Operation.css +++ b/src/components/Operation.css @@ -1,14 +1,20 @@ .operation { - display: inline-block; border: 1px solid lightgray; } +.operation .delete { + display: inline-block; +} + .operation .title { - text-align: left; + position: absolute; + background-color: black; + color: white; + padding: 2pt 5pt; + transform: translate(10px, -40px) } .operation .nuggets { - display: inline-flex; border-style: solid; border-radius: 10pt; } @@ -26,4 +32,8 @@ .operation.blended .nuggets, .operation.blend .nuggets { background-color: #a1af86; border-color: #58663d; +} + +.op-icon { + padding-top: 20pt; } \ No newline at end of file diff --git a/src/components/Operation.tsx b/src/components/Operation.tsx index 09ec87b..140c492 100644 --- a/src/components/Operation.tsx +++ b/src/components/Operation.tsx @@ -3,19 +3,19 @@ import React, { Children, DragEvent, ReactNode, useEffect } from 'react'; import "./Operation.css"; import { Op } from "../lib/operator"; import { v4 as randomUUID } from "uuid"; -import { $composition, Operation as OperationType, changeOperationOp, togglePromptItemMute } from "../lib/prompt"; +import { $composition, Category, Operation as OperationType, changeOperationOp, togglePromptItemMute, unlassooOperation } from "../lib/prompt"; import Nugget from "./Nugget"; import { PromptItemProps } from "./PromptItem"; import { useStore } from "@nanostores/react"; import { $sourceItem, cancelDrop, completeDrop, startHoverOver } from "../store/prompt-dnd"; -import { VolumeUp, VolumeOff } from "@mui/icons-material"; +import { VolumeUp, VolumeOff, Delete, Add, RotateLeftOutlined, Shuffle, Repeat, ArrowOutward } from "@mui/icons-material"; interface OperationProps extends PromptItemProps { operation: OperationType } function Operation(props: OperationProps) { - const { operation, onDragStart, onDragOver, onDragEnd, onDrop, onMouseEnter, onMouseLeave } = props; + const { operation, onDragStart, onDragOver, onDragEnd, onDrop, onMouseEnter, onDelete, onMouseLeave } = props; const [contextMenu, setContextMenu] = React.useState<{ mouseX: number; @@ -59,7 +59,7 @@ function Operation(props: OperationProps) { } const handleOnDragEnd = () => { - completeDrop(); + if (onDragEnd) onDragEnd(); } const handleContextMenu = (event: React.MouseEvent) => { event.preventDefault(); @@ -80,6 +80,10 @@ function Operation(props: OperationProps) { setContextMenu(null); }; + const handleDelete = () => { + onDelete(operation); + } + const changeOperator = (opV: string) => { changeOperationOp(operation.id, opV as Op); handleClose(); @@ -94,9 +98,23 @@ function Operation(props: OperationProps) { console.log("operation classname: %s", className); + const handleUngroup = () => { + unlassooOperation(operation); + } + + const getCategoryIcon = () => { + return { + [Op.AND]: (), + [Op.JOINED]: (), + [Op.SWAP]: (), + [Op.SWAPPED]: (), + [Op.BLEND]: (), + [Op.BLENDED]: (), + }[operation.op]; + } return ( -
+ + +
{operation.op}
{ - operation.items.map(nugget => { - return + operation.items.map((nugget, i) => { + return ( + <> + { }} /> + {i < operation.items.length-1 && ({getCategoryIcon()})} + + ) }) }
@@ -135,8 +163,11 @@ function Operation(props: OperationProps) { changeOperator(v)}>{v} ) })} + + Ungroup + -
+ ); } diff --git a/src/components/PromptComposer.css b/src/components/PromptComposer.css index 0cf0912..284e8ae 100644 --- a/src/components/PromptComposer.css +++ b/src/components/PromptComposer.css @@ -1,11 +1,5 @@ -.add-button { - position: absolute; - right: 10pt; - top: 10pt; -} - -.prompt-item { - margin: 4pt; - padding: 2pt; - border-radius: 5pt; +.composer-main { + padding-top: 30pt; + border-top: 1px solid black; + margin: 10pt; } \ No newline at end of file diff --git a/src/components/PromptComposer.tsx b/src/components/PromptComposer.tsx index e70e7f8..52670c2 100644 --- a/src/components/PromptComposer.tsx +++ b/src/components/PromptComposer.tsx @@ -1,17 +1,18 @@ -import { Button } from '@material-ui/core'; +import { Box, Button, ButtonGroup, Container, Paper, Snackbar, Typography } from '@material-ui/core'; import Masonry from '@mui/lab/Masonry'; import AddIcon from '@mui/icons-material/Add'; import "./PromptComposer.css"; import { PromptLibrary } from './PromptLibrary'; import React, { useEffect, useState } from 'react'; -import { $composition, $library, $slottedComposition, LibraryItem, PromptItem, addToOperation, insertIntoComposition, itemIsNugget, itemIsOperation, lassoNuggets, Composition } from '../lib/prompt'; -import { Category } from '@mui/icons-material'; +import { $composition, $library, $slottedComposition, LibraryItem, PromptItem, addToOperation, insertIntoComposition, itemIsNugget, itemIsOperation, lassoNuggets, Composition, _setComposition, removeItemFromLibrary, removeFromComposition, removeNuggetFromOperation } from '../lib/prompt'; +import { BackHand, Book, Category, DragHandle, LibraryBooks, MouseSharp, Score, Sort } from '@mui/icons-material'; import { useStore } from '@nanostores/react' import Nugget from './Nugget'; -import { Stack } from '@mui/material'; +import { Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'; import { Op, Operation } from './Operation'; -import { PromptItemProps } from './PromptItem'; -import { $dragDropState, $dropCandidate, $isDragInProgress, $sourceItem, completeDrop, endHoverOver, startDrag, startHoverOver } from '../store/prompt-dnd'; +import { EditorMode, PromptItemProps } from './PromptItem'; +import { ReactSortable } from "react-sortablejs"; +import { $dragDropState, $dropCandidate, $isDragInProgress, $sourceItem, CategoryMismatchError, completeDrop, endHoverOver, startDrag, startHoverOver } from '../store/prompt-dnd'; export interface PromptComposerProps { @@ -21,11 +22,11 @@ export default function PromptComposer(props: PromptComposerProps) { const [open, setOpen] = useState(false); const handleClickOpen = () => { - setOpen(true); + if (!open) { setOpen(true); } }; const handleClose = () => { - setOpen(false); + if (open) { setOpen(false); } }; const composition = useStore($composition); @@ -38,6 +39,31 @@ export default function PromptComposer(props: PromptComposerProps) { console.log(composition) } + const handleOnDeleteItem = (item: LibraryItem) => { + removeItemFromLibrary(item); + // also remove from the prompts + composition.filter(c => { + return (!("op" in c)) && c.item.id === item.id + }).forEach((c) => { + removeFromComposition(c); + }); + // and from any operation + composition.filter(c => { + return "op" in c && c.items.find(o => o.item.id === item.id); + }).forEach((o) => { + if (!("op" in o)) return; + o.items.forEach((i) => { + if (i.item.id === item.id) removeNuggetFromOperation(o, i); + }) + }) + } + + const [doSort, setDoSort] = useState(false); + + const [editMode, setEditMode] = useState("dnd" as EditorMode); + + const [error, setError] = useState(null as Error | null); + /** * * @param promptItem The prompt item that we're rendering @@ -51,12 +77,14 @@ export default function PromptComposer(props: PromptComposerProps) { // is either a Nugget or an Operation. const callbacks = { onDragStart: (item: PromptItem) => { + if (editMode !== "dnd") return; if (itemIsNugget(promptItem)) { startDrag(item); } // TODO: operation }, onDrop: (item: PromptItem) => { + if (editMode !== "dnd") return; const dnd = useStore($dragDropState); const isDragInProgress = useStore($isDragInProgress); const dropCandidate = useStore($dropCandidate); @@ -73,14 +101,27 @@ export default function PromptComposer(props: PromptComposerProps) { } completeDrop(); }, - onDragEnd: (item: PromptItem) => { - + onDragEnd: () => { + try { + completeDrop(); + } catch (err) { + if (err instanceof CategoryMismatchError) { + setError(err); + } else { + throw err; + } + } }, onMouseEnter: (item: PromptItem) => { }, onMouseLeave: (item: PromptItem) => { + if (editMode !== "dnd") return; endHoverOver(); }, + onDelete: (item: PromptItem) => { + if (editMode !== "dnd") return; + removeFromComposition(item); + }, } as PromptItemProps; return ("op" in promptItem ? @@ -88,23 +129,73 @@ export default function PromptComposer(props: PromptComposerProps) { : ) } + function setComposition(c: Composition) { + console.log("updated composition: %x", c) + _setComposition(c); + } + + const handleSetEditMode = ( + event: React.MouseEvent, + mode: EditorMode | null, + ) => { + if (mode) setEditMode(mode); + }; + + const handleErrorSnackbarClose = () => { + setError(null); + } + + return ( -
-
- -
-
- { - composition.map(c => promptItemFactory(c, `item-${c.id}`)) + + + + + + + + + + + + + + + + {editMode} Mode enabled + + + + + {editMode == "sort" ? ( + + {composition.map(c => promptItemFactory(c, `item-${c.id}`))} + + ) : composition.map(c => promptItemFactory(c, `item-${c.id}`)) } -
-
+ + + + ); } \ No newline at end of file diff --git a/src/components/PromptItem.css b/src/components/PromptItem.css index 373afc7..37b13d5 100644 --- a/src/components/PromptItem.css +++ b/src/components/PromptItem.css @@ -7,5 +7,17 @@ } .prompt-item.muted, .operation.muted .nugget { - color: gray; + opacity: 0.4; +} + +.prompt-item { + display: inline-flex; + border-radius: 5pt; + padding: 2pt 0pt; + margin: 2pt 4pt; + align-items: center; +} + +.prompt-item.toplevel { + height: 55px; } \ No newline at end of file diff --git a/src/components/PromptItem.tsx b/src/components/PromptItem.tsx index 15f5bad..e02c636 100644 --- a/src/components/PromptItem.tsx +++ b/src/components/PromptItem.tsx @@ -14,9 +14,12 @@ import {PromptItem as PIType} from "../lib/prompt" export interface PromptItemProps { onDragStart?: (item : PIType) => void, - onDragEnd?: (item: PIType) => void, + onDragEnd?: () => void, onDragOver?: (item: PIType) => void, onDrop?: (item : PIType) => void, onMouseEnter?: (item : PIType) => void, onMouseLeave?: (item : PIType) => void, -} \ No newline at end of file + onDelete : (item : PIType) => void, +} + +export type EditorMode = "dnd" | "sort" | "score" \ No newline at end of file diff --git a/src/components/PromptLibrary.test.tsx b/src/components/PromptLibrary.test.tsx index 6fa3267..b772243 100644 --- a/src/components/PromptLibrary.test.tsx +++ b/src/components/PromptLibrary.test.tsx @@ -25,6 +25,7 @@ const mockItem: LibItemType = { }; const mockOnClose = jest.fn(); +const mockOnDeleteItem = jest.fn(); const mockOpen: boolean = true; @@ -32,6 +33,7 @@ const mockProps: SimpleDialogProps = { open: mockOpen, onInsertItem: mockOnAddItem, onClose: mockOnClose, + onDeleteItem: mockOnDeleteItem, }; const mockLibrary: LibraryType = [ diff --git a/src/components/PromptLibrary.tsx b/src/components/PromptLibrary.tsx index e170422..4a0063d 100644 --- a/src/components/PromptLibrary.tsx +++ b/src/components/PromptLibrary.tsx @@ -1,4 +1,4 @@ -import { Button, Dialog, DialogTitle } from "@material-ui/core"; +import { Button, Dialog, DialogActions, DialogTitle } from "@material-ui/core"; import { LibraryItem as LibItemType, $library, Category, LibraryItem, insertIntoComposition } from "../lib/prompt"; import { MouseEvent, useMemo, useState } from "react"; import { useStore } from "@nanostores/react"; @@ -6,18 +6,19 @@ import { NewLibraryItem } from "./NewLibraryItem"; import { DataGrid, GridApi, GridColDef, GridColTypeDef } from '@mui/x-data-grid'; import "./PromptLibrary.css" import { title } from "../lib/util"; -import { Add } from "@mui/icons-material"; +import { Add, Delete } from "@mui/icons-material"; export interface SimpleDialogProps { open: boolean; onClose: () => void, // onAddItem: (item: LibItemType) => void, onInsertItem: (item: LibItemType) => void, + onDeleteItem: (item: LibItemType) => void, } export function PromptLibrary(props: SimpleDialogProps) { - const { open, onInsertItem, onClose } = props; + const { open, onInsertItem, onClose, onDeleteItem } = props; const library = useStore($library); @@ -54,11 +55,23 @@ export function PromptLibrary(props: SimpleDialogProps) { ); - } + }, headerName: "", }, { field: 'name', headerName: 'Name', width: 150 }, { field: 'prompt', headerName: 'Prompt', width: 250 }, { field: 'category', headerName: 'Category', width: 150 }, + { field: "delete", headerName: "headerName", width: 50, renderCell: (params) => { + const handleClick = ($e: MouseEvent) => { + $e.stopPropagation(); + const libItem = library.find(l => l.id === params.id) as LibItemType; + if (libItem) onDeleteItem(libItem); + } + return ( + + ); + } }, ]; const rows = filteredLibrary.map(item => ({ @@ -72,16 +85,22 @@ export function PromptLibrary(props: SimpleDialogProps) { Prompt Library
- +
+ + +
); } \ No newline at end of file diff --git a/src/lib/prompt.tsx b/src/lib/prompt.tsx index caa27c7..61084fb 100644 --- a/src/lib/prompt.tsx +++ b/src/lib/prompt.tsx @@ -1,6 +1,6 @@ import { v4 as randomUUID, v4 as uuidv4 } from "uuid"; import { Op } from "./operator"; -import { Atom, ReadableAtom, Store, WritableAtom, atom, computed } from "nanostores"; +import { atom, computed } from "nanostores"; type Id = string; @@ -73,7 +73,7 @@ export function removeItemFromLibrary(item: LibraryItem) { $library.set($library.get().filter(i => (i.id !== item.id))); } -export const $composition = atom([]) +export const $composition = atom([]); export function insertIntoComposition(item: LibraryItem) { $composition.set([ @@ -91,8 +91,19 @@ export function removeFromComposition(item: PromptItem) { function nuggetDelta(nuggetId: Id, delta: number) { $composition.set($composition.get().map(item => { if ((item.id === nuggetId) && ("score" in item)) { - const o = { ...item, score: item.score + delta }; - return o; + return { ...item, score: item.score + delta }; + } + if ("op" in item) { + return { + ...item, items: item.items.map( + nug => { + return { + ...nug, + score: nug.score + (nuggetId === nug.id ? delta : 0), + } + } + ) + } } return item; } @@ -227,4 +238,29 @@ export function togglePromptItemMute(id: Id) { } : c; } )); +} + +export function _setComposition(newComp: Composition) { + $composition.set(newComp); +} + +export function removeNuggetFromOperation(operation: Operation, nugget: Nugget) { + $composition.set($composition.get().map(item => { + return "op" in item ? { + ...item, + items: item.items.filter(i => i.id !== nugget.id), + } : item; + })) +}; + +export function unlassooOperation(operation: Operation) { + $composition.set( + $composition.get().flatMap(item => { + if ("op" in item && item.id === operation.id) { + return item.items; + } else { + return [item]; + } + }) + ) } \ No newline at end of file diff --git a/src/store/prompt-dnd.tsx b/src/store/prompt-dnd.tsx index 91a5fad..7d73c09 100644 --- a/src/store/prompt-dnd.tsx +++ b/src/store/prompt-dnd.tsx @@ -1,5 +1,5 @@ import { Atom, atom, computed } from "nanostores" -import { $composition, Nugget, Operation, PromptItem, addToOperation, itemIsNugget, itemIsOperation, lassoNuggets } from "../lib/prompt"; +import { $composition, Category, Nugget, Operation, PromptItem, addToOperation, itemIsNugget, itemIsOperation, lassoNuggets } from "../lib/prompt"; import { Op } from "../lib/operator"; export type DropCandidate = string | string [] @@ -55,6 +55,12 @@ export function cancelDrop() { $dragDropState.set({}); }; +export class CategoryMismatchError extends Error { + constructor(public c1 : Category, public c2 : Category) { + super(`Cannot merge '${c1}' into '${c2}'`); + } +} + export function completeDrop() { const source = $sourceItem.get(); const target = $dropCandidate.get(); @@ -65,7 +71,7 @@ export function completeDrop() { const c1 = nSource.item.category; const c2 = nTarget.items[0].item.category; if (c1 != c2) { - console.error("Category mismatch: cannot drop a %s into %s", c1, c2); + throw new CategoryMismatchError(c1, c2); } else { addToOperation(source.id, target.id); } @@ -76,7 +82,7 @@ export function completeDrop() { const c1 = nSource.item.category; const c2 = nTarget.item.category; if (c1 != c2) { - console.error("Category mismatch: cannot drop a %s into %s", c1, c2); + throw new CategoryMismatchError(c1, c2); } else { lassoNuggets(source.id, target.id, Op.AND) }