pretty much have it to a working state now.

This commit is contained in:
Jordan 2024-03-02 14:20:17 -08:00
parent 6da57d63bb
commit 4af0874b8f
17 changed files with 381 additions and 93 deletions

View File

@ -28,7 +28,9 @@
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-sortablejs": "^6.1.4",
"sortable": "^2.0.0", "sortable": "^2.0.0",
"sortablejs": "^1.15.2",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vite": "^5.1.4", "vite": "^5.1.4",
@ -61,6 +63,7 @@
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"jest-without-globals": "^0.0.3" "jest-without-globals": "^0.0.3"
}, },

View File

@ -83,9 +83,15 @@ dependencies:
react-scripts: react-scripts:
specifier: 5.0.1 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) 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: sortable:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
sortablejs:
specifier: ^1.15.2
version: 1.15.2
typescript: typescript:
specifier: ^4.9.5 specifier: ^4.9.5
version: 4.9.5 version: 4.9.5
@ -106,6 +112,9 @@ devDependencies:
'@types/jest': '@types/jest':
specifier: ^27.5.2 specifier: ^27.5.2
version: 27.5.2 version: 27.5.2
'@types/sortablejs':
specifier: ^1.15.8
version: 1.15.8
'@types/uuid': '@types/uuid':
specifier: ^9.0.8 specifier: ^9.0.8
version: 9.0.8 version: 9.0.8
@ -3838,6 +3847,9 @@ packages:
'@types/node': 16.18.82 '@types/node': 16.18.82
dev: false dev: false
/@types/sortablejs@1.15.8:
resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==}
/@types/stack-utils@1.0.1: /@types/stack-utils@1.0.1:
resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==} resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==}
dev: true dev: true
@ -5130,6 +5142,10 @@ packages:
static-extend: 0.1.2 static-extend: 0.1.2
dev: true dev: true
/classnames@2.3.1:
resolution: {integrity: sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==}
dev: false
/clean-css@5.3.3: /clean-css@5.3.3:
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
engines: {node: '>= 10.0'} engines: {node: '>= 10.0'}
@ -11299,6 +11315,22 @@ packages:
- webpack-plugin-serve - webpack-plugin-serve
dev: false 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): /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies: peerDependencies:
@ -12064,6 +12096,10 @@ packages:
observable: 1.3.1 observable: 1.3.1
dev: false dev: false
/sortablejs@1.15.2:
resolution: {integrity: sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==}
dev: false
/source-list-map@2.0.1: /source-list-map@2.0.1:
resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==}
dev: false dev: false
@ -12647,6 +12683,10 @@ packages:
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
dev: false dev: false
/tiny-invariant@1.2.0:
resolution: {integrity: sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==}
dev: false
/tiny-warning@1.0.3: /tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
dev: false dev: false

View File

@ -1,7 +1,7 @@
import React, { ChangeEvent, useEffect } from 'react'; import React, { ChangeEvent, useEffect } from 'react';
import './App.css'; import './App.css';
import PromptComposer from './components/PromptComposer'; 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 { $composition, $library, $textComposition, Category, Composition, Library, LibraryItem, addItemToLibrary, insertIntoComposition, lassoNuggets } from './lib/prompt';
import { TextPrompt } from './components/TextPrompt'; import { TextPrompt } from './components/TextPrompt';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
@ -72,7 +72,7 @@ function App() {
setValue(newValue); setValue(newValue);
}; };
return ( return (
<Container> <Paper>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="prompt-selection-tabs"> <Tabs value={value} onChange={handleChange} aria-label="prompt-selection-tabs">
<Tab label="Prompt Composer" {...a11yProps(0)} aria-label="prompt-composer-tab" /> <Tab label="Prompt Composer" {...a11yProps(0)} aria-label="prompt-composer-tab" />
@ -85,7 +85,7 @@ function App() {
<CustomTabPanel value={value} index={1}> <CustomTabPanel value={value} index={1}>
<TextPrompt /> <TextPrompt />
</CustomTabPanel> </CustomTabPanel>
</Container> </Paper>
); );
} }

View File

@ -64,6 +64,7 @@ export function NewLibraryItem(props: NewLibraryItemProps) {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
{categoryHasName(category) && (
<FormControl> <FormControl>
<InputLabel id="name-textfield-label">Name</InputLabel> <InputLabel id="name-textfield-label">Name</InputLabel>
<TextField <TextField
@ -71,9 +72,10 @@ export function NewLibraryItem(props: NewLibraryItemProps) {
id="name-textfield" id="name-textfield"
value={name} value={name}
onChange={handleNameChange} onChange={handleNameChange}
hidden={category === Category.subject || category === Category.medium}
/> />
</FormControl> </FormControl>
)
}
<FormControl> <FormControl>
<InputLabel id="prompt-textfield-label">Prompt</InputLabel> <InputLabel id="prompt-textfield-label">Prompt</InputLabel>
<TextField <TextField

View File

@ -9,11 +9,15 @@
display: inline-flex; display: inline-flex;
} }
.nugget > .text, .nugget > .score, .nugget.buttons { .nugget.child > .text, .nugget.child > .score, .nugget.child.buttons {
padding: 4pt; padding: 4pt 2pt;
display: inline-block; display: inline-flex;
}
.nugget.toplevel {
align-items: center;
} }
.nugget .buttons button { .nugget .buttons button {
max-height: 12pt; max-height: 14pt;
} }

View File

@ -14,7 +14,8 @@ const nugget: NuggetType = {
}; };
test('renders Nugget component', () => { test('renders Nugget component', () => {
render(<Nugget nugget={nugget} />); render(<Nugget nugget={nugget}
onDelete={i => {}} />);
const textElement = screen.getByText(nugget.item.prompt); const textElement = screen.getByText(nugget.item.prompt);
expect(textElement).toBeInTheDocument(); expect(textElement).toBeInTheDocument();
}); });
@ -25,6 +26,7 @@ test('increases score when button is clicked', () => {
const { rerender } = render( const { rerender } = render(
<Nugget <Nugget
nugget={nugget} nugget={nugget}
onDelete={i => {}}
/> />
); );
const increaseButton = screen.getByLabelText('incScore'); const increaseButton = screen.getByLabelText('incScore');
@ -32,6 +34,7 @@ test('increases score when button is clicked', () => {
rerender( rerender(
<Nugget <Nugget
nugget={{ ...nugget, score: nugget.score + 1 }} nugget={{ ...nugget, score: nugget.score + 1 }}
onDelete={i => {}}
/> />
); );
// expect(increaseScore).toHaveBeenCalledTimes(1); // expect(increaseScore).toHaveBeenCalledTimes(1);
@ -44,6 +47,7 @@ test('decreases score when button is clicked', () => {
const { rerender } = render( const { rerender } = render(
<Nugget <Nugget
nugget={nugget} nugget={nugget}
onDelete={i => {}}
/> />
); );
const decreaseButton = screen.getByLabelText('decScore'); const decreaseButton = screen.getByLabelText('decScore');
@ -51,6 +55,7 @@ test('decreases score when button is clicked', () => {
rerender( rerender(
<Nugget <Nugget
nugget={{ ...nugget, score: nugget.score - 1 }} nugget={{ ...nugget, score: nugget.score - 1 }}
onDelete={i => {}}
/> />
); );
// expect(decreaseScore).toHaveBeenCalledTimes(1); // expect(decreaseScore).toHaveBeenCalledTimes(1);

View File

@ -1,14 +1,13 @@
import { Button, ButtonGroup, Chip, Divider } from '@material-ui/core'; import { Button, ButtonGroup, Chip, Divider } from '@material-ui/core';
import React, { Component, DragEvent, useEffect, useState } from 'react'; import React, { Component, DragEvent, useEffect, useState } from 'react';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import {KeyboardArrowUp, KeyboardArrowDown} from '@mui/icons-material';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowUp';
import { $composition, Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore, togglePromptItemMute } from '../lib/prompt'; import { $composition, Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore, togglePromptItemMute } from '../lib/prompt';
import "./Nugget.css"; import "./Nugget.css";
import "./PromptItem.css" import "./PromptItem.css"
import { $sourceItem, cancelDrop, completeDrop, isPromptItemDropTarget, startDrag, startHoverOver } from '../store/prompt-dnd'; import { $sourceItem, cancelDrop, completeDrop, isPromptItemDropTarget, startDrag, startHoverOver } from '../store/prompt-dnd';
import { PromptItemProps } from './PromptItem'; import { PromptItemProps } from './PromptItem';
import { useStore } from '@nanostores/react'; 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 { export interface NuggetProps extends PromptItemProps {
nugget: NuggetType, nugget: NuggetType,
@ -19,8 +18,10 @@ export default function Nugget(props: NuggetProps) {
const { nugget, const { nugget,
onDragStart, onDragStart,
onDragEnd,
onMouseLeave, onMouseLeave,
isTopLevel, isTopLevel,
onDelete,
} = props; } = props;
const scoreDisp = nugget.score > 0 ? "+" + nugget.score : nugget.score; 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); 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; onDragStart ? onDragStart(nugget) : null;
} }
@ -68,11 +74,29 @@ export default function Nugget(props: NuggetProps) {
} }
const handleOnDragEnd = () => { 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 ( return (
<div <li
className={className} className={className}
id={thisId} id={thisId}
draggable draggable
@ -83,26 +107,32 @@ export default function Nugget(props: NuggetProps) {
onMouseOut={handleOnMouseLeave} onMouseOut={handleOnMouseLeave}
data-promptitem-id={nugget.id} data-promptitem-id={nugget.id}
> >
{isTopLevel && (<span className='delete'>
<Button onClick={handleDelete}>
<Delete />
</Button>
</span>)
}
<span className='text'>{nugget.item.name || nugget.item.prompt}</span> <span className='text'>{nugget.item.name || nugget.item.prompt}</span>
<Divider orientation="vertical" variant="middle" flexItem /> <Divider orientation="vertical" variant="middle" flexItem />
<span className='score'>{scoreDisp}</span> <span className='score'>{scoreDisp}</span>
<span className='buttons'> <span className='buttons'>
<ButtonGroup size="small" orientation='vertical'> <ButtonGroup size="small" orientation='vertical'>
<Button onClick={() => increaseNuggetScore(nugget.id)} className='incScore' aria-label="incScore"> <Button onClick={handleIncClick} className='incScore' aria-label="incScore">
<KeyboardArrowUpIcon /> <KeyboardArrowUp />
</Button> </Button>
<Button onClick={() => decreaseNuggetScore(nugget.id)} className='decScore' aria-label='decScore'> <Button onClick={handleDecClick} className='decScore' aria-label='decScore'>
<KeyboardArrowDownIcon /> <KeyboardArrowDown />
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</span> </span>
{isTopLevel && {isTopLevel &&
<span className='hide'> <span className='hide'>
<Button onClick={() => togglePromptItemMute(nugget.id)}> <Button onClick={() => togglePromptItemMute(nugget.id)}>
{nugget.muted ? <VolumeUp /> : <VolumeOff /> } {nugget.muted ? <VolumeUp /> : <VolumeOff />}
</Button> </Button>
</span> </span>
} }
</div> </li>
); );
} }

View File

@ -1,14 +1,20 @@
.operation { .operation {
display: inline-block;
border: 1px solid lightgray; border: 1px solid lightgray;
} }
.operation .delete {
display: inline-block;
}
.operation .title { .operation .title {
text-align: left; position: absolute;
background-color: black;
color: white;
padding: 2pt 5pt;
transform: translate(10px, -40px)
} }
.operation .nuggets { .operation .nuggets {
display: inline-flex;
border-style: solid; border-style: solid;
border-radius: 10pt; border-radius: 10pt;
} }
@ -27,3 +33,7 @@
background-color: #a1af86; background-color: #a1af86;
border-color: #58663d; border-color: #58663d;
} }
.op-icon {
padding-top: 20pt;
}

View File

@ -3,19 +3,19 @@ import React, { Children, DragEvent, ReactNode, useEffect } from 'react';
import "./Operation.css"; import "./Operation.css";
import { Op } from "../lib/operator"; import { Op } from "../lib/operator";
import { v4 as randomUUID } from "uuid"; 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 Nugget from "./Nugget";
import { PromptItemProps } from "./PromptItem"; import { PromptItemProps } from "./PromptItem";
import { useStore } from "@nanostores/react"; import { useStore } from "@nanostores/react";
import { $sourceItem, cancelDrop, completeDrop, startHoverOver } from "../store/prompt-dnd"; 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 { interface OperationProps extends PromptItemProps {
operation: OperationType operation: OperationType
} }
function Operation(props: OperationProps) { 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<{ const [contextMenu, setContextMenu] = React.useState<{
mouseX: number; mouseX: number;
@ -59,7 +59,7 @@ function Operation(props: OperationProps) {
} }
const handleOnDragEnd = () => { const handleOnDragEnd = () => {
completeDrop(); if (onDragEnd) onDragEnd();
} }
const handleContextMenu = (event: React.MouseEvent) => { const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
@ -80,6 +80,10 @@ function Operation(props: OperationProps) {
setContextMenu(null); setContextMenu(null);
}; };
const handleDelete = () => {
onDelete(operation);
}
const changeOperator = (opV: string) => { const changeOperator = (opV: string) => {
changeOperationOp(operation.id, opV as Op); changeOperationOp(operation.id, opV as Op);
handleClose(); handleClose();
@ -94,9 +98,23 @@ function Operation(props: OperationProps) {
console.log("operation classname: %s", className); console.log("operation classname: %s", className);
const handleUngroup = () => {
unlassooOperation(operation);
}
const getCategoryIcon = () => {
return {
[Op.AND]: (<Add />),
[Op.JOINED]: (<Add />),
[Op.SWAP]: (<Shuffle />),
[Op.SWAPPED]: (<Shuffle />),
[Op.BLEND]: (<Repeat />),
[Op.BLENDED]: (<Repeat />),
}[operation.op];
}
return ( return (
<div <li
draggable draggable
onDragStart={handleOnDragStart} onDragStart={handleOnDragStart}
onDragEnd={handleOnDragEnd} onDragEnd={handleOnDragEnd}
@ -107,11 +125,21 @@ function Operation(props: OperationProps) {
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
data-promptitem-id={operation.id} data-promptitem-id={operation.id}
> >
<span className='delete'>
<Button onClick={handleDelete}>
<Delete />
</Button>
</span>
<div className="title">{operation.op}</div> <div className="title">{operation.op}</div>
<div className="nuggets"> <div className="nuggets">
{ {
operation.items.map(nugget => { operation.items.map((nugget, i) => {
return <Nugget nugget={nugget} isTopLevel={false} /> return (
<>
<Nugget nugget={nugget} isTopLevel={false} onDelete={i => { }} />
{i < operation.items.length-1 && (<span className="op-icon">{getCategoryIcon()}</span>)}
</>
)
}) })
} }
</div> </div>
@ -135,8 +163,11 @@ function Operation(props: OperationProps) {
<MenuItem onClick={() => changeOperator(v)}>{v}</MenuItem> <MenuItem onClick={() => changeOperator(v)}>{v}</MenuItem>
) )
})} })}
<MenuItem onClick={handleUngroup}>
Ungroup
</MenuItem>
</Menu> </Menu>
</div> </li>
); );
} }

View File

@ -1,11 +1,5 @@
.add-button { .composer-main {
position: absolute; padding-top: 30pt;
right: 10pt; border-top: 1px solid black;
top: 10pt; margin: 10pt;
}
.prompt-item {
margin: 4pt;
padding: 2pt;
border-radius: 5pt;
} }

View File

@ -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 Masonry from '@mui/lab/Masonry';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import "./PromptComposer.css"; import "./PromptComposer.css";
import { PromptLibrary } from './PromptLibrary'; import { PromptLibrary } from './PromptLibrary';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { $composition, $library, $slottedComposition, LibraryItem, PromptItem, addToOperation, insertIntoComposition, itemIsNugget, itemIsOperation, lassoNuggets, Composition } from '../lib/prompt'; import { $composition, $library, $slottedComposition, LibraryItem, PromptItem, addToOperation, insertIntoComposition, itemIsNugget, itemIsOperation, lassoNuggets, Composition, _setComposition, removeItemFromLibrary, removeFromComposition, removeNuggetFromOperation } from '../lib/prompt';
import { Category } from '@mui/icons-material'; import { BackHand, Book, Category, DragHandle, LibraryBooks, MouseSharp, Score, Sort } from '@mui/icons-material';
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import Nugget from './Nugget'; import Nugget from './Nugget';
import { Stack } from '@mui/material'; import { Stack, ToggleButton, ToggleButtonGroup } from '@mui/material';
import { Op, Operation } from './Operation'; import { Op, Operation } from './Operation';
import { PromptItemProps } from './PromptItem'; import { EditorMode, PromptItemProps } from './PromptItem';
import { $dragDropState, $dropCandidate, $isDragInProgress, $sourceItem, completeDrop, endHoverOver, startDrag, startHoverOver } from '../store/prompt-dnd'; import { ReactSortable } from "react-sortablejs";
import { $dragDropState, $dropCandidate, $isDragInProgress, $sourceItem, CategoryMismatchError, completeDrop, endHoverOver, startDrag, startHoverOver } from '../store/prompt-dnd';
export interface PromptComposerProps { export interface PromptComposerProps {
@ -21,11 +22,11 @@ export default function PromptComposer(props: PromptComposerProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClickOpen = () => { const handleClickOpen = () => {
setOpen(true); if (!open) { setOpen(true); }
}; };
const handleClose = () => { const handleClose = () => {
setOpen(false); if (open) { setOpen(false); }
}; };
const composition = useStore($composition); const composition = useStore($composition);
@ -38,6 +39,31 @@ export default function PromptComposer(props: PromptComposerProps) {
console.log(composition) 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 * @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. // is either a Nugget or an Operation.
const callbacks = { const callbacks = {
onDragStart: (item: PromptItem) => { onDragStart: (item: PromptItem) => {
if (editMode !== "dnd") return;
if (itemIsNugget(promptItem)) { if (itemIsNugget(promptItem)) {
startDrag(item); startDrag(item);
} }
// TODO: operation // TODO: operation
}, },
onDrop: (item: PromptItem) => { onDrop: (item: PromptItem) => {
if (editMode !== "dnd") return;
const dnd = useStore($dragDropState); const dnd = useStore($dragDropState);
const isDragInProgress = useStore($isDragInProgress); const isDragInProgress = useStore($isDragInProgress);
const dropCandidate = useStore($dropCandidate); const dropCandidate = useStore($dropCandidate);
@ -73,14 +101,27 @@ export default function PromptComposer(props: PromptComposerProps) {
} }
completeDrop(); completeDrop();
}, },
onDragEnd: (item: PromptItem) => { onDragEnd: () => {
try {
completeDrop();
} catch (err) {
if (err instanceof CategoryMismatchError) {
setError(err);
} else {
throw err;
}
}
}, },
onMouseEnter: (item: PromptItem) => { onMouseEnter: (item: PromptItem) => {
}, },
onMouseLeave: (item: PromptItem) => { onMouseLeave: (item: PromptItem) => {
if (editMode !== "dnd") return;
endHoverOver(); endHoverOver();
}, },
onDelete: (item: PromptItem) => {
if (editMode !== "dnd") return;
removeFromComposition(item);
},
} as PromptItemProps; } as PromptItemProps;
return ("op" in promptItem ? return ("op" in promptItem ?
@ -88,23 +129,73 @@ export default function PromptComposer(props: PromptComposerProps) {
: <Nugget nugget={promptItem} key={key} isTopLevel={true} {...callbacks} />) : <Nugget nugget={promptItem} key={key} isTopLevel={true} {...callbacks} />)
} }
function setComposition(c: Composition) {
console.log("updated composition: %x", c)
_setComposition(c);
}
const handleSetEditMode = (
event: React.MouseEvent<HTMLElement>,
mode: EditorMode | null,
) => {
if (mode) setEditMode(mode);
};
const handleErrorSnackbarClose = () => {
setError(null);
}
return ( return (
<div> <Box>
<div> <Container>
<Button className="add-button" onClick={handleClickOpen}> <Stack direction="row" style={{ padding: "4pt" }} >
<AddIcon /> <ButtonGroup>
<Button onClick={handleClickOpen} >
<LibraryBooks />
</Button>
</ButtonGroup>
<ToggleButtonGroup
value={editMode}
exclusive
onChange={handleSetEditMode}
size="large"
>
<ToggleButton value="dnd" aria-label="drag-n-drop" size="large" style={{ padding: "4pt" }}>
<BackHand />
</ToggleButton>
<ToggleButton value="sort" aria-label='score' style={{ padding: "4pt" }}>
<Sort />
</ToggleButton>
</ToggleButtonGroup>
<Typography>
<strong>{editMode} Mode</strong> enabled
</Typography>
</Stack>
</Container>
<Container className="composer-main">
{editMode == "sort" ? (
<ReactSortable list={composition} setList={setComposition}>
{composition.map(c => promptItemFactory(c, `item-${c.id}`))}
</ReactSortable>
) : composition.map(c => promptItemFactory(c, `item-${c.id}`))
}
</Container>
<PromptLibrary <PromptLibrary
open={open} open={open}
onDeleteItem={handleOnDeleteItem}
onInsertItem={handleOnInsertItem} onInsertItem={handleOnInsertItem}
onClose={handleClose} onClose={handleClose} />
></PromptLibrary> <Snackbar
</Button> message={error?.message}
</div> open={error !== null}
<div> autoHideDuration={6000}
{ onClose={handleErrorSnackbarClose}
composition.map(c => promptItemFactory(c, `item-${c.id}`)) anchorOrigin={{
} vertical: "top",
</div> horizontal: "center",
</div> }}
/>
</Box>
); );
} }

View File

@ -7,5 +7,17 @@
} }
.prompt-item.muted, .operation.muted .nugget { .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;
} }

View File

@ -14,9 +14,12 @@ import {PromptItem as PIType} from "../lib/prompt"
export interface PromptItemProps { export interface PromptItemProps {
onDragStart?: (item : PIType) => void, onDragStart?: (item : PIType) => void,
onDragEnd?: (item: PIType) => void, onDragEnd?: () => void,
onDragOver?: (item: PIType) => void, onDragOver?: (item: PIType) => void,
onDrop?: (item : PIType) => void, onDrop?: (item : PIType) => void,
onMouseEnter?: (item : PIType) => void, onMouseEnter?: (item : PIType) => void,
onMouseLeave?: (item : PIType) => void, onMouseLeave?: (item : PIType) => void,
onDelete : (item : PIType) => void,
} }
export type EditorMode = "dnd" | "sort" | "score"

View File

@ -25,6 +25,7 @@ const mockItem: LibItemType = {
}; };
const mockOnClose = jest.fn(); const mockOnClose = jest.fn();
const mockOnDeleteItem = jest.fn();
const mockOpen: boolean = true; const mockOpen: boolean = true;
@ -32,6 +33,7 @@ const mockProps: SimpleDialogProps = {
open: mockOpen, open: mockOpen,
onInsertItem: mockOnAddItem, onInsertItem: mockOnAddItem,
onClose: mockOnClose, onClose: mockOnClose,
onDeleteItem: mockOnDeleteItem,
}; };
const mockLibrary: LibraryType = [ const mockLibrary: LibraryType = [

View File

@ -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 { LibraryItem as LibItemType, $library, Category, LibraryItem, insertIntoComposition } from "../lib/prompt";
import { MouseEvent, useMemo, useState } from "react"; import { MouseEvent, useMemo, useState } from "react";
import { useStore } from "@nanostores/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 { DataGrid, GridApi, GridColDef, GridColTypeDef } from '@mui/x-data-grid';
import "./PromptLibrary.css" import "./PromptLibrary.css"
import { title } from "../lib/util"; import { title } from "../lib/util";
import { Add } from "@mui/icons-material"; import { Add, Delete } from "@mui/icons-material";
export interface SimpleDialogProps { export interface SimpleDialogProps {
open: boolean; open: boolean;
onClose: () => void, onClose: () => void,
// onAddItem: (item: LibItemType) => void, // onAddItem: (item: LibItemType) => void,
onInsertItem: (item: LibItemType) => void, onInsertItem: (item: LibItemType) => void,
onDeleteItem: (item: LibItemType) => void,
} }
export function PromptLibrary(props: SimpleDialogProps) { export function PromptLibrary(props: SimpleDialogProps) {
const { open, onInsertItem, onClose } = props; const { open, onInsertItem, onClose, onDeleteItem } = props;
const library = useStore($library); const library = useStore($library);
@ -54,11 +55,23 @@ export function PromptLibrary(props: SimpleDialogProps) {
<Add /> <Add />
</Button> </Button>
); );
} }, headerName: "",
}, },
{ field: 'name', headerName: 'Name', width: 150 }, { field: 'name', headerName: 'Name', width: 150 },
{ field: 'prompt', headerName: 'Prompt', width: 250 }, { field: 'prompt', headerName: 'Prompt', width: 250 },
{ field: 'category', headerName: 'Category', width: 150 }, { field: 'category', headerName: 'Category', width: 150 },
{ field: "delete", headerName: "headerName", width: 50, renderCell: (params) => {
const handleClick = ($e: MouseEvent<any>) => {
$e.stopPropagation();
const libItem = library.find(l => l.id === params.id) as LibItemType;
if (libItem) onDeleteItem(libItem);
}
return (
<Button onClick={handleClick}>
<Delete />
</Button>
);
} },
]; ];
const rows = filteredLibrary.map(item => ({ const rows = filteredLibrary.map(item => ({
@ -72,16 +85,22 @@ export function PromptLibrary(props: SimpleDialogProps) {
<Dialog <Dialog
hideBackdrop hideBackdrop
disableEnforceFocus disableEnforceFocus
style={{ position: "initial", top: "30%", left: "30%", height: "fit-content", width: "fit-content" }} fullWidth={true}
maxWidth="lg"
className="prompt-library-dialog" className="prompt-library-dialog"
onClose={handleClose} onClose={handleClose}
open={open} open={open}
> >
<DialogTitle>Prompt Library</DialogTitle> <DialogTitle>Prompt Library</DialogTitle>
<div> <div>
<DataGrid rows={rows} columns={columns} /> <DataGrid rows={rows} columns={columns} style={{display: "block", width: "fit-contents"}} />
</div> </div>
<NewLibraryItem onNewCreated={handleOnNewCreated} /> <NewLibraryItem onNewCreated={handleOnNewCreated} />
<DialogActions>
<Button autoFocus onClick={handleClose}>
Close
</Button>
</DialogActions>
</Dialog> </Dialog>
); );
} }

View File

@ -1,6 +1,6 @@
import { v4 as randomUUID, v4 as uuidv4 } from "uuid"; import { v4 as randomUUID, v4 as uuidv4 } from "uuid";
import { Op } from "./operator"; import { Op } from "./operator";
import { Atom, ReadableAtom, Store, WritableAtom, atom, computed } from "nanostores"; import { atom, computed } from "nanostores";
type Id = string; type Id = string;
@ -73,7 +73,7 @@ export function removeItemFromLibrary(item: LibraryItem) {
$library.set($library.get().filter(i => (i.id !== item.id))); $library.set($library.get().filter(i => (i.id !== item.id)));
} }
export const $composition = atom<Composition>([]) export const $composition = atom<Composition>([]);
export function insertIntoComposition(item: LibraryItem) { export function insertIntoComposition(item: LibraryItem) {
$composition.set([ $composition.set([
@ -91,8 +91,19 @@ export function removeFromComposition(item: PromptItem) {
function nuggetDelta(nuggetId: Id, delta: number) { function nuggetDelta(nuggetId: Id, delta: number) {
$composition.set($composition.get().map(item => { $composition.set($composition.get().map(item => {
if ((item.id === nuggetId) && ("score" in item)) { if ((item.id === nuggetId) && ("score" in item)) {
const o = { ...item, score: item.score + delta }; return { ...item, score: item.score + delta };
return o; }
if ("op" in item) {
return {
...item, items: item.items.map(
nug => {
return {
...nug,
score: nug.score + (nuggetId === nug.id ? delta : 0),
}
}
)
}
} }
return item; return item;
} }
@ -228,3 +239,28 @@ export function togglePromptItemMute(id: Id) {
} }
)); ));
} }
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];
}
})
)
}

View File

@ -1,5 +1,5 @@
import { Atom, atom, computed } from "nanostores" 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"; import { Op } from "../lib/operator";
export type DropCandidate = string | string [] export type DropCandidate = string | string []
@ -55,6 +55,12 @@ export function cancelDrop() {
$dragDropState.set({}); $dragDropState.set({});
}; };
export class CategoryMismatchError extends Error {
constructor(public c1 : Category, public c2 : Category) {
super(`Cannot merge '${c1}' into '${c2}'`);
}
}
export function completeDrop() { export function completeDrop() {
const source = $sourceItem.get(); const source = $sourceItem.get();
const target = $dropCandidate.get(); const target = $dropCandidate.get();
@ -65,7 +71,7 @@ export function completeDrop() {
const c1 = nSource.item.category; const c1 = nSource.item.category;
const c2 = nTarget.items[0].item.category; const c2 = nTarget.items[0].item.category;
if (c1 != c2) { if (c1 != c2) {
console.error("Category mismatch: cannot drop a %s into %s", c1, c2); throw new CategoryMismatchError(c1, c2);
} else { } else {
addToOperation(source.id, target.id); addToOperation(source.id, target.id);
} }
@ -76,7 +82,7 @@ export function completeDrop() {
const c1 = nSource.item.category; const c1 = nSource.item.category;
const c2 = nTarget.item.category; const c2 = nTarget.item.category;
if (c1 != c2) { if (c1 != c2) {
console.error("Category mismatch: cannot drop a %s into %s", c1, c2); throw new CategoryMismatchError(c1, c2);
} else { } else {
lassoNuggets(source.id, target.id, Op.AND) lassoNuggets(source.id, target.id, Op.AND)
} }