Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# A suggestion list field for Nova apps

This package contains a Nova field to automatically generate suggestions based on an array of strings using GPT-3.

![Field View](docs/field.png)

## Installation

You can install the package in to a Laravel app that uses [Nova](https://nova.laravel.com) via composer:

```bash
composer require siteorigin/nova-suggestion-list-field
```

## Usage

Add the `Siteorigin\NovaSuggestionList\SuggestionList` field in your Nova resource:


```php
namespace App\Nova;

use Siteorigin\NovaSuggestionList\SuggestionList;

class User extends Resource
{
// ...

public function fields(Request $request)
{
return [
// ...

SuggestionList::make('Items'),

// ...
];
}
}
```

## Allowing auto refresh suggestion

If you need to refresh the suggestions after each list update you can call the `autorefresh` method.

```php
SuggestionList::make('Items')->autorefresh(),
```

## Options for the GPT-3 Suggester

You can also change the GPT-3 Suggester configuration using the `suggesterOptions` method.

```php

SuggestionList::make('Items')
->suggesterOptions([
'engine' => 'davinci'
]),
```
29 changes: 29 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "siteorigin/nova-suggestion-list",
"description": "A Laravel Nova field.",
"keywords": [
"laravel",
"nova"
],
"license": "MIT",
"require": {
"php": "^7.4|^8.0"
},
"autoload": {
"psr-4": {
"Siteorigin\\NovaSuggestionList\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"Siteorigin\\NovaSuggestionList\\FieldServiceProvider"
]
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
1 change: 1 addition & 0 deletions dist/css/field.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.btn-refresh{width:24px;height:24px;left:100%;top:5px}.suggestions-list{max-height:300px;overflow-y:auto}
1 change: 1 addition & 0 deletions dist/js/field.js

Large diffs are not rendered by default.

Binary file added docs/field.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.0.0",
"laravel-mix": "^1.0",
"laravel-nova": "^1.0"
},
"dependencies": {
"vue": "^2.5.0"
}
}
9 changes: 9 additions & 0 deletions resources/js/components/DetailField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<panel-item :field="field" />
</template>

<script>
export default {
props: ['resource', 'resourceName', 'resourceId', 'field'],
}
</script>
163 changes: 163 additions & 0 deletions resources/js/components/FormField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<template>
<default-field :field="field" :errors="errors" :show-help-text="showHelpText">
<template slot="field">
<div class="relative">
<div ref="suggestionList" class="flex suggestions-list mb-3">
<div
class="pr-3 w-1/2"
@drop='onDrop'
@dragover.prevent
@dragenter.prevent
>
<div
v-for="(item, index) in value"
:key="index"
class="flex border border-40 bg-30 p-2 mb-3 text-sm items-center justify-between"
>
{{ item }}
<button @click.prevent="deleteItem(index)">
<close-icon/>
</button>
</div>
</div>

<div class="pl-3 w-1/2 relative">
<div
v-for="(suggestion, index) in suggestions"
:key="index"
class="cursor-pointer flex border border-40 bg-30 p-2 mb-3 text-sm items-center"
draggable
@dragstart='startDrag($event, index)'
>
<button @click.prevent="addSuggestionToItems(index)">
<arrow-icon/>
</button>
<span class="ml-2">{{ suggestion }}</span>
</div>
</div>
</div>

<button
v-if="showRefreshButton"
class="absolute btn-refresh text-sm ml-3"
@click.prevent="fetchSuggestions"
>
<refresh-icon/>
</button>
</div>
<input
:id="field.name"
type="text"
class="w-full form-control form-input form-input-bordered"
:class="errorClasses"
:placeholder="field.name"
v-model.trim="newItem"
@keydown.enter.prevent="addItem"
/>
</template>
</default-field>
</template>

<script>
import { FormField, HandlesValidationErrors } from 'laravel-nova'

import CloseIcon from '../icons/Close'
import ArrowIcon from '../icons/Arrow'
import RefreshIcon from '../icons/Refresh'

export default {
mixins: [FormField, HandlesValidationErrors],

props: ['resourceName', 'resourceId', 'field'],

components: {
CloseIcon,
ArrowIcon,
RefreshIcon
},

data: () => ({
newItem: '',
suggestions: []
}),

computed: {
isAutoRefreshEnabled () {
return !!this.field.autorefresh
},
showRefreshButton () {
return this.value
&& this.value.length > 0
&& !this.isAutoRefreshEnabled
}
},

watch: {
value () {
if (this.isAutoRefreshEnabled) {
this.fetchSuggestions()
}
this.$nextTick(() => this.scrollToBottom())
}
},

methods: {
/*
* Set the initial, internal value for the field.
*/
setInitialValue () {
this.value = this.field.value || []
this.fetchSuggestions()
},

/**
* Fill the given FormData object with the field's internal value.
*/
fill (formData) {
const value = this.value ? JSON.stringify(this.value) : []
formData.append(this.field.attribute, value)
},

addItem () {
if (this.newItem) {
this.value.push(this.newItem)
this.newItem = ''
}
},

addSuggestionToItems (index) {
const suggestion = this.suggestions[index]
this.value.push(suggestion)
this.suggestions.splice(index, 1)
},

deleteItem (index) {
this.value.splice(index, 1)
},

scrollToBottom () {
this.$refs.suggestionList.scrollTop = this.$refs.suggestionList.scrollHeight
},

startDrag (event, suggestionIndex) {
event.dataTransfer.dropEffect = 'move'
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('suggestionIndex', suggestionIndex)
},

onDrop (event) {
const index = event.dataTransfer.getData('suggestionIndex')
this.addSuggestionToItems(index)
},

fetchSuggestions () {
Nova.request()
.post('/nova-vendor/suggestion-list/suggestions', {
value: this.value,
options: this.field.suggesterOptions
})
.then(({ data }) => (this.suggestions = data.suggestions))
}
},
}
</script>
9 changes: 9 additions & 0 deletions resources/js/components/IndexField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<span>{{ field.value.join(', ') }}</span>
</template>

<script>
export default {
props: ['resourceName', 'field'],
}
</script>
5 changes: 5 additions & 0 deletions resources/js/field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Nova.booting((Vue, router, store) => {
Vue.component('index-nova-suggestion-list', require('./components/IndexField'))
Vue.component('detail-nova-suggestion-list', require('./components/DetailField'))
Vue.component('form-nova-suggestion-list', require('./components/FormField'))
})
7 changes: 7 additions & 0 deletions resources/js/icons/Arrow.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<svg width="15" height="10" viewBox="0 0 15 10" fill="none">
<path d="M14.071 10V8H6.07101V10H14.071Z" fill="black"/>
<path d="M14.071 2V0H6.07101V2H14.071Z" fill="black"/>
<path d="M14.071 6V4H4.071V0.964996L0 4.965L4.071 8.965V6H14.071Z" fill="black"/>
</svg>
</template>
5 changes: 5 additions & 0 deletions resources/js/icons/Close.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<svg width="10" height="10" viewBox="0 0 13 13" fill="none">
<path d="M2.03953 0.233007L2.22148 0.38117L6.5 4.66229L10.7785 0.38117C11.0226 0.137126 11.3536 2.35498e-05 11.6987 2.35498e-05C12.0438 2.35498e-05 12.3748 0.137126 12.6189 0.38117C12.8629 0.625213 13 0.956207 13 1.30134C13 1.64647 12.8629 1.97746 12.6189 2.2215L8.33774 6.50002L12.6189 10.7785C12.8338 10.9939 12.9663 11.2779 12.9934 11.5809C13.0204 11.8839 12.9403 12.1869 12.767 12.4369L12.6189 12.6189C12.4035 12.8338 12.1195 12.9663 11.8165 12.9934C11.5135 13.0205 11.2105 12.9404 10.9605 12.767L10.7785 12.6189L6.5 8.33776L2.22148 12.6189C1.97744 12.8629 1.64644 13 1.30131 13C0.956183 13 0.625189 12.8629 0.381145 12.6189C0.137101 12.3748 6.80333e-09 12.0438 0 11.6987C-6.80333e-09 11.3536 0.137101 11.0226 0.381145 10.7785L4.66226 6.50002L0.381145 2.2215C0.166244 2.00613 0.0337128 1.7222 0.00663857 1.41915C-0.0204356 1.11611 0.0596626 0.813178 0.232982 0.563123L0.381145 0.38117C0.569028 0.193959 0.809692 0.0687127 1.07082 0.0222482C1.33195 -0.0242162 1.60105 0.0103255 1.84198 0.121235L2.03953 0.233007Z" fill="#000"/>
</svg>
</template>
6 changes: 6 additions & 0 deletions resources/js/icons/Refresh.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</template>
13 changes: 13 additions & 0 deletions resources/sass/field.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Nova Tool CSS

.btn-refresh {
width: 24px;
height: 24px;
left: 100%;
top: 5px;
}

.suggestions-list {
max-height: 300px;
overflow-y: auto;
}
5 changes: 5 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

use Siteorigin\NovaSuggestionList\Http\Controllers\SuggestionListController;

Route::post('suggestions', [SuggestionListController::class, 'index']);
24 changes: 24 additions & 0 deletions src/Contracts/Suggester.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Siteorigin\NovaSuggestionList\Contracts;

use Illuminate\Support\Collection;

interface Suggester
{
/**
* Apply the suggester to the given string collection.
*
* @param Collection $items String collection
* @return Collection Returns Collection of suggested items
*/
public function apply(Collection $items): Collection;

/**
* Configuring options for Suggester.
*
* @param array $options
* @return Suggester
*/
public function setOptions(array $options): Suggester;
}
Loading