3 Commits

Author SHA1 Message Date
wolfcode
a7a3ddef8b refactor(easy-admin): improve AI optimization feature and enhance user experience
- Remove line-height style from AI optimization content div
- Add smooth scrolling to the end of the content when streaming output Move input validation to the beginning of the aiOptimization function
2025-03-10 11:54:41 +08:00
wolfcode
51f2cbc0f4 feat(mall): 新增AI对话支持 add AI optimization function for product titles
- Integrate AI functionality to optimize product titles
- Add AI optimization button in product add and edit pages
- Implement AI chat service for generating optimized titles
- Update backend to support AI optimization requests
- Add error handling and loading state for AI optimization process
2025-03-07 17:58:11 +08:00
wolfcode
8acf9f3f6c Update .example.env 2025-03-06 11:02:49 +08:00
8 changed files with 185 additions and 8 deletions

View File

@@ -14,7 +14,7 @@ DB_PORT=3306
DB_CHARSET=utf8mb4
DB_PREFIX=ea8_
# 限流器开关
# 限流器开关 若启动需要配置 Redis 服务
RATE_LIMITING_STATUS=false
# Redis配置

View File

@@ -11,6 +11,8 @@ use app\admin\service\annotation\NodeAnnotation;
use app\Request;
use think\App;
use think\response\Json;
use Wolfcode\Ai\Enum\AiType;
use Wolfcode\Ai\Service\AiChatService;
#[ControllerAnnotation(title: '商城商品管理')]
class Goods extends AdminController
@@ -72,4 +74,62 @@ class Goods extends AdminController
{
return '这里演示方法不需要经过登录验证';
}
#[NodeAnnotation(title: 'AI优化', auth: true)]
public function aiOptimization(Request $request): void
{
$message = $request->post('message');
if (empty($message)) $this->error('请输入内容');
// 演示环境下 默认返回的内容
if ($this->isDemo) {
$content = <<<EOF
演示环境中 默认返回的内容
我来帮你优化这个标题,让它更有吸引力且更符合电商平台的搜索逻辑:
"商务男士高端定制马克杯 | 办公室精英必备 | 优质陶瓷防烫手柄"
这个优化后的标题:
1. 突出了目标用户群体(商务男士)
2. 强调了产品定位(高端定制)
3. 点明了使用场景(办公室)
4. 添加了材质和功能特点(优质陶瓷、防烫手柄)
5. 使用了吸引人的关键词(精英必备)
这样的标题不仅更具体,也更容易被搜索引擎识别,同时能精准触达目标客户群。您觉得这个版本如何?
EOF;
$choices = [['message' => [
'role' => 'assistant',
'content' => $content,
]]];
$this->success('success', compact('choices'));
}
try {
$result = AiChatService::instance()
// 当使用推理模型时,可能存在超时的情况,所以需要设置超时时间为 0
// ->setTimeLimit(0)
// 请替换为您需要的模型类型
->setAiType(AiType::QWEN)
// 如果需要指定模型的 API 地址,可自行设置
// ->setAiUrl('https://xxx.com')
// 请替换为您的模型
->setAiModel('qwen-plus')
// 请替换为您的 API KEY
->setAiKey('sk-1234567890')
// 此内容会作为系统提示,会影响到回答的内容 当前仅作为测试使用
->setSystemContent('你现在是一位资深的海外电商产品经理')
->chat($message);
$choices = $result['choices'];
}catch (\Throwable $exception) {
$choices = [['message' => [
'role' => 'assistant',
'content' => $exception->getMessage(),
]]];
}
$this->success('success', compact('choices'));
}
}

View File

@@ -35,7 +35,7 @@ class CheckAuth
!$check && $this->error('无权限访问');
// 判断是否为演示环境
if (env('EASYADMIN.IS_DEMO', false) && $request->isPost()) {
if (!in_array($currentNode, ['system.log/record', ''])) $this->error('演示环境下不允许修改');
if (!in_array($currentNode, ['system.log/record', 'mall.goods/aiOptimization'])) $this->error('演示环境下不允许修改');
}
}
return $next($request);

View File

@@ -26,9 +26,18 @@
<!-- </div>-->
<div class="layui-form-item">
<label class="layui-form-label">商品标题</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入商品标题" value="">
<div class="layui-row">
<label class="layui-form-label required">商品标题</label>
<div class="layui-input-block layui-col-space5">
<div class="layui-col-xs10">
<div class="layui-input-wrap">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入商品标题" value="">
</div>
</div>
<div class="layui-col-xs2">
<button class="layui-btn layui-bg-purple layui-btn-fluid" type="button" lay-on="AiOptimization">AI优化</button>
</div>
</div>
</div>
</div>

View File

@@ -26,12 +26,22 @@
<!-- </div>-->
<div class="layui-form-item">
<label class="layui-form-label">商品标题</label>
<div class="layui-input-block">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入商品标题" value="{$row.title|default=''}">
<div class="layui-row">
<label class="layui-form-label required">商品标题</label>
<div class="layui-input-block layui-col-space5">
<div class="layui-col-xs10">
<div class="layui-input-wrap">
<input type="text" name="title" class="layui-input" lay-verify="required" placeholder="请输入商品标题" value="{$row.title|default=''}">
</div>
</div>
<div class="layui-col-xs2">
<button class="layui-btn layui-bg-purple layui-btn-fluid" type="button" lay-on="AiOptimization">AI优化</button>
</div>
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label required">商品LOGO</label>
<div class="layui-input-block layuimini-upload">

View File

@@ -37,6 +37,7 @@
"wolf-leo/phplogviewer": "^0.11.3",
"wolfcode/authenticator": "^0.0.6",
"wolfcode/rate-limiting": "^0.1.0",
"wolfcode/php-ai": "^0.1.2",
"ext-json": "*",
"ext-mysqli": "*",
"ext-pdo": "*"

View File

@@ -85,13 +85,55 @@ define(["jquery", "easy-admin"], function ($, ea) {
ea.listen();
},
add: function () {
layui.util.on({
AiOptimization: function (data) {
let layOn = $(data).attr('lay-on')
$(data).attr('lay-on', layOn + 'Loading')
aiOptimization(data)
},
})
ea.listen();
},
edit: function () {
layui.util.on({
AiOptimization: function (data) {
let layOn = $(data).attr('lay-on')
$(data).attr('lay-on', layOn + 'Loading')
aiOptimization(data)
},
})
ea.listen();
},
stock: function () {
ea.listen();
},
};
function aiOptimization(data) {
let layOn = $(data).attr('lay-on')
let title = $('input[name="title"]').val()
// 告诉AI 你需要做什么
let message = `优化这个标题 ${title}`
if ($.trim(title) === '') {
ea.msg.error('标题不能为空', function () {
$(data).attr('lay-on', layOn.split('Loading')[0])
})
return false
}
let url = ea.url('mall.goods/aiOptimization')
ea.request.post({url: url, data: {message: message}}, function (res) {
let content = res.data?.choices[0]?.message?.content
// stream 为true 时AI 内容会逐字输出
let stream = true
ea.ai.chat(content, {stream: stream}, function () {
$(data).attr('lay-on', layOn.split('Loading')[0])
})
}, function (error) {
ea.msg.error(error.msg, function () {
$(data).attr('lay-on', layOn.split('Loading')[0])
})
})
}
});

View File

@@ -1761,6 +1761,61 @@ define(["jquery", "tableSelect", "miniTheme", "xmSelect"], function ($, tableSel
}
},
},
ai: {
chat: function (content, options, cancel) {
let id = 'chat_' + (new Date()).getTime()
layer.open({
'title': options?.title || 'AI建议',
type: 1,
area: options?.area || ['42%', '60%'],
shade: options?.shade || 0,
shadeClose: options?.shadeClose || false,
scrollbar: options?.scrollbar || false,
maxmin: options?.maxmin || true,
anim: options?.anim || 0,
content: `<div style="padding: 20px;white-space: pre-wrap;" id="${id}"></div>`,
success: function (layero, index) {
let elem = document.getElementById(id)
if (options?.stream) {
clearTimeout(aiStreamTimeout)
aiStreamCurrentIndex = 0
setTimeout(() => {
admin.ai.streamOutput(elem, content)
}, 300)
} else {
content = content.replace(/\r\n/g, '<br>').replace(/\n/g, '<br>')
setTimeout(() => {
elem.innerHTML = content
}, 100)
}
},
cancel: function (index, layero) {
cancel()
}
})
},
streamOutput: function (dom, htmlContent) {
const chunkSize = 1;
let length = htmlContent.length;
if (aiStreamCurrentIndex < length) {
const endIndex = Math.min(aiStreamCurrentIndex + chunkSize, length);
const chunk = htmlContent.slice(aiStreamCurrentIndex, endIndex);
const tempDiv = document.createElement('div');
tempDiv.innerHTML = chunk;
while (tempDiv.firstChild) {
dom.appendChild(tempDiv.firstChild);
}
aiStreamCurrentIndex = endIndex;
aiStreamTimeout = setTimeout(() => {
admin.ai.streamOutput(dom, htmlContent);
dom.scrollIntoView({behavior: "smooth", block: "end"});
}, 60);
}
}
},
};
var aiStreamCurrentIndex = 0;
var aiStreamTimeout = null;
return admin;
});