1 Commits

Author SHA1 Message Date
d35ef995a3 bump version to 0.4.0
Some checks failed
Security Scan / security (pull_request) Successful in 42s
Security Scan / dependency-check (pull_request) Successful in 46s
Test Suite / lint (pull_request) Failing after 31s
Test Suite / test (3.11) (pull_request) Successful in 1m28s
Test Suite / build (pull_request) Has been skipped
2025-09-07 16:47:13 -07:00
23 changed files with 406 additions and 306 deletions

View File

@@ -4,9 +4,7 @@
"Bash(mkdir:*)",
"Bash(uv run:*)",
"Bash(uv add:*)",
"Bash(uv sync:*)",
"Bash(tree:*)",
"WebFetch(domain:www.dash-bootstrap-components.com)"
"Bash(uv sync:*)"
],
"deny": [],
"ask": [],

View File

@@ -22,13 +22,11 @@ uv sync
**Run the application:**
Development mode (with auto-reload):
```bash
uv run run_dev.py
```
Production mode (with Gunicorn WSGI server):
```bash
# First install production dependencies
uv sync --extra prod
@@ -38,12 +36,11 @@ uv run run_prod.py
```
Legacy mode (basic Dash server):
```bash
uv run main.py
```
The app will be available at <http://127.0.0.1:8050>
The app will be available at http://127.0.0.1:8050
**Run tests:**

View File

@@ -65,11 +65,6 @@ ENV EMBEDDINGBUDDY_ENV=production
# Expose port
EXPOSE 8050
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8050/', timeout=5)" || exit 1

View File

@@ -152,38 +152,22 @@ The application follows a modular architecture for improved maintainability and
```text
src/embeddingbuddy/
├── app.py # Main application entry point and factory
├── config/ # Configuration management
│ └── settings.py # Centralized app settings
├── data/ # Data parsing and processing
── parser.py # NDJSON parsing logic
│ ├── processor.py # Data transformation utilities
── sources/ # Data source integrations
└── opensearch.py # OpenSearch data source
├── models/ # Data schemas and algorithms
│ ├── schemas.py # Pydantic data models
── reducers.py # Dimensionality reduction algorithms
│ └── field_mapper.py # Field mapping utilities
├── visualization/ # Plot creation and styling
│ ├── plots.py # Plot factory and creation logic
│ └── colors.py # Color mapping utilities
── ui/ # User interface components
│ ├── layout.py # Main application layout
│ ├── components/ # Reusable UI components
│ │ ├── sidebar.py # Sidebar component
│ │ ├── upload.py # Upload components
│ │ ├── textinput.py # Text input components
│ │ └── datasource.py # Data source components
│ └── callbacks/ # Organized callback functions
│ ├── data_processing.py # Data upload/processing callbacks
│ ├── visualization.py # Plot update callbacks
│ └── interactions.py # User interaction callbacks
└── utils/ # Utility functions
main.py # Application runner (at project root)
main.py # Application runner (at project root)
run_dev.py # Development server runner
run_prod.py # Production server runner
├── config/ # Configuration management
│ └── settings.py # Centralized app settings
├── data/ # Data parsing and processing
│ ├── parser.py # NDJSON parsing logic
── processor.py # Data transformation utilities
├── models/ # Data schemas and algorithms
── schemas.py # Pydantic data models
└── reducers.py # Dimensionality reduction algorithms
├── visualization/ # Plot creation and styling
│ ├── plots.py # Plot factory and creation logic
── colors.py # Color mapping utilities
├── ui/ # User interface components
│ ├── layout.py # Main application layout
│ ├── components/ # Reusable UI components
│ └── callbacks/ # Organized callback functions
── utils/ # Utility functions
```
### Testing

View File

@@ -1,17 +0,0 @@
/* CSS override for transparent hover boxes in Plotly plots */
/* Make hover boxes transparent while preserving text readability */
.hovertext {
fill-opacity: 0.8 !important;
stroke-opacity: 1 !important;
}
/* Alternative selector for different Plotly versions */
g.hovertext > path {
opacity: 0.8 !important;
}
/* Ensure text remains fully visible */
.hovertext text {
opacity: 1 !important;
}

View File

@@ -45,12 +45,28 @@ class TransformersEmbedder {
console.log('✅ Using globally loaded Transformers.js pipeline');
}
this.extractor = await window.transformers.pipeline('feature-extraction', modelName);
// Show loading progress to user
if (window.updateModelLoadingProgress) {
window.updateModelLoadingProgress(0, `Loading ${modelName}...`);
}
this.extractor = await window.transformers.pipeline('feature-extraction', modelName, {
progress_callback: (data) => {
if (window.updateModelLoadingProgress && data.progress !== undefined) {
const progress = Math.round(data.progress);
window.updateModelLoadingProgress(progress, data.status || 'Loading...');
}
}
});
this.modelCache.set(modelName, this.extractor);
this.currentModel = modelName;
this.isLoading = false;
if (window.updateModelLoadingProgress) {
window.updateModelLoadingProgress(100, 'Model loaded successfully');
}
return { success: true, model: modelName };
} catch (error) {
this.isLoading = false;
@@ -100,6 +116,15 @@ class TransformersEmbedder {
}
});
// Update progress
const progress = Math.min(100, ((i + batch.length) / texts.length) * 100);
if (window.updateEmbeddingProgress) {
window.updateEmbeddingProgress(progress, `Processing ${i + batch.length}/${texts.length} texts`);
}
}
if (window.updateEmbeddingProgress) {
window.updateEmbeddingProgress(100, `Generated ${embeddings.length} embeddings successfully`);
}
return embeddings;
@@ -114,6 +139,30 @@ class TransformersEmbedder {
window.transformersEmbedder = new TransformersEmbedder();
console.log('📦 TransformersEmbedder instance created');
// Global progress update functions
window.updateModelLoadingProgress = function(progress, status) {
const progressBar = document.getElementById('model-loading-progress');
const statusText = document.getElementById('model-loading-status');
if (progressBar) {
progressBar.style.width = progress + '%';
progressBar.setAttribute('aria-valuenow', progress);
}
if (statusText) {
statusText.textContent = status;
}
};
window.updateEmbeddingProgress = function(progress, status) {
const progressBar = document.getElementById('embedding-progress');
const statusText = document.getElementById('embedding-status');
if (progressBar) {
progressBar.style.width = progress + '%';
progressBar.setAttribute('aria-valuenow', progress);
}
if (statusText) {
statusText.textContent = status;
}
};
// Dash clientside callback functions
window.dash_clientside = window.dash_clientside || {};
@@ -132,7 +181,9 @@ window.dash_clientside.transformers = {
const initResult = await window.transformersEmbedder.initializeModel(modelName);
if (!initResult.success) {
return [
{ error: `Model loading error: ${initResult.error}` },
{ error: initResult.error },
`❌ Model loading error: ${initResult.error}`,
"danger",
false
];
}
@@ -143,6 +194,7 @@ window.dash_clientside.transformers = {
switch (tokenizationMethod) {
case 'sentence':
// Simple sentence splitting - can be enhanced with proper NLP
textChunks = trimmedText
.split(/[.!?]+/)
.map(s => s.trim())
@@ -167,6 +219,8 @@ window.dash_clientside.transformers = {
if (textChunks.length === 0) {
return [
{ error: 'No valid text chunks found after tokenization' },
'❌ Error: No valid text chunks found after tokenization',
"danger",
false
];
}
@@ -176,7 +230,9 @@ window.dash_clientside.transformers = {
if (!embeddings || embeddings.length !== textChunks.length) {
return [
{ error: 'Embedding generation failed' },
{ error: 'Embedding generation failed - mismatch in text chunks and embeddings' },
'❌ Error: Embedding generation failed',
"danger",
false
];
}
@@ -191,16 +247,13 @@ window.dash_clientside.transformers = {
tags: []
}));
// Return the successful embeddings data
const embeddingsData = {
documents: documents,
embeddings: embeddings
};
console.log('✅ Embeddings generated successfully:', embeddingsData);
return [
embeddingsData,
{
documents: documents,
embeddings: embeddings
},
`✅ Generated embeddings for ${documents.length} text chunks using ${modelName}`,
"success",
false
];
@@ -208,18 +261,18 @@ window.dash_clientside.transformers = {
console.error('Client-side embedding error:', error);
return [
{ error: error.message },
`❌ Error: ${error.message}`,
"danger",
false
];
}
}
};
console.log('✅ Transformers.js client-side setup complete');
console.log('Available:', {
transformersEmbedder: !!window.transformersEmbedder,
dashClientside: !!window.dash_clientside,
transformersModule: !!window.dash_clientside?.transformers,
generateFunction: typeof window.dash_clientside?.transformers?.generateEmbeddings,
processAsync: typeof window.processEmbeddingsAsync
generateFunction: typeof window.dash_clientside?.transformers?.generateEmbeddings
});

View File

@@ -111,17 +111,6 @@ window.dash_clientside.transformers = {
}
try {
// Ensure Transformers.js is loaded
if (!window.transformersLibraryLoaded) {
const loaded = await initializeTransformers();
if (!loaded) {
return [
{ error: 'Failed to load Transformers.js' },
false
];
}
}
// Tokenize text
let textChunks;
const trimmedText = textContent.trim();
@@ -141,10 +130,7 @@ window.dash_clientside.transformers = {
}
if (textChunks.length === 0) {
return [
{ error: 'No valid text chunks after tokenization' },
false
];
throw new Error('No valid text chunks after tokenization');
}
// Generate embeddings
@@ -160,16 +146,13 @@ window.dash_clientside.transformers = {
tags: []
}));
// Return the successful embeddings data
const embeddingsData = {
documents: documents,
embeddings: embeddings
};
console.log('✅ Embeddings generated successfully:', embeddingsData);
return [
embeddingsData,
{
documents: documents,
embeddings: embeddings
},
`✅ Generated embeddings for ${documents.length} text chunks using ${modelName}`,
"success",
false
];
@@ -177,12 +160,13 @@ window.dash_clientside.transformers = {
console.error('❌ Error generating embeddings:', error);
return [
{ error: error.message },
`❌ Error: ${error.message}`,
"danger",
false
];
}
}
};
console.log('✅ Simple Transformers.js setup complete');
console.log('Available functions:', Object.keys(window.dash_clientside.transformers));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

After

Width:  |  Height:  |  Size: 339 KiB

File diff suppressed because one or more lines are too long

1
prompts-raw.ndjson Normal file

File diff suppressed because one or more lines are too long

64
prompts.ndjson Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
[project]
name = "embeddingbuddy"
version = "0.5.0"
version = "0.4.0"
description = "A Python Dash application for interactive exploration and visualization of embedding vectors through dimensionality reduction techniques."
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -25,7 +25,7 @@ def main():
"--workers", str(AppSettings.GUNICORN_WORKERS),
"--bind", AppSettings.GUNICORN_BIND,
"--timeout", str(AppSettings.GUNICORN_TIMEOUT),
"--keep-alive", str(AppSettings.GUNICORN_KEEPALIVE),
"--keepalive", str(AppSettings.GUNICORN_KEEPALIVE),
"--access-logfile", "-",
"--error-logfile", "-",
"--log-level", "info",

View File

@@ -15,12 +15,7 @@ def create_app():
assets_path = os.path.join(project_root, "assets")
app = dash.Dash(
__name__,
external_stylesheets=[
dbc.themes.BOOTSTRAP,
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",
],
assets_folder=assets_path,
__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], assets_folder=assets_path
)
# Allow callbacks to components that are dynamically created in tabs
@@ -80,12 +75,16 @@ def _register_client_side_callbacks(app):
return [
{ error: 'Transformers.js not loaded. Please refresh the page and try again.' },
errorMsg + ' Please refresh the page.',
'danger',
false
];
}
""",
[
Output("embeddings-generated-trigger", "data"),
Output("text-input-status-immediate", "children"),
Output("text-input-status-immediate", "color"),
Output("generate-embeddings-btn", "disabled", allow_duplicate=True),
],
[Input("generate-embeddings-btn", "n_clicks")],

View File

@@ -74,9 +74,7 @@ class AppSettings:
PORT = int(os.getenv("EMBEDDINGBUDDY_PORT", "8050"))
# Environment Configuration
ENVIRONMENT = os.getenv(
"EMBEDDINGBUDDY_ENV", "development"
) # development, production
ENVIRONMENT = os.getenv("EMBEDDINGBUDDY_ENV", "development") # development, production
# WSGI Server Configuration (for production)
GUNICORN_WORKERS = int(os.getenv("GUNICORN_WORKERS", "4"))

View File

@@ -621,12 +621,6 @@ class DataProcessingCallbacks:
if not embeddings_data:
return no_update, no_update, no_update, no_update, no_update
# Check if this is a request trigger (contains textContent) vs actual embeddings data
if isinstance(embeddings_data, dict) and "textContent" in embeddings_data:
# This is a processing request trigger, not the actual results
# The JavaScript will handle the async processing and update the UI directly
return no_update, no_update, no_update, no_update, no_update
processed_data = self.processor.process_client_embeddings(embeddings_data)
if processed_data.error:

View File

@@ -1,5 +1,6 @@
import dash
from dash import callback, Input, Output
from dash import callback, Input, Output, State, html
import dash_bootstrap_components as dbc
class InteractionCallbacks:
@@ -7,16 +8,75 @@ class InteractionCallbacks:
self._register_callbacks()
def _register_callbacks(self):
@callback(
Output("point-details", "children"),
Input("embedding-plot", "clickData"),
[State("processed-data", "data"), State("processed-prompts", "data")],
)
def display_click_data(clickData, data, prompts_data):
if not clickData or not data:
return "Click on a point to see details"
point_data = clickData["points"][0]
trace_name = point_data.get("fullData", {}).get("name", "Documents")
if "pointIndex" in point_data:
point_index = point_data["pointIndex"]
elif "pointNumber" in point_data:
point_index = point_data["pointNumber"]
else:
return "Could not identify clicked point"
if (
trace_name.startswith("Prompts")
and prompts_data
and "prompts" in prompts_data
):
item = prompts_data["prompts"][point_index]
item_type = "Prompt"
else:
item = data["documents"][point_index]
item_type = "Document"
return self._create_detail_card(item, item_type)
@callback(
[
Output("processed-data", "data", allow_duplicate=True),
Output("processed-prompts", "data", allow_duplicate=True),
Output("point-details", "children", allow_duplicate=True),
],
Input("reset-button", "n_clicks"),
prevent_initial_call=True,
)
def reset_data(n_clicks):
if n_clicks is None or n_clicks == 0:
return dash.no_update, dash.no_update
return dash.no_update, dash.no_update, dash.no_update
return None, None
return None, None, "Click on a point to see details"
@staticmethod
def _create_detail_card(item, item_type):
return dbc.Card(
[
dbc.CardBody(
[
html.H5(f"{item_type}: {item['id']}", className="card-title"),
html.P(f"Text: {item['text']}", className="card-text"),
html.P(
f"Category: {item.get('category', 'Unknown')}",
className="card-text",
),
html.P(
f"Subcategory: {item.get('subcategory', 'Unknown')}",
className="card-text",
),
html.P(
f"Tags: {', '.join(item.get('tags', [])) if item.get('tags') else 'None'}",
className="card-text",
),
html.P(f"Type: {item_type}", className="card-text text-muted"),
]
)
]
)

View File

@@ -1,11 +1,13 @@
from dash import dcc, html
import dash_bootstrap_components as dbc
from .upload import UploadComponent
from .textinput import TextInputComponent
class DataSourceComponent:
def __init__(self):
self.upload_component = UploadComponent()
self.text_input_component = TextInputComponent()
def create_tabbed_interface(self):
"""Create tabbed interface for different data sources."""
@@ -17,6 +19,7 @@ class DataSourceComponent:
[
dbc.Tab(label="File Upload", tab_id="file-tab"),
dbc.Tab(label="OpenSearch", tab_id="opensearch-tab"),
dbc.Tab(label="Text Input", tab_id="text-input-tab"),
],
id="data-source-tabs",
active_tab="file-tab",
@@ -208,6 +211,10 @@ class DataSourceComponent:
]
)
def create_text_input_tab(self):
"""Create text input tab content for browser-based embedding generation."""
return html.Div([self.text_input_component.create_text_input_interface()])
def _create_opensearch_section(self, section_type):
"""Create a complete OpenSearch section for either 'data' or 'prompts'."""
section_id = section_type # 'data' or 'prompts'

View File

@@ -2,26 +2,31 @@ from dash import dcc, html
import dash_bootstrap_components as dbc
from .upload import UploadComponent
from .datasource import DataSourceComponent
from .textinput import TextInputComponent
class SidebarComponent:
def __init__(self):
self.upload_component = UploadComponent()
self.datasource_component = DataSourceComponent()
self.textinput_component = TextInputComponent()
def create_layout(self):
return dbc.Col(
[
dbc.Accordion(
[
self._create_data_sources_item(),
self._create_generate_embeddings_item(),
self._create_visualization_controls_item(),
],
always_open=True,
)
html.H5("Data Sources", className="mb-3"),
self.datasource_component.create_error_alert(),
self.datasource_component.create_success_alert(),
self.datasource_component.create_tabbed_interface(),
html.H5("Visualization Controls", className="mb-3 mt-4"),
]
+ self._create_method_dropdown()
+ self._create_color_dropdown()
+ self._create_dimension_toggle()
+ self._create_prompts_toggle()
+ [
html.H5("Point Details", className="mb-3"),
html.Div(
id="point-details", children="Click on a point to see details"
),
],
width=3,
style={"padding-right": "20px"},
@@ -81,63 +86,3 @@ class SidebarComponent:
style={"margin-bottom": "20px"},
),
]
def _create_generate_embeddings_item(self):
return dbc.AccordionItem(
[
self.textinput_component.create_text_input_interface(),
],
title=html.Span(
[
"Generate Embeddings ",
html.I(
className="fas fa-info-circle text-muted",
style={"cursor": "pointer"},
id="generate-embeddings-info-icon",
title="Create new embeddings from text input using various in-browser models",
),
]
),
item_id="generate-embeddings-accordion",
)
def _create_data_sources_item(self):
return dbc.AccordionItem(
[
self.datasource_component.create_error_alert(),
self.datasource_component.create_success_alert(),
self.datasource_component.create_tabbed_interface(),
],
title=html.Span(
[
"Load Embeddings ",
html.I(
className="fas fa-info-circle text-muted",
style={"cursor": "pointer"},
id="load-embeddings-info-icon",
title="Load existing embeddings: upload files or read from OpenSearch",
),
]
),
item_id="data-sources-accordion",
)
def _create_visualization_controls_item(self):
return dbc.AccordionItem(
self._create_method_dropdown()
+ self._create_color_dropdown()
+ self._create_dimension_toggle()
+ self._create_prompts_toggle(),
title=html.Span(
[
"Visualization Controls ",
html.I(
className="fas fa-info-circle text-muted",
style={"cursor": "pointer"},
id="visualization-controls-info-icon",
title="Configure plot settings: select dimensionality reduction method, colors, and display options",
),
]
),
item_id="visualization-controls-accordion",
)

View File

@@ -30,6 +30,9 @@ class TextInputComponent:
# Generation controls
self._create_generation_controls(),
html.Hr(),
# Progress indicators
self._create_progress_indicators(),
html.Hr(),
# Status and results
self._create_status_section(),
# Hidden components for data flow
@@ -294,10 +297,65 @@ class TextInputComponent:
]
)
def _create_progress_indicators(self):
"""Create progress bars for model loading and embedding generation."""
return html.Div(
[
# Model loading progress
html.Div(
[
html.H6("Model Loading Progress", className="mb-2"),
dbc.Progress(
id="model-loading-progress",
value=0,
striped=True,
animated=True,
className="mb-2",
),
html.Small(
id="model-loading-status",
children="No model loading in progress",
className="text-muted",
),
],
id="model-loading-section",
style={"display": "none"},
),
html.Br(),
# Embedding generation progress
html.Div(
[
html.H6("Embedding Generation Progress", className="mb-2"),
dbc.Progress(
id="embedding-progress",
value=0,
striped=True,
animated=True,
className="mb-2",
),
html.Small(
id="embedding-status",
children="No embedding generation in progress",
className="text-muted",
),
],
id="embedding-progress-section",
style={"display": "none"},
),
]
)
def _create_status_section(self):
"""Create status alerts and results preview."""
return html.Div(
[
# Immediate status (from client-side)
dbc.Alert(
id="text-input-status-immediate",
children="Ready to generate embeddings",
color="light",
className="mb-3",
),
# Server-side status
dbc.Alert(
id="text-input-status",

View File

@@ -5,75 +5,39 @@ import dash_bootstrap_components as dbc
class UploadComponent:
@staticmethod
def create_data_upload():
return html.Div(
[
dcc.Upload(
id="upload-data",
children=html.Div(
[
"Upload Data ",
html.I(
className="fas fa-info-circle",
style={"color": "#6c757d", "fontSize": "14px"},
id="data-upload-info",
),
]
),
style={
"width": "100%",
"height": "60px",
"lineHeight": "60px",
"borderWidth": "1px",
"borderStyle": "dashed",
"borderRadius": "5px",
"textAlign": "center",
"margin-bottom": "20px",
},
multiple=False,
),
dbc.Tooltip(
"Click here or drag and drop NDJSON files containing document embeddings",
target="data-upload-info",
placement="top",
),
]
return dcc.Upload(
id="upload-data",
children=html.Div(["Drag and Drop or ", html.A("Select Files")]),
style={
"width": "100%",
"height": "60px",
"lineHeight": "60px",
"borderWidth": "1px",
"borderStyle": "dashed",
"borderRadius": "5px",
"textAlign": "center",
"margin-bottom": "20px",
},
multiple=False,
)
@staticmethod
def create_prompts_upload():
return html.Div(
[
dcc.Upload(
id="upload-prompts",
children=html.Div(
[
"Upload Prompts ",
html.I(
className="fas fa-info-circle",
style={"color": "#6c757d", "fontSize": "14px"},
id="prompts-upload-info",
),
]
),
style={
"width": "100%",
"height": "60px",
"lineHeight": "60px",
"borderWidth": "1px",
"borderStyle": "dashed",
"borderRadius": "5px",
"textAlign": "center",
"margin-bottom": "20px",
"borderColor": "#28a745",
},
multiple=False,
),
dbc.Tooltip(
"Click here or drag and drop NDJSON files containing prompt embeddings",
target="prompts-upload-info",
placement="top",
),
]
return dcc.Upload(
id="upload-prompts",
children=html.Div(["Drag and Drop Prompts or ", html.A("Select Files")]),
style={
"width": "100%",
"height": "60px",
"lineHeight": "60px",
"borderWidth": "1px",
"borderStyle": "dashed",
"borderRadius": "5px",
"textAlign": "center",
"margin-bottom": "20px",
"borderColor": "#28a745",
},
multiple=False,
)
@staticmethod

View File

@@ -38,9 +38,9 @@ class PlotFactory:
if dimensions == "3d":
fig = px.scatter_3d(
df,
x="x",
y="y",
z="z",
x="dim_1",
y="dim_2",
z="dim_3",
color=color_values,
hover_data=hover_fields,
title=f"3D Embedding Visualization - {method} (colored by {color_by})",
@@ -49,8 +49,8 @@ class PlotFactory:
else:
fig = px.scatter(
df,
x="x",
y="y",
x="dim_1",
y="dim_2",
color=color_values,
hover_data=hover_fields,
title=f"2D Embedding Visualization - {method} (colored by {color_by})",
@@ -77,17 +77,17 @@ class PlotFactory:
if dimensions == "3d":
doc_fig = px.scatter_3d(
doc_df,
x="x",
y="y",
z="z",
x="dim_1",
y="dim_2",
z="dim_3",
color=doc_color_values,
hover_data=hover_fields,
)
else:
doc_fig = px.scatter(
doc_df,
x="x",
y="y",
x="dim_1",
y="dim_2",
color=doc_color_values,
hover_data=hover_fields,
)
@@ -114,17 +114,17 @@ class PlotFactory:
if dimensions == "3d":
prompt_fig = px.scatter_3d(
prompt_df,
x="x",
y="y",
z="z",
x="dim_1",
y="dim_2",
z="dim_3",
color=prompt_color_values,
hover_data=hover_fields,
)
else:
prompt_fig = px.scatter(
prompt_df,
x="x",
y="y",
x="dim_1",
y="dim_2",
color=prompt_color_values,
hover_data=hover_fields,
)
@@ -168,11 +168,11 @@ class PlotFactory:
"category": doc.category,
"subcategory": doc.subcategory,
"tags_str": ", ".join(doc.tags) if doc.tags else "None",
"x": coordinates[i, 0],
"y": coordinates[i, 1],
"dim_1": coordinates[i, 0],
"dim_2": coordinates[i, 1],
}
if dimensions == "3d":
row["z"] = coordinates[i, 2]
row["dim_3"] = coordinates[i, 2]
df_data.append(row)
return pd.DataFrame(df_data)

2
uv.lock generated
View File

@@ -412,7 +412,7 @@ wheels = [
[[package]]
name = "embeddingbuddy"
version = "0.5.0"
version = "0.3.0"
source = { editable = "." }
dependencies = [
{ name = "dash" },