专长:增强 AI 设置和搜索功能
- 更新了 AI 设置页面,统一了 AI 补充、自然语言搜索、入站预处理和标签标准化的配置。 - 在编辑页面新增了 AI 标签和笔记标准化板块,包括建议预览及应用功能。 - 改进的搜索页面,支持带有示例的自然语言查询和模糊选择下拉菜单。 - 增强的搜索结果显示,包含更多匹配信息和查看 AI 搜索过程的模态。 - 更新新组件样式,优化布局以提升用户体验。
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
<header class="hero slim">
|
||||
<div>
|
||||
<h1>AI参数设置</h1>
|
||||
<p>在此修改硅基流动 API 和补货建议参数</p>
|
||||
<p>在此统一配置 AI 补货、自然语言搜索、入库预处理与标签标准化参数</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="btn btn-light" href="{{ url_for('types_page') }}">返回仓库概览</a>
|
||||
@@ -27,7 +27,8 @@
|
||||
|
||||
<section class="panel">
|
||||
<form class="form-grid" method="post">
|
||||
<h2 class="full">AI补货建议参数</h2>
|
||||
<h2 class="full">通用 AI 参数</h2>
|
||||
<p class="hint full">以下参数会同时用于 AI 入库预处理、自然语言搜索、重复巡检、补货建议、标签与备注标准化。</p>
|
||||
<label>
|
||||
API URL *
|
||||
<input type="text" name="api_url" required value="{{ settings.api_url }}" placeholder="https://api.siliconflow.cn/v1/chat/completions">
|
||||
|
||||
@@ -36,15 +36,15 @@
|
||||
<input type="hidden" name="q" value="{{ search_query or '' }}">
|
||||
<label>
|
||||
料号 *
|
||||
<input type="text" name="part_no" required value="{{ component.part_no if component else '' }}" aria-label="料号" placeholder="如 STM32F103C8T6">
|
||||
<input id="part-no-input" type="text" name="part_no" required value="{{ component.part_no if component else '' }}" aria-label="料号" placeholder="如 STM32F103C8T6">
|
||||
</label>
|
||||
<label>
|
||||
名称 *
|
||||
<input type="text" name="name" required value="{{ component.name if component else '' }}" aria-label="名称" placeholder="如 MCU STM32F103C8T6">
|
||||
<input id="name-input" type="text" name="name" required value="{{ component.name if component else '' }}" aria-label="名称" placeholder="如 MCU STM32F103C8T6">
|
||||
</label>
|
||||
<label>
|
||||
规格
|
||||
<input type="text" name="specification" value="{{ component.specification if component else '' }}" placeholder="如 Cortex-M3 / LQFP-48">
|
||||
<input id="specification-input" type="text" name="specification" value="{{ component.specification if component else '' }}" placeholder="如 Cortex-M3 / LQFP-48">
|
||||
</label>
|
||||
<label>
|
||||
数量
|
||||
@@ -52,7 +52,7 @@
|
||||
</label>
|
||||
<label class="full">
|
||||
备注
|
||||
<textarea name="note" rows="3" placeholder="如 LCSC item 9243">{{ component.note if component else '' }}</textarea>
|
||||
<textarea id="note-input" name="note" rows="3" placeholder="如 LCSC item 9243">{{ component.note if component else '' }}</textarea>
|
||||
</label>
|
||||
<label class="full">
|
||||
<input type="checkbox" name="confirm_merge" value="1">
|
||||
@@ -82,6 +82,42 @@
|
||||
</section>
|
||||
|
||||
<aside class="entry-sidebar">
|
||||
<section class="panel quick-inbound-panel">
|
||||
<h2>AI 标签与备注标准化</h2>
|
||||
<p class="hint">生成更适合标签打印的短名称,并自动补全统一搜索关键词。确认后再回填到表单。</p>
|
||||
<p class="hint" id="standardize-status" aria-live="polite"></p>
|
||||
<section class="ai-standardize-preview" id="standardize-preview" hidden>
|
||||
<div class="standardize-grid">
|
||||
<div>
|
||||
<strong>短标签</strong>
|
||||
<p id="standardize-short-label">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>建议名称</strong>
|
||||
<p id="standardize-name">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>建议规格</strong>
|
||||
<p id="standardize-specification">-</p>
|
||||
</div>
|
||||
<div class="full">
|
||||
<strong>建议备注</strong>
|
||||
<p id="standardize-note">-</p>
|
||||
</div>
|
||||
<div class="full">
|
||||
<strong>搜索关键词</strong>
|
||||
<div class="match-tags" id="standardize-keywords"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn" type="button" id="apply-standardization-btn">应用到表单</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="actions">
|
||||
<button class="btn btn-light" type="button" id="generate-standardization-btn">生成标准化建议</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel quick-inbound-panel">
|
||||
<h2>立创编号入库</h2>
|
||||
<p class="hint">当前编辑位置: {{ slot_code }}。仅支持粘贴立创商品详情页链接,系统会自动提取 itemId 并查询。</p>
|
||||
@@ -126,5 +162,104 @@
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var partNoInput = document.getElementById('part-no-input');
|
||||
var nameInput = document.getElementById('name-input');
|
||||
var specificationInput = document.getElementById('specification-input');
|
||||
var noteInput = document.getElementById('note-input');
|
||||
var generateBtn = document.getElementById('generate-standardization-btn');
|
||||
var applyBtn = document.getElementById('apply-standardization-btn');
|
||||
var status = document.getElementById('standardize-status');
|
||||
var preview = document.getElementById('standardize-preview');
|
||||
var shortLabelNode = document.getElementById('standardize-short-label');
|
||||
var nameNode = document.getElementById('standardize-name');
|
||||
var specificationNode = document.getElementById('standardize-specification');
|
||||
var noteNode = document.getElementById('standardize-note');
|
||||
var keywordNode = document.getElementById('standardize-keywords');
|
||||
var latestSuggestion = null;
|
||||
|
||||
if (!partNoInput || !nameInput || !specificationInput || !noteInput || !generateBtn || !applyBtn || !status || !preview) {
|
||||
return;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderSuggestion(suggestion) {
|
||||
latestSuggestion = suggestion;
|
||||
preview.hidden = false;
|
||||
shortLabelNode.textContent = suggestion.short_label || '-';
|
||||
nameNode.textContent = suggestion.name || '-';
|
||||
specificationNode.textContent = suggestion.specification || '-';
|
||||
noteNode.textContent = suggestion.note || '-';
|
||||
keywordNode.innerHTML = (suggestion.keywords || []).map(function (keyword) {
|
||||
return '<span class="tag">' + escapeHtml(keyword) + '</span>';
|
||||
}).join('') || '<span class="tag">-</span>';
|
||||
}
|
||||
|
||||
generateBtn.addEventListener('click', function () {
|
||||
if (!partNoInput.value.trim() && !nameInput.value.trim()) {
|
||||
status.textContent = '请先填写料号或名称';
|
||||
return;
|
||||
}
|
||||
|
||||
generateBtn.disabled = true;
|
||||
status.textContent = '正在生成标准化建议...';
|
||||
|
||||
var payload = new URLSearchParams();
|
||||
payload.set('part_no', partNoInput.value || '');
|
||||
payload.set('name', nameInput.value || '');
|
||||
payload.set('specification', specificationInput.value || '');
|
||||
payload.set('note', noteInput.value || '');
|
||||
|
||||
fetch('{{ url_for('ai_component_standardize') }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
},
|
||||
body: payload.toString()
|
||||
}).then(function (resp) {
|
||||
return resp.json().then(function (data) {
|
||||
if (!resp.ok || !data.ok) {
|
||||
throw new Error(data.message || '生成失败');
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}).then(function (data) {
|
||||
renderSuggestion(data.suggestion || {});
|
||||
status.textContent = data.parse_notice || '标准化建议已生成,可先预览再应用';
|
||||
}).catch(function (error) {
|
||||
status.textContent = '生成失败: ' + error.message;
|
||||
}).finally(function () {
|
||||
generateBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
applyBtn.addEventListener('click', function () {
|
||||
if (!latestSuggestion) {
|
||||
status.textContent = '请先生成标准化建议';
|
||||
return;
|
||||
}
|
||||
|
||||
if (latestSuggestion.name) {
|
||||
nameInput.value = latestSuggestion.name;
|
||||
}
|
||||
if (latestSuggestion.specification) {
|
||||
specificationInput.value = latestSuggestion.specification;
|
||||
}
|
||||
if (latestSuggestion.note) {
|
||||
noteInput.value = latestSuggestion.note;
|
||||
}
|
||||
status.textContent = '建议已回填到表单,确认无误后再保存';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<header class="hero slim">
|
||||
<div>
|
||||
<h1>快速搜索</h1>
|
||||
<p>按料号或名称搜索,点击可跳转到对应位置并直接出库</p>
|
||||
<p>支持自然语言搜索,自动映射到料号、名称、规格和备注组合查询</p>
|
||||
</div>
|
||||
<nav class="hero-actions">
|
||||
<a class="btn btn-light" href="{{ url_for('index') }}">返回首页</a>
|
||||
@@ -28,12 +28,57 @@
|
||||
|
||||
<section class="panel">
|
||||
<form id="search-form" method="get" action="{{ url_for('search_page') }}" class="search-row">
|
||||
<input id="search-input" type="search" name="q" placeholder="输入料号或名称" value="{{ keyword }}" aria-label="搜索关键字">
|
||||
<input id="search-input" type="search" name="q" placeholder="如 3.3V 稳压芯片、0805 常用电阻、USB 相关器件" value="{{ keyword }}" aria-label="搜索关键字">
|
||||
<select id="fuzziness-select" name="fuzziness" aria-label="匹配宽松度">
|
||||
{% for key, profile in fuzziness_profiles.items() %}
|
||||
<option value="{{ key }}" {% if fuzziness == key %}selected{% endif %}>{{ profile.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn" type="submit">搜索</button>
|
||||
</form>
|
||||
<div class="search-examples">
|
||||
<button class="chip" type="button" data-example="3.3V 稳压芯片">3.3V 稳压芯片</button>
|
||||
<button class="chip" type="button" data-example="0805 常用电阻">0805 常用电阻</button>
|
||||
<button class="chip" type="button" data-example="USB 相关器件">USB 相关器件</button>
|
||||
</div>
|
||||
<p class="hint">出库只需要输入数量,系统会自动扣减库存并记录统计。</p>
|
||||
<p class="hint">当前宽松度: {{ fuzziness_profiles[fuzziness].label }}(严格更精准,宽松更容易召回)</p>
|
||||
</section>
|
||||
|
||||
{% if search_plan %}
|
||||
<section class="panel search-analysis">
|
||||
<div class="group-title-row">
|
||||
<h2>搜索解析</h2>
|
||||
<div class="actions">
|
||||
<span class="hint">模式: {{ 'AI解析' if search_plan.mode == 'ai' else '规则解析' }}</span>
|
||||
<button class="btn btn-light" type="button" id="show-search-trace">查看AI过程</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">{{ search_plan.summary }}</p>
|
||||
{% if search_parse_notice %}
|
||||
<p class="notice">{{ search_parse_notice }}</p>
|
||||
{% endif %}
|
||||
<div class="search-map">
|
||||
<div>
|
||||
<strong>料号</strong>
|
||||
<p>{{ ' / '.join(search_plan.field_map.part_no) if search_plan.field_map.part_no else '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>名称</strong>
|
||||
<p>{{ ' / '.join(search_plan.field_map.name) if search_plan.field_map.name else '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>规格</strong>
|
||||
<p>{{ ' / '.join(search_plan.field_map.specification) if search_plan.field_map.specification else '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>备注</strong>
|
||||
<p>{{ ' / '.join(search_plan.field_map.note) if search_plan.field_map.note else '-' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="panel">
|
||||
<h2>搜索结果</h2>
|
||||
<div class="table-wrap">
|
||||
@@ -45,6 +90,7 @@
|
||||
<th>规格</th>
|
||||
<th>库存</th>
|
||||
<th>位置</th>
|
||||
<th>匹配说明</th>
|
||||
<th>跳转</th>
|
||||
<th>出库</th>
|
||||
</tr>
|
||||
@@ -58,10 +104,32 @@
|
||||
<td>{{ c.specification or '-' }}</td>
|
||||
<td>{{ c.quantity }}</td>
|
||||
<td>{{ row.box_name }} / {{ row.slot_code }}</td>
|
||||
<td>
|
||||
<div>{{ row.match_summary }}</div>
|
||||
<div class="hint">综合分: {{ '%.1f'|format(row.match_score) }}</div>
|
||||
{% if row.matched_terms %}
|
||||
<div class="match-tags">
|
||||
{% for term in row.matched_terms %}
|
||||
<span class="tag">{{ term }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if row.fuzzy_matches %}
|
||||
<details class="fuzzy-details">
|
||||
<summary>模糊命中详情</summary>
|
||||
<ul>
|
||||
{% for item in row.fuzzy_matches %}
|
||||
<li>{{ item.term }} ({{ item.score }})</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a class="btn btn-light" href="{{ row.edit_url }}">进入位置</a></td>
|
||||
<td>
|
||||
<form method="post" action="{{ url_for('quick_outbound', component_id=c.id) }}" class="search-outbound-form">
|
||||
<input type="hidden" name="q" value="{{ keyword }}">
|
||||
<input type="hidden" name="fuzziness" value="{{ fuzziness }}">
|
||||
<input type="number" name="amount" min="1" step="1" placeholder="数量" required class="outbound-amount">
|
||||
<button class="btn" type="submit">出库</button>
|
||||
</form>
|
||||
@@ -69,13 +137,47 @@
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7">{% if keyword %}未找到匹配元件{% else %}先输入关键字进行搜索{% endif %}</td>
|
||||
<td colspan="8">{% if keyword %}未找到匹配元件{% else %}先输入关键字进行搜索{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="modal-backdrop" id="search-trace-modal" hidden>
|
||||
<div class="modal-card panel" role="dialog" aria-modal="true" aria-labelledby="search-trace-title">
|
||||
<div class="group-title-row">
|
||||
<h2 id="search-trace-title">AI 搜索工作过程</h2>
|
||||
<button class="btn btn-light" type="button" id="close-search-trace">关闭</button>
|
||||
</div>
|
||||
{% if search_trace %}
|
||||
<ol class="trace-steps">
|
||||
<li>收到自然语言输入: {{ search_trace.query }}</li>
|
||||
<li>规则拆分候选字段,生成 fallback 计划</li>
|
||||
<li>{% if search_trace.used_ai %}调用 AI 解析并返回字段映射{% else %}未调用 AI(参数未配置){% endif %}</li>
|
||||
<li>{% if search_trace.used_fallback %}最终回退规则计划{% else %}最终采用 AI 计划{% endif %}</li>
|
||||
<li>对每条库存记录执行多字段模糊评分并排序</li>
|
||||
<li>当前宽松度: {{ search_trace.fuzziness_label if search_trace.fuzziness_label else '-' }}</li>
|
||||
</ol>
|
||||
|
||||
{% if search_trace.ai_error %}
|
||||
<p class="alert">AI 错误: {{ search_trace.ai_error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>最终计划</h3>
|
||||
<pre class="trace-block">{{ search_plan|tojson(indent=2) }}</pre>
|
||||
<h3>规则兜底计划</h3>
|
||||
<pre class="trace-block">{{ search_trace.fallback_plan|tojson(indent=2) }}</pre>
|
||||
{% if search_trace.ai_raw %}
|
||||
<h3>AI 原始返回</h3>
|
||||
<pre class="trace-block">{{ search_trace.ai_raw }}</pre>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="hint">当前没有可展示的过程数据。</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
@@ -92,6 +194,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-example]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
if (!searchInput || !searchForm) {
|
||||
return;
|
||||
}
|
||||
searchInput.value = button.getAttribute('data-example') || '';
|
||||
searchForm.requestSubmit();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.outbound-amount').forEach(function (input) {
|
||||
input.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
@@ -103,6 +215,42 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var traceOpenBtn = document.getElementById('show-search-trace');
|
||||
var traceCloseBtn = document.getElementById('close-search-trace');
|
||||
var traceModal = document.getElementById('search-trace-modal');
|
||||
function closeTraceModal() {
|
||||
if (!traceModal) {
|
||||
return;
|
||||
}
|
||||
traceModal.hidden = true;
|
||||
document.body.classList.remove('modal-open');
|
||||
}
|
||||
|
||||
if (traceOpenBtn && traceModal) {
|
||||
traceOpenBtn.addEventListener('click', function () {
|
||||
traceModal.hidden = false;
|
||||
document.body.classList.add('modal-open');
|
||||
});
|
||||
}
|
||||
|
||||
if (traceCloseBtn) {
|
||||
traceCloseBtn.addEventListener('click', closeTraceModal);
|
||||
}
|
||||
|
||||
if (traceModal) {
|
||||
traceModal.addEventListener('click', function (event) {
|
||||
if (event.target === traceModal) {
|
||||
closeTraceModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeTraceModal();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user