+ half of ai prompt data source selection view, task #95117.

This commit is contained in:
liwenrui
2023-06-25 13:39:19 +08:00
parent 76016a8bb0
commit 17c0dd05bc
4 changed files with 360 additions and 0 deletions
+5
View File
@@ -32,3 +32,8 @@ foreach($config->ai->openai->contentTypeMapping as $contentType => $apis)
$config->ai->createprompt = new stdclass();
$config->ai->createprompt->requiredFields = 'name';
$config->ai->dataSource = array();
$config->ai->dataSource['story']['story'] = array('title', 'spec', 'verify', 'product', 'module', 'pri', 'category', 'estimate');
$config->ai->dataSource['execution']['execution'] = array('name', 'desc', 'estimate');
$config->ai->dataSource['execution']['tasks'] = array('name', 'pri', 'status', 'estimate', 'consumed', 'left', 'progress', 'estStarted', 'realStarted', 'finishedDate', 'closedReason');
+10
View File
@@ -177,6 +177,16 @@ class ai extends control
*/
public function promptSelectDataSource($promptID)
{
$prompt = $this->ai->getPromptByID($promptID);
if($_POST)
{
// TODO: do stuff.
}
$this->view->activeMenu = 'story';
$this->view->dataSource = $this->config->ai->dataSource;
$this->view->prompt = $prompt;
$this->view->promptID = $promptID;
$this->view->title = $this->lang->ai->prompts->selectDataSource . " {$this->lang->colon} " . $this->lang->ai->prompts->common;
$this->view->position[] = $this->lang->ai->prompts->common;
+42
View File
@@ -30,12 +30,14 @@ $lang->ai->prompts->createdDate = '创建时间';
$lang->ai->prompts->targetForm = '表单';
$lang->ai->prompts->funcDesc = '功能描述';
/* Steps of prompt creation. */
$lang->ai->prompts->assignRole = '指定角色';
$lang->ai->prompts->selectDataSource = '选择对象';
$lang->ai->prompts->setPurpose = '确认操作';
$lang->ai->prompts->setTargetForm = '结果处理';
$lang->ai->prompts->finalize = '准备发布';
/* Role assigning. */
$lang->ai->prompts->assignModel = '指定语言模型';
$lang->ai->prompts->model = '语言模型';
$lang->ai->prompts->role = '角色';
@@ -43,6 +45,46 @@ $lang->ai->prompts->characterization = '角色描述';
$lang->ai->prompts->rolePlaceholder = '“你来扮演 <一个什么角色>”';
$lang->ai->prompts->charPlaceholder = '该角色的具体描述信息';
/* Data source selecting. */
$lang->ai->prompts->selectData = '选择数据';
$lang->ai->prompts->selectDataTip = '选择对象后,此处会展示已选对象的数据。';
$lang->ai->prompts->selectedFormat = '已选对象为%s,已选 %d 条数据';
$lang->ai->prompts->nonSelected = '暂无所选数据。';
$lang->ai->prompts->sortTip = '可根据重要性给数据字段排序。';
/* Data source definition. */
$lang->ai->dataSource = array();
$lang->ai->dataSource['story']['common'] = '需求';
$lang->ai->dataSource['execution']['common'] = '执行';
$lang->ai->dataSource['story']['story']['common'] = '需求';
$lang->ai->dataSource['story']['story']['title'] = '需求标题';
$lang->ai->dataSource['story']['story']['spec'] = '需求描述';
$lang->ai->dataSource['story']['story']['verify'] = '验收标准';
$lang->ai->dataSource['story']['story']['product'] = '产品';
$lang->ai->dataSource['story']['story']['module'] = '模块';
$lang->ai->dataSource['story']['story']['pri'] = '优先级';
$lang->ai->dataSource['story']['story']['category'] = '需求类型';
$lang->ai->dataSource['story']['story']['estimate'] = '预计工时';
$lang->ai->dataSource['execution']['execution']['common'] = '执行';
$lang->ai->dataSource['execution']['execution']['name'] = '执行名称';
$lang->ai->dataSource['execution']['execution']['desc'] = '执行描述';
$lang->ai->dataSource['execution']['execution']['estimate'] = '预计工时';
$lang->ai->dataSource['execution']['tasks']['common'] = '任务列表';
$lang->ai->dataSource['execution']['tasks']['name'] = '任务名称';
$lang->ai->dataSource['execution']['tasks']['pri'] = '优先级';
$lang->ai->dataSource['execution']['tasks']['status'] = '状态';
$lang->ai->dataSource['execution']['tasks']['estimate'] = '预计工时';
$lang->ai->dataSource['execution']['tasks']['consumed'] = '已消耗';
$lang->ai->dataSource['execution']['tasks']['left'] = '剩余';
$lang->ai->dataSource['execution']['tasks']['progress'] = '进度';
$lang->ai->dataSource['execution']['tasks']['estStarted'] = '预计开始';
$lang->ai->dataSource['execution']['tasks']['realStarted'] = '实际开始';
$lang->ai->dataSource['execution']['tasks']['finishedDate']= '完成日期';
$lang->ai->dataSource['execution']['tasks']['closedReason']= '关闭原因';
$lang->ai->prompts->statuses = array();
$lang->ai->prompts->statuses[''] = '全部';
$lang->ai->prompts->statuses['draft'] = '草稿';
@@ -10,4 +10,307 @@
*/
?>
<?php include '../../common/view/header.html.php';?>
<?php js::set('dataSource', $dataSource);?>
<?php js::set('dataSourceLang', $lang->ai->dataSource);?>
<style>
.center-wrapper {display: flex; justify-content: center; height: 100%;}
.center-content {width: 100%; height: 100%; display: flex; flex-direction: column;}
#data-selector {display: flex; flex-direction: row; height: 100%; margin-bottom: 16px;}
#data-sources {flex-grow: 2; display: flex; flex-direction: row; margin-right: 16px;}
#data-selected {flex-grow: 1; display: flex; flex-direction: column;}
.heading > * {display: inline-block; padding: 4px 0;}
#data-category-select {flex-grow: 1; background-color: #fafafa; border: 1px solid #eee; border-right: none;}
#data-category-select > ul {padding: 16px; margin: 0; list-style: none;}
#data-category-select > ul > li > a.active {font-weight: bold;}
#data-categories {flex-grow: 1; display: flex; flex-direction: column;}
#data-properties {flex-grow: 3; display: flex; flex-direction: column;}
#data-properties > .heading {padding: 4px 20px;}
#data-properties > .heading > h4 {margin: 6px 0;}
#data-property-select {flex-grow: 1; border: 1px solid #eee; border-left: none;}
#data-property-selector {height: 100%; width: 100%;}
#data-selected-items {flex-grow: 1; border: 1px solid #eee;}
.obj-view {display: flex; flex-direction: column; padding: 20px;}
.obj-view:not(:first-of-type) {border-top: 1px solid #eee;}
.obj-view-body {display: grid; padding: 4px 20px; grid-template-columns: repeat(4, 1fr);}
.obj-view-header.checkbox-inline, .obj-view-item.checkbox-inline {margin: 4px; cursor: unset;}
.obj-view-item.checkbox-inline+.obj-view-item.checkbox-inline {margin-left: 4px; margin-top: 4px;}
.checkbox-inline > label, .checkbox-inline > input {cursor: pointer;}
</style>
<script>
/* Store and sync value of datasource input, change by checkbox changes. */
class DataSourceStore
{
value = {};
listeners = [];
/* Get store value. */
get value()
{
return this.value;
}
/* Set store value. */
set value(value)
{
this.value = value;
}
/* Reset store value. */
reset()
{
this.value = {};
}
/* Sync value from form checkboxes. */
sync()
{
const dataSources = document.querySelectorAll('#data-property-selector');
const value = {};
dataSources.forEach(dataSource =>
{
const group = dataSource.getAttribute('object-group');
const items = dataSource.querySelectorAll('input[type="checkbox"]');
items.forEach(item =>
{
if(item.checked) value[item.getAttribute('prop')] = true;
});
});
this.value = value;
document.querySelector('#datasource').value = JSON.stringify(this.value);
/* Notify listeners. */
this.listeners.forEach(l => (l(this.value)));
}
/* Subscribe for store value changes. */
subscribe(listener)
{
this.listeners.push(listener);
}
/* Unsubscribe for store value changes. */
unsubscribe(listener)
{
this.listeners = this.listeners.filter(l => l !== listener);
}
}
window.dataSourceStore = new DataSourceStore(); // Create store instance.
/* Data property selector component, declared as custom element. */
class DataPropertySelector extends HTMLDivElement
{
/* Render with given options. */
render(options)
{
this.innerHTML = '';
/* Get options. */
const {group} = options;
/* Get data source. */
const dataSource = window.dataSource[group];
const dataSourceLang = window.dataSourceLang[group];
/* Render data source. */
for(const [source, props] of Object.entries(dataSource))
{
const objView = document.createElement('div');
objView.classList.add('obj-view');
const headerItem = new DataPropertyCheckbox();
headerItem.classList.add('obj-view-header');
headerItem.setAttribute('source', source);
headerItem.setAttribute('prop', group);
objView.appendChild(headerItem);
const objViewBody = document.createElement('div');
objViewBody.classList.add('obj-view-body');
props.forEach(prop =>
{
const item = new DataPropertyCheckbox();
item.classList.add('obj-view-item');
item.setAttribute('source', source);
item.setAttribute('group', group);
item.setAttribute('prop', prop);
objViewBody.appendChild(item);
});
objView.appendChild(objViewBody);
this.appendChild(objView);
}
}
/* Handle attr change, rerender. */
attributeChangedCallback(name, oldValue, newValue)
{
if(name === 'object-group' && oldValue !== newValue) this.render({group: newValue});
}
/* Define observed attr. */
static get observedAttributes()
{
return ['object-group'];
}
}
customElements.define('data-property-selector', DataPropertySelector, {extends: 'div'});
/*
* Checkbox with label for data selection, declared as custom element.
*
* @prop {string} group - Data source group name (selected from left sidebar).
* @prop {string} source - Data source category (sub-group).
* @prop {string} prop - Data source property name.
*/
class DataPropertyCheckbox extends HTMLDivElement
{
/* Render checkbox with label, listen for events. */
render()
{
this.innerHTML = '';
const prop = this.getAttribute('prop');
const source = this.getAttribute('source');
const group = this.getAttribute('group');
this.classList.add('checkbox-inline');
const itemOption = document.createElement('input');
itemOption.setAttribute('type', 'checkbox');
itemOption.setAttribute('prop', !!group ? `${source}.${prop}` : source);
this.appendChild(itemOption);
const itemLabel = document.createElement('label');
itemLabel.appendChild(document.createTextNode(!!group ? dataSourceLang[group][source][prop] : dataSourceLang[prop][source].common));
/* Setup event listeners. */
itemLabel.addEventListener('click', (e =>
{
itemOption.checked = !itemOption.checked;
this.handleCheck(itemOption.checked);
}).bind(this));
itemOption.addEventListener('change', (e =>
{
this.handleCheck(itemOption.checked);
}).bind(this));
this.appendChild(itemLabel);
}
/* Handle check all for checkbox group, bidirectionally. And push selections into datasource input. */
handleCheck(newValue)
{
if(this.classList.contains('obj-view-header'))
{
const objViewBody = this.parentElement.querySelector('.obj-view-body');
for(const item of objViewBody.querySelectorAll('.obj-view-item input[type="checkbox"]')) item.checked = newValue;
}
else
{
const objViewBody = this.parentElement;
const headerItem = objViewBody.parentElement.querySelector('.obj-view-header input[type="checkbox"]');
const groupItems = objViewBody.querySelectorAll('.obj-view-item input[type="checkbox"]');
let allChecked = true;
for(const item of groupItems)
{
if(!item.checked) allChecked = false;
}
headerItem.checked = allChecked;
}
window.dataSourceStore.sync();
}
/* Handle attr change, rerender. */
attributeChangedCallback()
{
/* Skip rendering if props not complete. */
if(!this.hasAttribute('prop') || !this.hasAttribute('source')) return;
this.render();
}
/* Define observed attr. */
static get observedAttributes()
{
return ['group', 'source', 'prop'];
}
}
customElements.define('data-property-checkbox', DataPropertyCheckbox, {extends: 'div'});
</script>
<div id='mainMenu' class='clearfix' style='display: flex; flex-direction: row;'>
<?php echo html::backButton("<i class='icon icon-back icon-sm'></i> $lang->goback", '', 'btn btn-info');?>
<?php include 'promptdesignprogressbar.html.php';?>
<?php echo html::submitButton("<i class='icon icon-save icon-sm'></i> $lang->save", '', 'btn btn-primary');?>
</div>
<div id='mainContent' class='main-content' style='height: calc(100vh - 120px);'>
<form class='load-indicator main-form form-ajax' method='post' style='height: 100%;'>
<div class='center-wrapper'>
<div class='center-content'>
<div id='data-selector'>
<div id='data-sources'>
<div id='data-categories'>
<div class='heading text-center'>
<h4><?php echo $lang->ai->prompts->selectDataSource;?></h4>
</div>
<div id='data-category-select'>
<ul>
<?php foreach(array_keys($dataSource) as $index => $objectGroupKey):?>
<li data-group-key='<?php echo $objectGroupKey;?>'>
<a class='btn btn-block <?php echo $index == 0 ? ' active btn-info' : ' btn-link';?>'>
<?php echo $lang->ai->dataSource[$objectGroupKey]['common'];?>
</a>
</li>
<?php endforeach;?>
</ul>
</div>
</div>
<div id='data-properties'>
<div class='heading'>
<h4><?php echo $lang->ai->prompts->selectData;?></h4>
<small class='text-gray'><?php echo $lang->ai->prompts->selectDataTip;?></small>
</div>
<div id='data-property-select'>
<div id='data-property-selector' is='data-property-selector' object-group='story'></div>
</div>
</div>
</div>
<div id='data-selected'>
<div class='heading'>
<h4><?php echo $lang->ai->prompts->selectedFormat;?></h4>
</div>
<div id='data-selected-items'>
<?php echo html::hidden('datasource');?>
</div>
</div>
</div>
<div style='display: flex; flex-grow: 1; flex-direction: column-reverse;'>
<div style='display: flex; justify-content: center;'><?php echo html::submitButton($lang->ai->nextStep, 'disabled');?></div>
</div>
</div>
</div>
</form>
</div>
<script>
$(function()
{
/* Handle category switching. */
$('#data-category-select > ul > li > a').click(function()
{
if($(this).hasClass('active')) return;
$('#data-category-select > ul > li > a').removeClass('active btn-info').addClass('btn-link');
$(this).addClass('active btn-info').removeClass('btn-link');
$('#data-property-selector').attr('object-group', $(this).parent().attr('data-group-key'));
/* Reset selected data. */
window.dataSourceStore.reset();
});
/* Disable checkboxes to prevent them getting posted as form data. */
$('form').submit(function()
{
$('#data-property-selector input[type="checkbox"]').each(function() {$(this).attr('disabled', 'disabled');});
});
});
</script>
<?php include '../../common/view/footer.html.php';?>