v1.1.3: default server 0.0.0.0:80 + row-level clustering + filter UI + dataset selector
This commit is contained in:
+2
-19
@@ -899,7 +899,7 @@ def manual_run_analysis(request):
|
|||||||
algorithm = body.get('algorithm', 'hdbscan')
|
algorithm = body.get('algorithm', 'hdbscan')
|
||||||
min_cluster_size = int(body.get('min_cluster_size', 5))
|
min_cluster_size = int(body.get('min_cluster_size', 5))
|
||||||
head = body.get('head')
|
head = body.get('head')
|
||||||
cluster_mode = body.get('cluster_mode', 'entity')
|
cluster_mode = body.get('cluster_mode', 'raw')
|
||||||
|
|
||||||
def _analysis_fn(run, ctx):
|
def _analysis_fn(run, ctx):
|
||||||
from analysis.session_store import SessionStore
|
from analysis.session_store import SessionStore
|
||||||
@@ -909,7 +909,7 @@ def manual_run_analysis(request):
|
|||||||
min_cluster_size = ctx['min_cluster_size']
|
min_cluster_size = ctx['min_cluster_size']
|
||||||
head = ctx.get('head')
|
head = ctx.get('head')
|
||||||
pk = ctx['pk']
|
pk = ctx['pk']
|
||||||
cluster_mode = ctx.get('cluster_mode', 'entity')
|
cluster_mode = ctx.get('cluster_mode', 'raw')
|
||||||
|
|
||||||
store = SessionStore()
|
store = SessionStore()
|
||||||
try:
|
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}'
|
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)
|
# Use the unified clustering pipeline (clustering → extraction → PCA)
|
||||||
from analysis.views import _run_clustering_pipeline
|
from analysis.views import _run_clustering_pipeline
|
||||||
_run_clustering_pipeline(
|
_run_clustering_pipeline(
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
server:
|
server:
|
||||||
host: 127.0.0.1
|
host: 0.0.0.0
|
||||||
port: 8000
|
port: 80
|
||||||
debug: false
|
debug: false
|
||||||
data:
|
data:
|
||||||
schema_strict: false
|
schema_strict: false
|
||||||
|
|||||||
+2
-2
@@ -10,8 +10,8 @@ class Config(BaseModel):
|
|||||||
"""Typed configuration for TianXuan."""
|
"""Typed configuration for TianXuan."""
|
||||||
|
|
||||||
class Server(BaseModel):
|
class Server(BaseModel):
|
||||||
host: str = '127.0.0.1'
|
host: str = '0.0.0.0'
|
||||||
port: int = 8000
|
port: int = 80
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
|
|
||||||
class Data(BaseModel):
|
class Data(BaseModel):
|
||||||
|
|||||||
@@ -111,19 +111,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Filter builder card -->
|
||||||
<div class="card" id="filterCard" style="display:none;">
|
<div class="card" id="filterCard" style="background:#f0f4ff;border-color:#c8d6e5;">
|
||||||
<h3>🔍 数据筛选</h3>
|
<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;">
|
<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-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;">
|
<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>
|
<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>
|
</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>
|
<label style="font-size:0.85rem;white-space:nowrap;">逻辑:</label>
|
||||||
<select id="filterLogic" style="padding:0.35rem;border:1px solid #ccc;border-radius:4px;">
|
<select id="filterLogic" style="padding:0.35rem;border:1px solid #ccc;border-radius:4px;">
|
||||||
<option value="and">AND</option><option value="or">OR</option>
|
<option value="and">AND</option><option value="or">OR</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn" onclick="addFilterRow()" style="font-size:0.8rem;background:#e8f0fe;color:#4361ee;">+ 添加条件</button>
|
<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>
|
<button class="btn btn-primary" onclick="applyFilter()" style="margin-left:auto;">应用筛选</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="filterResult" style="display:none;margin-top:0.5rem;padding:0.5rem;background:#e8f5e9;border-radius:4px;font-size:0.85rem;"></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;
|
let stepCounter = 0;
|
||||||
const workflow = [];
|
const workflow = [];
|
||||||
let running = false;
|
let running = false;
|
||||||
var clusterMode = 'entity';
|
|
||||||
|
|
||||||
// ── Dataset selector with status badges ──
|
// ── Dataset selector with status badges ──
|
||||||
const DATASETS_WITH_STATUS = {{ datasets_with_status_json|safe }};
|
const DATASETS_WITH_STATUS = {{ datasets_with_status_json|safe }};
|
||||||
@@ -362,16 +354,16 @@ async function executeStep(idx) {
|
|||||||
resultDiv.style.display = 'none';
|
resultDiv.style.display = 'none';
|
||||||
const params = collectParams(idx);
|
const params = collectParams(idx);
|
||||||
|
|
||||||
// Cluster mode: skip build_entity_profiles in raw mode
|
// Raw row-level clustering: skip entity aggregation entirely
|
||||||
if (clusterMode === 'raw' && step.tool.name === 'build_entity_profiles') {
|
if (step.tool.name === 'build_entity_profiles') {
|
||||||
step.status = 'done';
|
step.status = 'done';
|
||||||
resultDiv.textContent = '[已跳过] 逐行直接聚类模式,无需实体聚合';
|
resultDiv.textContent = '[已跳过] 逐行直接聚类模式,无需实体聚合';
|
||||||
resultDiv.style.display = 'block';
|
resultDiv.style.display = 'block';
|
||||||
renderSteps();
|
renderSteps();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Pass cluster_mode to run_clustering tool
|
// Always cluster raw rows directly
|
||||||
if (clusterMode === 'raw' && step.tool.name === 'run_clustering') {
|
if (step.tool.name === 'run_clustering') {
|
||||||
params.cluster_mode = 'raw';
|
params.cluster_mode = 'raw';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,9 +570,30 @@ const DATASET_COLUMNS = {
|
|||||||
// ── Filter builder ──
|
// ── Filter builder ──
|
||||||
function onDatasetChange() {
|
function onDatasetChange() {
|
||||||
const dsId = document.getElementById('datasetSelect').value;
|
const dsId = document.getElementById('datasetSelect').value;
|
||||||
const filterCard = document.getElementById('filterCard');
|
const noDatasetMsg = document.getElementById('filterNoDataset');
|
||||||
if (!dsId) { filterCard.style.display = 'none'; return; }
|
const filterRows = document.getElementById('filterRows');
|
||||||
filterCard.style.display = 'block';
|
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] || [];
|
const cols = DATASET_COLUMNS[dsId] || [];
|
||||||
document.querySelectorAll('.filter-col').forEach(sel => {
|
document.querySelectorAll('.filter-col').forEach(sel => {
|
||||||
const cur = sel.value;
|
const cur = sel.value;
|
||||||
@@ -591,7 +604,7 @@ function onDatasetChange() {
|
|||||||
|
|
||||||
function addFilterRow() {
|
function addFilterRow() {
|
||||||
const rows = document.querySelectorAll('.filter-row');
|
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);
|
const template = rows[0].cloneNode(true);
|
||||||
template.querySelector('.filter-val').value = '';
|
template.querySelector('.filter-val').value = '';
|
||||||
template.querySelector('.filter-col').selectedIndex = 0;
|
template.querySelector('.filter-col').selectedIndex = 0;
|
||||||
@@ -608,6 +621,20 @@ function removeFilterRow(btn) {
|
|||||||
btn.closest('.filter-row').remove();
|
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() {
|
function collectFilters() {
|
||||||
const filters = [];
|
const filters = [];
|
||||||
document.querySelectorAll('.filter-row').forEach(row => {
|
document.querySelectorAll('.filter-row').forEach(row => {
|
||||||
@@ -650,7 +677,7 @@ async function applyFilter() {
|
|||||||
|
|
||||||
const resultDiv = document.getElementById('filterResult');
|
const resultDiv = document.getElementById('filterResult');
|
||||||
resultDiv.style.display = 'block';
|
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 + ' 行');
|
showSuccess('筛选完成: ' + data.row_count + ' 行');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
showError('筛选请求失败: ' + e.message);
|
showError('筛选请求失败: ' + e.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user