Spaces:
Running
Running
feature: ai prompt to create vis
Browse filesNow let's take the project in a different direction. Instead of having a dropdown of choices to create 2D plots, let's give the user a text box in which to enter a prompt for
an LLM. From the user's perspective, they can ask for any visualization they want on top of the Parquet file. The LLM is given a system prompt telling it to output a Vega-Lite
spec implementing the user's prompt. We can then display the resulting Vega-Lite. In order to implement the LLM, use Hugging Face Inference Providers. Follow the example at
https://huggingface.co/docs/inference-providers/en/index?javascript-clients=fetch#javascript.
- index.html +116 -196
index.html
CHANGED
|
@@ -51,19 +51,25 @@
|
|
| 51 |
<div id="status" class="status"></div>
|
| 52 |
|
| 53 |
<div id="visualizationSection" class="visualization-section" style="display: none;">
|
| 54 |
-
<h2>Visualization</h2>
|
| 55 |
<div class="form-group">
|
| 56 |
-
<label for="
|
| 57 |
-
<
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
</div>
|
| 61 |
<div class="form-group">
|
| 62 |
-
<label for="
|
| 63 |
-
<
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
</div>
|
|
|
|
| 67 |
<div id="vizContainer" class="viz-container"></div>
|
| 68 |
</div>
|
| 69 |
|
|
@@ -164,7 +170,7 @@
|
|
| 164 |
}));
|
| 165 |
|
| 166 |
setStatus(`Detected ${columnInfo.length} columns`, 'success');
|
| 167 |
-
|
| 168 |
|
| 169 |
} catch (error) {
|
| 170 |
console.error('Error detecting columns:', error);
|
|
@@ -173,127 +179,14 @@
|
|
| 173 |
}
|
| 174 |
}
|
| 175 |
|
| 176 |
-
//
|
| 177 |
-
function
|
| 178 |
-
const xColSelect = document.getElementById('xColSelect');
|
| 179 |
-
const yColSelect = document.getElementById('yColSelect');
|
| 180 |
const vizSection = document.getElementById('visualizationSection');
|
| 181 |
-
|
| 182 |
-
// Clear existing options except first
|
| 183 |
-
xColSelect.innerHTML = '<option value="">-- Select X column --</option>';
|
| 184 |
-
yColSelect.innerHTML = '<option value="">-- Select Y column --</option>';
|
| 185 |
-
|
| 186 |
-
// Populate both dropdowns with all visualizable columns (numeric or text)
|
| 187 |
-
columnInfo.forEach(col => {
|
| 188 |
-
const isNumeric = isNumericType(col.type);
|
| 189 |
-
const isText = isTextType(col.type);
|
| 190 |
-
|
| 191 |
-
if (isNumeric || isText) {
|
| 192 |
-
const xOption = document.createElement('option');
|
| 193 |
-
xOption.value = col.name;
|
| 194 |
-
xOption.textContent = `${col.name} (${col.type})`;
|
| 195 |
-
xColSelect.appendChild(xOption);
|
| 196 |
-
|
| 197 |
-
const yOption = document.createElement('option');
|
| 198 |
-
yOption.value = col.name;
|
| 199 |
-
yOption.textContent = `${col.name} (${col.type})`;
|
| 200 |
-
yColSelect.appendChild(yOption);
|
| 201 |
-
}
|
| 202 |
-
});
|
| 203 |
-
|
| 204 |
-
// Find first valid pair and pre-select it
|
| 205 |
-
let firstValidPair = null;
|
| 206 |
-
for (let i = 0; i < columnInfo.length && !firstValidPair; i++) {
|
| 207 |
-
const xCol = columnInfo[i];
|
| 208 |
-
const xIsNumeric = isNumericType(xCol.type);
|
| 209 |
-
const xIsText = isTextType(xCol.type);
|
| 210 |
-
|
| 211 |
-
if (!xIsNumeric && !xIsText) continue;
|
| 212 |
-
|
| 213 |
-
for (let j = 0; j < columnInfo.length; j++) {
|
| 214 |
-
if (i === j) continue; // Skip same column
|
| 215 |
-
|
| 216 |
-
const yCol = columnInfo[j];
|
| 217 |
-
const yIsNumeric = isNumericType(yCol.type);
|
| 218 |
-
const yIsText = isTextType(yCol.type);
|
| 219 |
-
|
| 220 |
-
// Valid combinations: at least one must be numeric
|
| 221 |
-
if ((xIsNumeric && yIsNumeric) ||
|
| 222 |
-
(xIsNumeric && yIsText) ||
|
| 223 |
-
(xIsText && yIsNumeric)) {
|
| 224 |
-
firstValidPair = { x: xCol.name, y: yCol.name };
|
| 225 |
-
break;
|
| 226 |
-
}
|
| 227 |
-
}
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
if (firstValidPair) {
|
| 231 |
-
// Pre-select the first valid pair
|
| 232 |
-
xColSelect.value = firstValidPair.x;
|
| 233 |
-
yColSelect.value = firstValidPair.y;
|
| 234 |
-
vizSection.style.display = 'block';
|
| 235 |
-
|
| 236 |
-
// Render the visualization immediately
|
| 237 |
-
renderVisualization(firstValidPair.x, firstValidPair.y, 'scatter');
|
| 238 |
-
} else if (columnInfo.length > 0) {
|
| 239 |
vizSection.style.display = 'block';
|
| 240 |
} else {
|
| 241 |
vizSection.style.display = 'none';
|
| 242 |
-
setStatus('No
|
| 243 |
-
}
|
| 244 |
-
}
|
| 245 |
-
|
| 246 |
-
// Render visualization using Vega-Lite
|
| 247 |
-
async function renderVisualization(xCol, yCol, chartType) {
|
| 248 |
-
const vizContainer = document.getElementById('vizContainer');
|
| 249 |
-
vizContainer.innerHTML = ''; // Clear previous
|
| 250 |
-
|
| 251 |
-
if (chartType === 'unsupported') {
|
| 252 |
-
setStatus(`Error: Unsupported column type combination. X column type: ${columnInfo.find(c => c.name === xCol)?.type}, Y column type: ${columnInfo.find(c => c.name === yCol)?.type}`, 'error');
|
| 253 |
-
return;
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
try {
|
| 257 |
-
setStatus('Fetching data for visualization...', 'info');
|
| 258 |
-
|
| 259 |
-
// Fetch data (limit to reasonable amount for visualization)
|
| 260 |
-
const query = `SELECT "${xCol}", "${yCol}" FROM 'data.parquet' LIMIT 1000`;
|
| 261 |
-
const result = await conn.query(query);
|
| 262 |
-
const data = result.toArray();
|
| 263 |
-
|
| 264 |
-
setStatus('Rendering visualization...', 'info');
|
| 265 |
-
|
| 266 |
-
let spec;
|
| 267 |
-
|
| 268 |
-
if (chartType === 'scatter') {
|
| 269 |
-
// Scatter plot: determine type for each axis based on column type
|
| 270 |
-
const xColInfo = columnInfo.find(c => c.name === xCol);
|
| 271 |
-
const yColInfo = columnInfo.find(c => c.name === yCol);
|
| 272 |
-
|
| 273 |
-
const xType = isNumericType(xColInfo.type) ? 'quantitative' : 'nominal';
|
| 274 |
-
const yType = isNumericType(yColInfo.type) ? 'quantitative' : 'nominal';
|
| 275 |
-
|
| 276 |
-
spec = {
|
| 277 |
-
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
|
| 278 |
-
description: `Scatter plot of ${xCol} vs ${yCol}`,
|
| 279 |
-
data: { values: data },
|
| 280 |
-
mark: 'point',
|
| 281 |
-
encoding: {
|
| 282 |
-
x: { field: xCol, type: xType },
|
| 283 |
-
y: { field: yCol, type: yType }
|
| 284 |
-
},
|
| 285 |
-
width: 600,
|
| 286 |
-
height: 400
|
| 287 |
-
};
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
// Render using vega-embed
|
| 291 |
-
await vegaEmbed('#vizContainer', spec);
|
| 292 |
-
setStatus('Visualization rendered successfully!', 'success');
|
| 293 |
-
|
| 294 |
-
} catch (error) {
|
| 295 |
-
console.error('Error rendering visualization:', error);
|
| 296 |
-
setStatus(`Error rendering visualization: ${error.message}`, 'error');
|
| 297 |
}
|
| 298 |
}
|
| 299 |
|
|
@@ -345,90 +238,117 @@
|
|
| 345 |
}
|
| 346 |
});
|
| 347 |
|
| 348 |
-
//
|
| 349 |
-
function
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
const xColInfo = columnInfo.find(c => c.name === xCol);
|
| 353 |
-
const yColInfo = columnInfo.find(c => c.name === yCol);
|
| 354 |
-
|
| 355 |
-
if (!xColInfo || !yColInfo) return false;
|
| 356 |
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
}
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
if (!xCol || !yCol) {
|
| 370 |
-
return 'Please select both X and Y columns';
|
| 371 |
-
}
|
| 372 |
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
}
|
| 383 |
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
|
| 389 |
-
|
| 390 |
-
|
|
|
|
| 391 |
}
|
|
|
|
| 392 |
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
|
|
|
| 396 |
|
| 397 |
-
if (!
|
| 398 |
-
|
|
|
|
| 399 |
}
|
| 400 |
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
// Handle X axis selection
|
| 405 |
-
document.getElementById('xColSelect').addEventListener('change', async function(e) {
|
| 406 |
-
const xCol = e.target.value;
|
| 407 |
-
const yCol = document.getElementById('yColSelect').value;
|
| 408 |
-
|
| 409 |
-
if (isValidCombination(xCol, yCol)) {
|
| 410 |
-
await renderVisualization(xCol, yCol, 'scatter');
|
| 411 |
-
} else if (xCol || yCol) {
|
| 412 |
-
// Show error and clear visualization
|
| 413 |
-
const errorMsg = getInvalidCombinationError(xCol, yCol);
|
| 414 |
-
setStatus(errorMsg, 'error');
|
| 415 |
-
document.getElementById('vizContainer').innerHTML = '';
|
| 416 |
}
|
| 417 |
-
});
|
| 418 |
|
| 419 |
-
|
| 420 |
-
document.getElementById('yColSelect').addEventListener('change', async function(e) {
|
| 421 |
-
const yCol = e.target.value;
|
| 422 |
-
const xCol = document.getElementById('xColSelect').value;
|
| 423 |
-
|
| 424 |
-
if (isValidCombination(xCol, yCol)) {
|
| 425 |
-
await renderVisualization(xCol, yCol, 'scatter');
|
| 426 |
-
} else if (xCol || yCol) {
|
| 427 |
-
// Show error and clear visualization
|
| 428 |
-
const errorMsg = getInvalidCombinationError(xCol, yCol);
|
| 429 |
-
setStatus(errorMsg, 'error');
|
| 430 |
-
document.getElementById('vizContainer').innerHTML = '';
|
| 431 |
-
}
|
| 432 |
});
|
| 433 |
|
| 434 |
// Set up event listeners
|
|
|
|
| 51 |
<div id="status" class="status"></div>
|
| 52 |
|
| 53 |
<div id="visualizationSection" class="visualization-section" style="display: none;">
|
| 54 |
+
<h2>Create Visualization</h2>
|
| 55 |
<div class="form-group">
|
| 56 |
+
<label for="hfToken">Hugging Face Token (required for LLM)</label>
|
| 57 |
+
<input
|
| 58 |
+
type="password"
|
| 59 |
+
id="hfToken"
|
| 60 |
+
placeholder="Enter your HF token with Inference Providers permission"
|
| 61 |
+
/>
|
| 62 |
+
<small>Get a token from <a href="https://huggingface.co/settings/tokens" target="_blank">HF Settings</a> with "Make calls to Inference Providers" permission</small>
|
| 63 |
</div>
|
| 64 |
<div class="form-group">
|
| 65 |
+
<label for="vizPrompt">Describe the visualization you want</label>
|
| 66 |
+
<textarea
|
| 67 |
+
id="vizPrompt"
|
| 68 |
+
rows="3"
|
| 69 |
+
placeholder="e.g., Show a scatter plot of price vs quantity, Create a bar chart showing count by category..."
|
| 70 |
+
></textarea>
|
| 71 |
</div>
|
| 72 |
+
<button type="button" id="generateVizBtn">Generate Visualization</button>
|
| 73 |
<div id="vizContainer" class="viz-container"></div>
|
| 74 |
</div>
|
| 75 |
|
|
|
|
| 170 |
}));
|
| 171 |
|
| 172 |
setStatus(`Detected ${columnInfo.length} columns`, 'success');
|
| 173 |
+
showVisualizationSection();
|
| 174 |
|
| 175 |
} catch (error) {
|
| 176 |
console.error('Error detecting columns:', error);
|
|
|
|
| 179 |
}
|
| 180 |
}
|
| 181 |
|
| 182 |
+
// Show visualization section after dataset is loaded
|
| 183 |
+
function showVisualizationSection() {
|
|
|
|
|
|
|
| 184 |
const vizSection = document.getElementById('visualizationSection');
|
| 185 |
+
if (columnInfo.length > 0) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
vizSection.style.display = 'block';
|
| 187 |
} else {
|
| 188 |
vizSection.style.display = 'none';
|
| 189 |
+
setStatus('No columns found in dataset', 'error');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
}
|
| 191 |
}
|
| 192 |
|
|
|
|
| 238 |
}
|
| 239 |
});
|
| 240 |
|
| 241 |
+
// Generate Vega-Lite spec using LLM
|
| 242 |
+
async function generateVisualization(prompt, hfToken) {
|
| 243 |
+
const vizContainer = document.getElementById('vizContainer');
|
| 244 |
+
vizContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
+
try {
|
| 247 |
+
setStatus('Generating visualization with LLM...', 'info');
|
| 248 |
+
|
| 249 |
+
// Prepare column information for the LLM
|
| 250 |
+
const columnDescriptions = columnInfo.map(col => `- ${col.name}: ${col.type}`).join('\n');
|
| 251 |
+
|
| 252 |
+
// Create system prompt
|
| 253 |
+
const systemPrompt = `You are a data visualization assistant that generates Vega-Lite specifications.
|
| 254 |
+
|
| 255 |
+
Available dataset columns:
|
| 256 |
+
${columnDescriptions}
|
| 257 |
+
|
| 258 |
+
Instructions:
|
| 259 |
+
1. Generate a valid Vega-Lite v5 specification based on the user's request
|
| 260 |
+
2. Use ONLY columns that exist in the dataset above
|
| 261 |
+
3. The data will be provided as an array of objects in the "data.values" field
|
| 262 |
+
4. Output ONLY the JSON specification, no explanations or markdown
|
| 263 |
+
5. Do not include the data itself, just reference fields by name
|
| 264 |
+
6. Include appropriate width and height (e.g., 600x400)
|
| 265 |
+
7. Make sure the spec is complete and valid
|
| 266 |
+
|
| 267 |
+
Output only the JSON spec starting with { and ending with }.`;
|
| 268 |
+
|
| 269 |
+
// Call HF Inference API
|
| 270 |
+
const response = await fetch(
|
| 271 |
+
"https://router.huggingface.co/v1/chat/completions",
|
| 272 |
+
{
|
| 273 |
+
method: "POST",
|
| 274 |
+
headers: {
|
| 275 |
+
Authorization: `Bearer ${hfToken}`,
|
| 276 |
+
"Content-Type": "application/json",
|
| 277 |
+
},
|
| 278 |
+
body: JSON.stringify({
|
| 279 |
+
model: "openai/gpt-4o-mini:fastest",
|
| 280 |
+
messages: [
|
| 281 |
+
{
|
| 282 |
+
role: "system",
|
| 283 |
+
content: systemPrompt
|
| 284 |
+
},
|
| 285 |
+
{
|
| 286 |
+
role: "user",
|
| 287 |
+
content: prompt
|
| 288 |
+
}
|
| 289 |
+
],
|
| 290 |
+
temperature: 0.7,
|
| 291 |
+
max_tokens: 2000
|
| 292 |
+
}),
|
| 293 |
+
}
|
| 294 |
+
);
|
| 295 |
|
| 296 |
+
if (!response.ok) {
|
| 297 |
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
| 298 |
+
}
|
|
|
|
| 299 |
|
| 300 |
+
const data = await response.json();
|
| 301 |
+
const vegaSpec = data.choices[0].message.content;
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
+
// Parse and validate the Vega-Lite spec
|
| 304 |
+
let spec;
|
| 305 |
+
try {
|
| 306 |
+
// Try to extract JSON if wrapped in markdown code blocks
|
| 307 |
+
let jsonStr = vegaSpec.trim();
|
| 308 |
+
if (jsonStr.startsWith('```')) {
|
| 309 |
+
jsonStr = jsonStr.replace(/```json\n?/g, '').replace(/```\n?/g, '');
|
| 310 |
+
}
|
| 311 |
+
spec = JSON.parse(jsonStr);
|
| 312 |
+
} catch (e) {
|
| 313 |
+
throw new Error(`Failed to parse LLM response as JSON: ${e.message}`);
|
| 314 |
+
}
|
| 315 |
|
| 316 |
+
// Fetch data for the visualization
|
| 317 |
+
setStatus('Fetching data for visualization...', 'info');
|
| 318 |
+
const query = `SELECT * FROM 'data.parquet' LIMIT 1000`;
|
| 319 |
+
const result = await conn.query(query);
|
| 320 |
+
const dataArray = result.toArray();
|
| 321 |
|
| 322 |
+
// Inject data into the spec
|
| 323 |
+
spec.data = { values: dataArray };
|
|
|
|
| 324 |
|
| 325 |
+
// Render the visualization
|
| 326 |
+
setStatus('Rendering visualization...', 'info');
|
| 327 |
+
await vegaEmbed('#vizContainer', spec);
|
| 328 |
+
setStatus('Visualization generated successfully!', 'success');
|
| 329 |
|
| 330 |
+
} catch (error) {
|
| 331 |
+
console.error('Error generating visualization:', error);
|
| 332 |
+
setStatus(`Error: ${error.message}`, 'error');
|
| 333 |
}
|
| 334 |
+
}
|
| 335 |
|
| 336 |
+
// Handle generate visualization button
|
| 337 |
+
document.getElementById('generateVizBtn').addEventListener('click', async function() {
|
| 338 |
+
const prompt = document.getElementById('vizPrompt').value.trim();
|
| 339 |
+
const hfToken = document.getElementById('hfToken').value.trim();
|
| 340 |
|
| 341 |
+
if (!prompt) {
|
| 342 |
+
setStatus('Please enter a visualization prompt', 'error');
|
| 343 |
+
return;
|
| 344 |
}
|
| 345 |
|
| 346 |
+
if (!hfToken) {
|
| 347 |
+
setStatus('Please enter your Hugging Face token', 'error');
|
| 348 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
}
|
|
|
|
| 350 |
|
| 351 |
+
await generateVisualization(prompt, hfToken);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
});
|
| 353 |
|
| 354 |
// Set up event listeners
|