prompts can be added to prompt composer.

This commit is contained in:
Jordan 2024-02-28 08:36:40 -08:00
parent b0c70f6d17
commit d9c1282d99
19 changed files with 283 additions and 59 deletions

View File

@ -27,6 +27,7 @@
"react-redux": "^9.1.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"uuid": "^9.0.1",
"vite": "^5.1.4",
"web-vitals": "^2.1.4"
},
@ -57,11 +58,12 @@
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@types/jest": "^27.5.2",
"@types/uuid": "^9.0.8",
"jest-without-globals": "^0.0.3"
},
"jest": {
"transformIgnorePatterns": [
"!node_modules/"
]
"transformIgnorePatterns": [
"!node_modules/"
]
}
}

View File

@ -80,6 +80,9 @@ dependencies:
typescript:
specifier: ^4.9.5
version: 4.9.5
uuid:
specifier: ^9.0.1
version: 9.0.1
vite:
specifier: ^5.1.4
version: 5.1.4(@types/node@16.18.82)
@ -94,6 +97,9 @@ devDependencies:
'@types/jest':
specifier: ^27.5.2
version: 27.5.2
'@types/uuid':
specifier: ^9.0.8
version: 9.0.8
jest-without-globals:
specifier: ^0.0.3
version: 0.0.3(jest@25.5.4)
@ -3816,6 +3822,10 @@ packages:
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
dev: false
/@types/uuid@9.0.8:
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
dev: true
/@types/ws@8.5.10:
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
dependencies:
@ -12908,6 +12918,11 @@ packages:
hasBin: true
dev: false
/uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
dev: false
/v8-to-istanbul@4.1.4:
resolution: {integrity: sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==}
engines: {node: 8.x.x || >=10.10.0}

View File

@ -3,6 +3,7 @@ import './App.css';
import PromptComposer from './components/PromptComposer';
import { Box, Tabs, Tab, Typography } from '@material-ui/core';
import { $textComposition } from './lib/prompt';
import { TextPrompt } from './components/TextPrompt';
interface TabPanelProps {
@ -56,9 +57,7 @@ function App() {
<PromptComposer />
</CustomTabPanel>
<CustomTabPanel value={value} index={1}>
<div>
<textarea aria-label='text-area'>{ $textComposition.get() }</textarea>
</div>
<TextPrompt />
</CustomTabPanel>
</>
);

View File

@ -1,5 +0,0 @@
import { Component, ReactComponentElement, ReactInstance, ReactNode } from "react";
export type Composable = {
} & ReactNode

View File

@ -1,4 +1,3 @@
import { Composable } from "./IComposable";
import { LibraryItem as LibItem } from "../lib/prompt";
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
import { Button } from "@material-ui/core";
@ -23,5 +22,5 @@ export function LibraryItem(props: StyleProps) {
}
</span>
</div>
) as Composable;
);
};

View File

@ -0,0 +1,77 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import {NewLibraryItem} from './NewLibraryItem';
import { Category, addItemToLibrary, categoryHasName } from '../lib/prompt';
jest.mock('../lib/prompt', () => ({
Category: Category,
addItemToLibrary: jest.fn(),
categoryHasName: jest.fn(),
}));
describe('NewLibraryItem', () => {
beforeEach(() => {
(addItemToLibrary as jest.Mock).mockReset();
(categoryHasName as jest.Mock).mockReset();
});
it('renders the form', () => {
render(<NewLibraryItem />);
expect(screen.getByText('Category')).toBeInTheDocument();
expect(screen.getByText('Subject')).toBeInTheDocument();
expect(screen.getByText('Prompt')).toBeInTheDocument();
expect(screen.getByText('Create')).toBeInTheDocument();
});
it('changes the category', () => {
render(<NewLibraryItem />);
fireEvent.change(screen.getByLabelText('Prompt Item Category'), {
target: { value: Category.vibes },
});
expect(screen.getByLabelText('Prompt Item Name')).toBeVisible();
});
it('changes the name', () => {
render(<NewLibraryItem />);
fireEvent.change(screen.getByLabelText('Name'), {
target: { value: 'Test' },
});
expect(screen.getByDisplayValue('Test')).toBeInTheDocument();
});
it('changes the prompt', () => {
render(<NewLibraryItem />);
fireEvent.change(screen.getByLabelText('Prompt'), {
target: { value: 'Test' },
});
expect(screen.getByDisplayValue('Test')).toBeInTheDocument();
});
it('creates a new library item', () => {
render(<NewLibraryItem />);
fireEvent.change(screen.getByLabelText('Category'), {
target: { value: Category.subject },
});
fireEvent.change(screen.getByLabelText('Name'), {
target: { value: 'Test' },
});
fireEvent.change(screen.getByLabelText('Prompt'), {
target: { value: 'Test' },
});
fireEvent.click(screen.getByText('Create'));
expect(addItemToLibrary).toHaveBeenCalledWith({
category: Category.subject,
name: 'Test',
prompt: 'Test',
});
});
});

View File

@ -0,0 +1,81 @@
import { Button, FormControl, InputLabel, MenuItem, TextField } from "@material-ui/core";
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { Category, LibraryItem, addItemToLibrary, categoryHasName } from "../lib/prompt";
import { ChangeEvent, useState } from "react";
import {v4 as uuidv4} from "uuid"
export interface NewLibraryItemProps {
onNewCreated?: () => void;
}
export function NewLibraryItem(props: NewLibraryItemProps) {
const { onNewCreated } = props;
const [category, setCategory] = useState(Category.subject);
const [name, setName] = useState("");
const [prompt, setPrompt] = useState("");
const handleCategoryChange = (e: any | SelectChangeEvent) => {
setCategory(e.target.value as Category);
}
const handleNameChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setName(e.target.value);
}
const handlePromptChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setPrompt(e.target.value);
}
const titleCase = (s: string) => {
return s[0].toUpperCase() + s.substring(1);
}
const handleCreateItem = () => {
const libraryItem = {
id: uuidv4(),
category,
name: categoryHasName(category) ? name : null,
prompt,
} as LibraryItem;
addItemToLibrary(libraryItem);
setCategory(Category.subject);
setName("");
setPrompt("");
onNewCreated ? onNewCreated() : null;
}
const catChoices = Object.keys(Category);
return (
<div>
<div>
<FormControl>
<InputLabel htmlFor="new-prompt-category">Category</InputLabel>
<Select
native
id="new-prompt-category"
aria-label="Prompt Item Category"
value={category}
onChange={(e) => handleCategoryChange(e)}
>
{catChoices.map(cat => (
<option value={cat} id={cat} key={cat}>{titleCase(cat)}</option>
))}
</Select>
</FormControl>
</div>
<div>
<FormControl>
{categoryHasName(category) ? (<InputLabel htmlFor="name">Name</InputLabel>) : <></>}
{categoryHasName(category) ? (<TextField aria-label="Prompt Item Name" value={name} onChange={handleNameChange} id="name" />) : <></>}
</FormControl>
</div>
<div>
<FormControl>
<InputLabel htmlFor="prompt">Prompt</InputLabel>
<TextField aria-label="Prompt Item Text" value={prompt} onChange={handlePromptChange} id="prompt" />
<Button onClick={handleCreateItem} >Create</Button>
</FormControl>
</div>
</div>
)
}

View File

@ -2,7 +2,6 @@ import { Button, ButtonGroup, Chip, Divider } from '@material-ui/core';
import React, { Component, useState } from 'react';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowUp';
import {Composable} from "./IComposable"
import "./Nugget.css";
import { Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore } from '../lib/prompt';
@ -32,5 +31,5 @@ export default function Nugget(props : NuggetProps) {
</ButtonGroup>
</span>
</div>
) as Composable;
);
}

View File

@ -2,8 +2,7 @@ import { Menu, MenuItem } from "@material-ui/core";
import React, { Children, ReactNode } from 'react';
import "./Operation.css";
import { Op } from "../lib/operator";
import { randomUUID } from "crypto";
import { Composable } from "./IComposable";
import { v4 as randomUUID } from "uuid";
import { Operation as OperationType, changeOperationOp } from "../lib/prompt";
import Nugget from "./Nugget";
@ -72,7 +71,7 @@ function Operation(props : OperationProps) {
})}
</Menu>
</div>
) as Composable;
);
}
export { Operation, Op };

View File

@ -1,27 +1,29 @@
import { Button } from '@material-ui/core';
import Masonry from '@mui/lab/Masonry';
import Nugget from './Nugget';
import { Operation } from './Operation';
import AddIcon from '@mui/icons-material/Add';
import "./PromptArea.css";
import "./PromptComposer.css";
import { PromptLibrary } from './PromptLibrary';
import React from 'react';
import { $composition, $slottedComposition, LibraryItem, insertIntoComposition } from '../lib/prompt';
import React, { useState } from 'react';
import { $slottedComposition, LibraryItem, PromptItem, insertIntoComposition } from '../lib/prompt';
import { Category } from '@mui/icons-material';
import { useStore } from '@nanostores/react'
import Nugget from './Nugget';
import { Stack } from '@mui/material';
import { Operation } from './Operation';
type Composable = (typeof Nugget) | (typeof Operation);
export interface PromptComposerProps {
export default function PromptComposer(props) {
const [open, setOpen] = React.useState(false);
}
export default function PromptComposer(props: PromptComposerProps) {
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = (value: string) => {
const handleClose = () => {
setOpen(false);
// setSelectedValue(value);
};
const handleOnInsertItem = (item: LibraryItem) => {
@ -30,19 +32,31 @@ export default function PromptComposer(props) {
const slottedComposition = useStore($slottedComposition);
const promptItemFactory = (promptItem : PromptItem, key : string) => {
return "op" in promptItem ? <Operation operation={promptItem} key={key} /> : <Nugget nugget={promptItem} key={key} />
}
return (
<div>
<Button className="add-button">
<AddIcon />
<PromptLibrary
open={open}
onInsertItem={handleOnInsertItem}
></PromptLibrary>
</Button>
<Masonry columns={Object.keys(Category).length} spacing={2} sequential>
<div>Something</div>
</Masonry>
<div>
<Button className="add-button" onClick={handleClickOpen}>
<AddIcon />
<PromptLibrary
open={open}
onInsertItem={handleOnInsertItem}
onClose={handleClose}
></PromptLibrary>
</Button>
</div>
<div>
{
slottedComposition.map((itemCol, i) => (
<Stack>
{itemCol.map((promptItem, j) => promptItemFactory(promptItem, `item-${j}-${j}`))}
</Stack>
))
}
</div>
</div>
);
}

View File

@ -0,0 +1,3 @@
.prompt-library-dialog .categories div {
display: inline;
}

View File

@ -31,6 +31,7 @@ const mockOpen: boolean = true;
const mockProps: SimpleDialogProps = {
open: mockOpen,
onInsertItem: mockOnAddItem,
onClose: mockOnClose,
};
const mockLibrary: LibraryType = [

View File

@ -1,12 +1,15 @@
import { Checkbox, Dialog, DialogTitle } from "@material-ui/core";
import { Button, Checkbox, Dialog, DialogTitle } from "@material-ui/core";
import { LibraryItem as LibItemType, $library, Category } from "../lib/prompt";
import { LibraryItem } from "./LibraryItem";
import { ChangeEvent } from "react";
import { ChangeEvent, useState } from "react";
import { useStore } from "@nanostores/react";
import { ExitToAppOutlined } from "@mui/icons-material";
import { NewLibraryItem } from "./NewLibraryItem";
import "./PromptLibrary.css"
export interface SimpleDialogProps {
open: boolean;
// onClose: (composable: Composable) => void,
onClose: () => void,
// onAddItem: (item: LibItemType) => void,
onInsertItem: (item: LibItemType) => void,
}
@ -25,10 +28,14 @@ const show = ($el: Element) => {
}
export function PromptLibrary(props: SimpleDialogProps) {
const { open, onInsertItem } = props;
const { open, onInsertItem, onClose } = props;
const library = useStore($library);
const [doCreate, setDoCreate] = useState(false);
const [visibleCategories, setVisibleCategories] = useState([] as Category[]);
const handleOnAddItem = (item: LibItemType) => {
// onAddItem(item);
}
@ -37,32 +44,47 @@ export function PromptLibrary(props: SimpleDialogProps) {
onInsertItem(item);
}
const filterCat = (catKey: string, catVal: string, v: ChangeEvent<HTMLInputElement>) => {
const filterCat = (catKey: string, v: ChangeEvent<HTMLInputElement>) => {
const isChecked = v.target.value === '1';
document.querySelectorAll(`.category-${catVal}`).forEach($el => {
setVisibleCategories((visibleCategories.includes(catKey as Category) && !isChecked) ? visibleCategories.filter(c => c != catKey) : [...visibleCategories, catKey as Category]);
document.querySelectorAll(`.category-${catKey}`).forEach($el => {
if (isChecked) show($el)
else hide($el)
});
}
const handleClose = () => {
onClose();
}
const handleOnNewCreated = () => {
setDoCreate(false);
}
const categoryChoices = Object.keys(Category);
return (
<Dialog open={open}>
<Dialog className="prompt-library-dialog" onClose={handleClose} open={open}>
<DialogTitle>Prompt Library</DialogTitle>
<div className="categories">
{Object.entries(Category).map(([catKey, catVal]) => {
{categoryChoices.map(catKey => {
return (
<div>
<Checkbox onChange={v => filterCat(catKey, catVal, v)} />
<span>{title(catVal)}</span>
<div key={catKey}>
<Checkbox onChange={v => filterCat(catKey, v)} />
<span>{title(catKey)}</span>
</div>
)
})}
</div>
<div>
{
library?.map(item => <LibraryItem item={item} onInsertItem={handleOnInsertItem} />)
library?.map(item => <LibraryItem key={item.id} item={item} onInsertItem={handleOnInsertItem} />)
}
</div>
<div>
<Button onClick={() => setDoCreate(true)}>Create</Button>
</div>
{doCreate ? (<NewLibraryItem onNewCreated={handleOnNewCreated} />) : (<></>)}
</Dialog>
);
}

View File

@ -0,0 +1,6 @@
.text-prompt {
width: 75%;
height: 75%;
padding: 10pt;
margin: 4pt;
}

View File

@ -0,0 +1,11 @@
import { TextareaAutosize } from "@material-ui/core";
import { $textComposition } from "../lib/prompt";
import "./TextPrompt.css";
import { useStore } from "@nanostores/react";
export function TextPrompt () {
const text = useStore($textComposition);
return (
<TextareaAutosize content={text} className="text-prompt" />
)
}

View File

@ -3,17 +3,13 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from "./store"
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider >
<App />
</React.StrictMode>
);

View File

@ -1,4 +1,4 @@
import { randomUUID } from "crypto";
import { v4 as randomUUID } from "uuid";
import { Op } from "./operator";
import { createSlice } from '@reduxjs/toolkit'

View File

@ -1,4 +1,4 @@
import { randomUUID } from "crypto";
import { v4 as randomUUID } from "uuid";
import { Op } from "./operator";
import {
Library as LibraryType,

View File

@ -1,4 +1,4 @@
import { randomUUID } from "crypto";
import { v4 as randomUUID } from "uuid";
import { Op } from "./operator";
import { atom, computed } from "nanostores";
@ -8,6 +8,7 @@ type IdAble = {
id: Id,
}
// only vibes and styles will have a name.
export enum Category {
subject = "subject",
style = "style",
@ -15,6 +16,10 @@ export enum Category {
medium = "medium",
}
export function categoryHasName(cat : Category) {
return (cat === Category.style || cat === Category.vibes)
}
const N_CATEGORIES = Object.keys(Category).length;
export function catI(c: Category | string): number {