v1.1.3: default server 0.0.0.0:80 + row-level clustering + filter UI + dataset selector

This commit is contained in:
PM-pinou
2026-07-21 14:15:56 +08:00
parent ec83d5df85
commit d20b1c3b28
4 changed files with 55 additions and 45 deletions
+2 -19
View File
@@ -899,7 +899,7 @@ def manual_run_analysis(request):
algorithm = body.get('algorithm', 'hdbscan')
min_cluster_size = int(body.get('min_cluster_size', 5))
head = body.get('head')
cluster_mode = body.get('cluster_mode', 'entity')
cluster_mode = body.get('cluster_mode', 'raw')
def _analysis_fn(run, ctx):
from analysis.session_store import SessionStore
@@ -909,7 +909,7 @@ def manual_run_analysis(request):
min_cluster_size = ctx['min_cluster_size']
head = ctx.get('head')
pk = ctx['pk']
cluster_mode = ctx.get('cluster_mode', 'entity')
cluster_mode = ctx.get('cluster_mode', 'raw')
store = SessionStore()
try:
@@ -931,23 +931,6 @@ def manual_run_analysis(request):
ds_id = upload_ds_id if store.get_dataset(upload_ds_id) else f'upload_{pk}'
# ── Cluster mode: entity aggregation vs raw ──
if cluster_mode == 'entity':
# Build entity profiles first, then cluster on entity dataset
from analysis.tool_registry import _handle_build_entity_profiles
import asyncio
entity_result = asyncio.new_event_loop().run_until_complete(
_handle_build_entity_profiles(dataset_id=ds_id, auto_detect=True)
)
if 'error' in entity_result:
run.error_message = f'实体聚合失败: {entity_result["error"]}'
run.status = 'failed'
run.save(update_fields=['status', 'error_message'])
return
ds_id = entity_result['dataset_id']
run.entity_count = entity_result.get('n_entities')
run.save(update_fields=['entity_count'])
# Use the unified clustering pipeline (clustering → extraction → PCA)
from analysis.views import _run_clustering_pipeline
_run_clustering_pipeline(
+2 -2
View File
@@ -1,6 +1,6 @@
server:
host: 127.0.0.1
port: 8000
host: 0.0.0.0
port: 80
debug: false
data:
schema_strict: false
+2 -2
View File
@@ -10,8 +10,8 @@ class Config(BaseModel):
"""Typed configuration for TianXuan."""
class Server(BaseModel):
host: str = '127.0.0.1'
port: int = 8000
host: str = '0.0.0.0'
port: int = 80
debug: bool = False
class Data(BaseModel):
+49 -22
View File
@@ -111,19 +111,11 @@
</div>
</div>
<!-- Cluster Mode -->
<div class="card" id="clusterModeCard">
<label style="font-weight:600;">聚类模式</label>
<div style="display:flex;gap:1.5rem;margin-top:0.5rem;">
<label><input type="radio" name="clusterMode" value="entity" checked onchange="clusterMode='entity'"> 实体聚合后聚类(默认)</label>
<label><input type="radio" name="clusterMode" value="raw" onchange="clusterMode='raw'"> 逐行直接聚类</label>
</div>
</div>
<!-- Filter builder card -->
<div class="card" id="filterCard" style="display:none;">
<div class="card" id="filterCard" style="background:#f0f4ff;border-color:#c8d6e5;">
<h3>🔍 数据筛选</h3>
<div id="filterRows">
<div id="filterNoDataset" style="text-align:center;color:#999;padding:1rem;font-size:0.9rem;">选择数据集后可用</div>
<div id="filterRows" style="display:none;">
<div class="filter-row" data-idx="0" style="display:flex;gap:0.5rem;align-items:center;margin-bottom:0.4rem;">
<select class="filter-col" style="flex:1;padding:0.35rem;border:1px solid #ccc;border-radius:4px;"><option value="">-- 列 --</option></select>
<select class="filter-op" style="width:130px;padding:0.35rem;border:1px solid #ccc;border-radius:4px;">
@@ -139,12 +131,13 @@
<button class="btn" onclick="removeFilterRow(this)" style="background:#dc3545;color:#fff;font-size:0.8rem;padding:0.2rem 0.5rem;display:none;"></button>
</div>
</div>
<div style="margin:0.5rem 0;display:flex;gap:0.5rem;align-items:center;">
<div id="filterControls" style="display:none;margin:0.5rem 0;gap:0.5rem;align-items:center;">
<label style="font-size:0.85rem;white-space:nowrap;">逻辑:</label>
<select id="filterLogic" style="padding:0.35rem;border:1px solid #ccc;border-radius:4px;">
<option value="and">AND</option><option value="or">OR</option>
</select>
<button class="btn" onclick="addFilterRow()" style="font-size:0.8rem;background:#e8f0fe;color:#4361ee;">+ 添加条件</button>
<button class="btn" onclick="resetFilter()" style="font-size:0.8rem;background:#fce4ec;color:#c62828;">重置</button>
<button class="btn btn-primary" onclick="applyFilter()" style="margin-left:auto;">应用筛选</button>
</div>
<div id="filterResult" style="display:none;margin-top:0.5rem;padding:0.5rem;background:#e8f5e9;border-radius:4px;font-size:0.85rem;"></div>
@@ -181,7 +174,6 @@ const CATEGORIES = {
let stepCounter = 0;
const workflow = [];
let running = false;
var clusterMode = 'entity';
// ── Dataset selector with status badges ──
const DATASETS_WITH_STATUS = {{ datasets_with_status_json|safe }};
@@ -362,16 +354,16 @@ async function executeStep(idx) {
resultDiv.style.display = 'none';
const params = collectParams(idx);
// Cluster mode: skip build_entity_profiles in raw mode
if (clusterMode === 'raw' && step.tool.name === 'build_entity_profiles') {
// Raw row-level clustering: skip entity aggregation entirely
if (step.tool.name === 'build_entity_profiles') {
step.status = 'done';
resultDiv.textContent = '[已跳过] 逐行直接聚类模式,无需实体聚合';
resultDiv.style.display = 'block';
renderSteps();
return;
}
// Pass cluster_mode to run_clustering tool
if (clusterMode === 'raw' && step.tool.name === 'run_clustering') {
// Always cluster raw rows directly
if (step.tool.name === 'run_clustering') {
params.cluster_mode = 'raw';
}
@@ -578,9 +570,30 @@ const DATASET_COLUMNS = {
// ── Filter builder ──
function onDatasetChange() {
const dsId = document.getElementById('datasetSelect').value;
const filterCard = document.getElementById('filterCard');
if (!dsId) { filterCard.style.display = 'none'; return; }
filterCard.style.display = 'block';
const noDatasetMsg = document.getElementById('filterNoDataset');
const filterRows = document.getElementById('filterRows');
const filterControls = document.getElementById('filterControls');
const filterResult = document.getElementById('filterResult');
if (!dsId) {
// No dataset selected: show placeholder, hide filter UI
noDatasetMsg.style.display = 'block';
filterRows.style.display = 'none';
filterControls.style.display = 'none';
filterResult.style.display = 'none';
// Clear column dropdowns
document.querySelectorAll('.filter-col').forEach(sel => {
sel.innerHTML = '<option value="">-- 请先选择数据集 --</option>';
});
return;
}
// Dataset selected: show filter UI, hide placeholder
noDatasetMsg.style.display = 'none';
filterRows.style.display = 'block';
filterControls.style.display = 'flex';
// Populate column dropdowns
const cols = DATASET_COLUMNS[dsId] || [];
document.querySelectorAll('.filter-col').forEach(sel => {
const cur = sel.value;
@@ -591,7 +604,7 @@ function onDatasetChange() {
function addFilterRow() {
const rows = document.querySelectorAll('.filter-row');
if (rows.length >= 2) return; // MVP: max 2 rows
if (rows.length >= 5) return; // max 5 rows
const template = rows[0].cloneNode(true);
template.querySelector('.filter-val').value = '';
template.querySelector('.filter-col').selectedIndex = 0;
@@ -608,6 +621,20 @@ function removeFilterRow(btn) {
btn.closest('.filter-row').remove();
}
function resetFilter() {
// Remove all rows except the first, then reset the first row
const rows = document.querySelectorAll('.filter-row');
for (let i = rows.length - 1; i >= 1; i--) {
rows[i].remove();
}
const firstRow = rows[0];
firstRow.querySelector('.filter-val').value = '';
firstRow.querySelector('.filter-col').selectedIndex = 0;
firstRow.querySelector('.filter-op').selectedIndex = 0;
firstRow.querySelector('button').style.display = 'none';
document.getElementById('filterResult').style.display = 'none';
}
function collectFilters() {
const filters = [];
document.querySelectorAll('.filter-row').forEach(row => {
@@ -650,7 +677,7 @@ async function applyFilter() {
const resultDiv = document.getElementById('filterResult');
resultDiv.style.display = 'block';
resultDiv.textContent = '筛选完成: ' + data.dataset_id + ' (' + data.row_count + ' 行)';
resultDiv.innerHTML = '<strong>✅ 筛选完成</strong>: 数据集 <code>' + data.dataset_id + '</code>,共 <strong>' + data.row_count + '</strong> 行';
showSuccess('筛选完成: ' + data.row_count + ' 行');
} catch(e) {
showError('筛选请求失败: ' + e.message);