anywidget-vector 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. anywidget_vector/__init__.py +1 -1
  2. anywidget_vector/backends/__init__.py +103 -0
  3. anywidget_vector/backends/chroma/__init__.py +27 -0
  4. anywidget_vector/backends/chroma/client.py +60 -0
  5. anywidget_vector/backends/chroma/converter.py +86 -0
  6. anywidget_vector/backends/grafeo/__init__.py +20 -0
  7. anywidget_vector/backends/grafeo/client.py +33 -0
  8. anywidget_vector/backends/grafeo/converter.py +46 -0
  9. anywidget_vector/backends/lancedb/__init__.py +22 -0
  10. anywidget_vector/backends/lancedb/client.py +56 -0
  11. anywidget_vector/backends/lancedb/converter.py +71 -0
  12. anywidget_vector/backends/pinecone/__init__.py +21 -0
  13. anywidget_vector/backends/pinecone/client.js +45 -0
  14. anywidget_vector/backends/pinecone/converter.py +62 -0
  15. anywidget_vector/backends/qdrant/__init__.py +26 -0
  16. anywidget_vector/backends/qdrant/client.js +61 -0
  17. anywidget_vector/backends/qdrant/converter.py +83 -0
  18. anywidget_vector/backends/weaviate/__init__.py +33 -0
  19. anywidget_vector/backends/weaviate/client.js +50 -0
  20. anywidget_vector/backends/weaviate/converter.py +81 -0
  21. anywidget_vector/static/icons.js +14 -0
  22. anywidget_vector/traitlets.py +84 -0
  23. anywidget_vector/ui/__init__.py +206 -0
  24. anywidget_vector/ui/canvas.js +521 -0
  25. anywidget_vector/ui/constants.js +64 -0
  26. anywidget_vector/ui/properties.js +158 -0
  27. anywidget_vector/ui/settings.js +265 -0
  28. anywidget_vector/ui/styles.css +348 -0
  29. anywidget_vector/ui/toolbar.js +117 -0
  30. anywidget_vector/widget.py +174 -1120
  31. {anywidget_vector-0.2.0.dist-info → anywidget_vector-0.2.1.dist-info}/METADATA +3 -3
  32. anywidget_vector-0.2.1.dist-info/RECORD +34 -0
  33. anywidget_vector-0.2.0.dist-info/RECORD +0 -6
  34. {anywidget_vector-0.2.0.dist-info → anywidget_vector-0.2.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,26 @@
1
+ """Qdrant backend adapter.
2
+
3
+ Query Format (JSON):
4
+ # Similarity search
5
+ {"vector": [0.1, 0.2, 0.3], "limit": 10}
6
+
7
+ # With filter
8
+ {
9
+ "vector": [0.1, 0.2, 0.3],
10
+ "filter": {"must": [{"key": "category", "match": {"value": "tech"}}]},
11
+ "limit": 10
12
+ }
13
+
14
+ # Recommend by ID
15
+ {"recommend": {"positive": ["point_123"]}, "limit": 10}
16
+
17
+ # Get by IDs
18
+ {"ids": ["point_1", "point_2"]}
19
+
20
+ # Scroll/filter only
21
+ {"filter": {"must": [...]}, "limit": 100}
22
+ """
23
+
24
+ from anywidget_vector.backends.qdrant.converter import build_filter, to_points
25
+
26
+ __all__ = ["to_points", "build_filter"]
@@ -0,0 +1,61 @@
1
+ // Qdrant browser-side client
2
+
3
+ export async function executeQuery(query, config) {
4
+ const { url, apiKey, collection } = config;
5
+ const headers = { "Content-Type": "application/json" };
6
+ if (apiKey) headers["api-key"] = apiKey;
7
+
8
+ const parsed = typeof query === "string" ? JSON.parse(query) : query;
9
+ let endpoint, body;
10
+
11
+ if (parsed.ids) {
12
+ endpoint = `${url}/collections/${collection}/points`;
13
+ body = { ids: parsed.ids, with_payload: true, with_vectors: true };
14
+ } else if (parsed.recommend) {
15
+ endpoint = `${url}/collections/${collection}/points/recommend`;
16
+ body = {
17
+ positive: parsed.recommend.positive || [],
18
+ negative: parsed.recommend.negative || [],
19
+ limit: parsed.limit || 10,
20
+ with_payload: true,
21
+ with_vectors: true,
22
+ };
23
+ } else if (parsed.vector) {
24
+ endpoint = `${url}/collections/${collection}/points/search`;
25
+ body = {
26
+ vector: parsed.vector,
27
+ filter: parsed.filter,
28
+ limit: parsed.limit || 10,
29
+ with_payload: true,
30
+ with_vectors: true,
31
+ score_threshold: parsed.score_threshold,
32
+ };
33
+ } else if (parsed.filter) {
34
+ endpoint = `${url}/collections/${collection}/points/scroll`;
35
+ body = {
36
+ filter: parsed.filter,
37
+ limit: parsed.limit || 100,
38
+ with_payload: true,
39
+ with_vectors: true,
40
+ };
41
+ } else {
42
+ throw new Error("Invalid query: need vector, ids, recommend, or filter");
43
+ }
44
+
45
+ const resp = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify(body) });
46
+ if (!resp.ok) throw new Error(`Qdrant error: ${await resp.text()}`);
47
+ return await resp.json();
48
+ }
49
+
50
+ export function toPoints(response) {
51
+ const results = response.result || response.points || [];
52
+ return results.map(r => ({
53
+ id: String(r.id),
54
+ score: r.score,
55
+ x: r.vector?.[0] ?? r.payload?.x ?? 0,
56
+ y: r.vector?.[1] ?? r.payload?.y ?? 0,
57
+ z: r.vector?.[2] ?? r.payload?.z ?? 0,
58
+ vector: r.vector,
59
+ ...r.payload,
60
+ }));
61
+ }
@@ -0,0 +1,83 @@
1
+ """Qdrant result conversion and filter building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def to_points(response: dict[str, Any]) -> list[dict[str, Any]]:
9
+ """Convert Qdrant response to points format.
10
+
11
+ Args:
12
+ response: Qdrant API response
13
+
14
+ Returns:
15
+ List of point dicts with id, x, y, z, and metadata
16
+ """
17
+ results = response.get("result") or response.get("points") or []
18
+
19
+ points = []
20
+ for r in results:
21
+ point: dict[str, Any] = {"id": str(r.get("id", ""))}
22
+
23
+ if "score" in r:
24
+ point["score"] = r["score"]
25
+
26
+ vector = r.get("vector")
27
+ payload = r.get("payload", {})
28
+
29
+ if vector:
30
+ point["x"] = float(vector[0]) if len(vector) > 0 else 0
31
+ point["y"] = float(vector[1]) if len(vector) > 1 else 0
32
+ point["z"] = float(vector[2]) if len(vector) > 2 else 0
33
+ point["vector"] = vector
34
+ else:
35
+ point["x"] = float(payload.get("x", 0))
36
+ point["y"] = float(payload.get("y", 0))
37
+ point["z"] = float(payload.get("z", 0))
38
+
39
+ point.update(payload)
40
+ points.append(point)
41
+
42
+ return points
43
+
44
+
45
+ def build_filter(conditions: list[tuple[str, str, Any]]) -> dict[str, Any]:
46
+ """Build Qdrant filter from conditions.
47
+
48
+ Args:
49
+ conditions: List of (field, operator, value) tuples
50
+ Operators: =, !=, >, >=, <, <=, ~, :
51
+
52
+ Returns:
53
+ Qdrant filter dict
54
+
55
+ Example:
56
+ >>> build_filter([("category", "=", "tech"), ("year", ">", 2020)])
57
+ {"must": [
58
+ {"key": "category", "match": {"value": "tech"}},
59
+ {"key": "year", "range": {"gt": 2020}}
60
+ ]}
61
+ """
62
+ must = []
63
+
64
+ for field, op, value in conditions:
65
+ if op == "=":
66
+ must.append({"key": field, "match": {"value": value}})
67
+ elif op == "!=":
68
+ # Qdrant doesn't have direct != so we use must_not
69
+ continue # Handle separately
70
+ elif op == ">":
71
+ must.append({"key": field, "range": {"gt": value}})
72
+ elif op == ">=":
73
+ must.append({"key": field, "range": {"gte": value}})
74
+ elif op == "<":
75
+ must.append({"key": field, "range": {"lt": value}})
76
+ elif op == "<=":
77
+ must.append({"key": field, "range": {"lte": value}})
78
+ elif op == "~":
79
+ must.append({"key": field, "match": {"text": value}})
80
+ elif op == ":":
81
+ must.append({"key": field, "match": {"any": [value]}})
82
+
83
+ return {"must": must} if must else {}
@@ -0,0 +1,33 @@
1
+ """Weaviate backend adapter.
2
+
3
+ Query Format (GraphQL):
4
+ # Get all with limit
5
+ { Get { Article(limit: 10) { title _additional { id vector } } } }
6
+
7
+ # Near vector search
8
+ {
9
+ Get {
10
+ Article(nearVector: {vector: [0.1, 0.2, ...]}, limit: 10) {
11
+ title
12
+ _additional { id vector distance }
13
+ }
14
+ }
15
+ }
16
+
17
+ # With where filter
18
+ {
19
+ Get {
20
+ Article(
21
+ where: {path: ["category"], operator: Equal, valueText: "tech"}
22
+ limit: 10
23
+ ) { title _additional { id } }
24
+ }
25
+ }
26
+
27
+ # Near text (requires text2vec module)
28
+ { Get { Article(nearText: {concepts: ["AI"]}, limit: 10) { ... } } }
29
+ """
30
+
31
+ from anywidget_vector.backends.weaviate.converter import build_where, to_points
32
+
33
+ __all__ = ["to_points", "build_where"]
@@ -0,0 +1,50 @@
1
+ // Weaviate browser-side client
2
+
3
+ export async function executeQuery(query, config) {
4
+ const { url, apiKey } = config;
5
+ const headers = { "Content-Type": "application/json" };
6
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
7
+
8
+ // Query is GraphQL string
9
+ const graphql = typeof query === "string" ? query : JSON.stringify(query);
10
+
11
+ const resp = await fetch(`${url}/v1/graphql`, {
12
+ method: "POST",
13
+ headers,
14
+ body: JSON.stringify({ query: graphql }),
15
+ });
16
+
17
+ if (!resp.ok) throw new Error(`Weaviate error: ${await resp.text()}`);
18
+ const data = await resp.json();
19
+
20
+ if (data.errors) {
21
+ throw new Error(`GraphQL error: ${data.errors[0].message}`);
22
+ }
23
+
24
+ return data;
25
+ }
26
+
27
+ export function toPoints(response, className) {
28
+ const data = response?.data?.Get?.[className] || [];
29
+
30
+ return data.map((item, i) => {
31
+ const additional = item._additional || {};
32
+ const vector = additional.vector || [];
33
+
34
+ const point = {
35
+ id: additional.id || `point_${i}`,
36
+ score: additional.distance ? 1 - additional.distance : undefined,
37
+ x: vector[0] ?? item.x ?? 0,
38
+ y: vector[1] ?? item.y ?? 0,
39
+ z: vector[2] ?? item.z ?? 0,
40
+ vector: vector.length ? vector : undefined,
41
+ };
42
+
43
+ // Add other fields
44
+ for (const [k, v] of Object.entries(item)) {
45
+ if (k !== "_additional") point[k] = v;
46
+ }
47
+
48
+ return point;
49
+ });
50
+ }
@@ -0,0 +1,81 @@
1
+ """Weaviate result conversion and filter building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def to_points(response: dict[str, Any], class_name: str) -> list[dict[str, Any]]:
9
+ """Convert Weaviate GraphQL response to points format."""
10
+ data = response.get("data", {}).get("Get", {}).get(class_name, [])
11
+
12
+ points = []
13
+ for i, item in enumerate(data):
14
+ additional = item.get("_additional", {})
15
+ point: dict[str, Any] = {"id": additional.get("id", f"point_{i}")}
16
+
17
+ if "distance" in additional:
18
+ point["score"] = 1 - additional["distance"]
19
+
20
+ vector = additional.get("vector", [])
21
+ if vector:
22
+ point["x"] = float(vector[0]) if len(vector) > 0 else 0
23
+ point["y"] = float(vector[1]) if len(vector) > 1 else 0
24
+ point["z"] = float(vector[2]) if len(vector) > 2 else 0
25
+ point["vector"] = vector
26
+
27
+ # Add all other fields except _additional
28
+ for k, v in item.items():
29
+ if k != "_additional":
30
+ point[k] = v
31
+
32
+ points.append(point)
33
+
34
+ return points
35
+
36
+
37
+ def build_where(conditions: list[tuple[str, str, Any]]) -> dict[str, Any]:
38
+ """Build Weaviate where filter from conditions.
39
+
40
+ Weaviate operators: Equal, NotEqual, GreaterThan, GreaterThanEqual,
41
+ LessThan, LessThanEqual, Like, ContainsAny
42
+ """
43
+ op_map = {
44
+ "=": "Equal",
45
+ "!=": "NotEqual",
46
+ ">": "GreaterThan",
47
+ ">=": "GreaterThanEqual",
48
+ "<": "LessThan",
49
+ "<=": "LessThanEqual",
50
+ "~": "Like",
51
+ ":": "ContainsAny",
52
+ }
53
+
54
+ if len(conditions) == 0:
55
+ return {}
56
+
57
+ if len(conditions) == 1:
58
+ field, op, value = conditions[0]
59
+ return _single_condition(field, op_map.get(op, "Equal"), value)
60
+
61
+ # Multiple conditions: AND them together
62
+ operands = [_single_condition(f, op_map.get(o, "Equal"), v) for f, o, v in conditions]
63
+ return {"operator": "And", "operands": operands}
64
+
65
+
66
+ def _single_condition(field: str, operator: str, value: Any) -> dict[str, Any]:
67
+ """Build single where condition."""
68
+ result = {"path": [field], "operator": operator}
69
+
70
+ if isinstance(value, str):
71
+ result["valueText"] = value
72
+ elif isinstance(value, bool):
73
+ result["valueBoolean"] = value
74
+ elif isinstance(value, int):
75
+ result["valueInt"] = value
76
+ elif isinstance(value, float):
77
+ result["valueNumber"] = value
78
+ elif isinstance(value, list):
79
+ result["valueText"] = value # For ContainsAny
80
+
81
+ return result
@@ -0,0 +1,14 @@
1
+ // SVG Icons for anywidget-vector UI
2
+ export const ICONS = {
3
+ play: '<svg viewBox="0 0 24 24" fill="currentColor"><polygon points="5,3 19,12 5,21"/></svg>',
4
+ settings: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83"/></svg>',
5
+ close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
6
+ search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
7
+ vector: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>',
8
+ filter: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22,3 2,3 10,12.46 10,19 14,21 14,12.46"/></svg>',
9
+ id: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="7" y1="9" x2="17" y2="9"/><line x1="7" y1="13" x2="13" y2="13"/></svg>',
10
+ text: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4,7 4,4 20,4 20,7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>',
11
+ loader: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10" stroke-dasharray="31.4" stroke-dashoffset="10"/></svg>',
12
+ cube: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27,6.96 12,12.01 20.73,6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>',
13
+ reset: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>',
14
+ };
@@ -0,0 +1,84 @@
1
+ """Traitlet definitions for VectorSpace widget."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import traitlets
6
+
7
+
8
+ class VectorSpaceTraits(traitlets.HasTraits):
9
+ """All traitlets for the VectorSpace widget, grouped logically."""
10
+
11
+ # === Data ===
12
+ points = traitlets.List(trait=traitlets.Dict()).tag(sync=True)
13
+
14
+ # === Display ===
15
+ width = traitlets.Int(default_value=800).tag(sync=True)
16
+ height = traitlets.Int(default_value=600).tag(sync=True)
17
+ background = traitlets.Unicode(default_value="#1a1a2e").tag(sync=True)
18
+
19
+ # === Axes and Grid ===
20
+ show_axes = traitlets.Bool(default_value=True).tag(sync=True)
21
+ show_grid = traitlets.Bool(default_value=True).tag(sync=True)
22
+ axis_labels = traitlets.Dict(default_value={"x": "X", "y": "Y", "z": "Z"}).tag(sync=True)
23
+ grid_divisions = traitlets.Int(default_value=10).tag(sync=True)
24
+
25
+ # === Color Mapping ===
26
+ color_field = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
27
+ color_scale = traitlets.Unicode(default_value="viridis").tag(sync=True)
28
+ color_domain = traitlets.List(default_value=None, allow_none=True).tag(sync=True)
29
+
30
+ # === Size Mapping ===
31
+ size_field = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
32
+ size_range = traitlets.List(default_value=[0.02, 0.1]).tag(sync=True)
33
+
34
+ # === Shape Mapping ===
35
+ shape_field = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
36
+ shape_map = traitlets.Dict(default_value={}).tag(sync=True)
37
+
38
+ # === Camera ===
39
+ camera_position = traitlets.List(default_value=[2, 2, 2]).tag(sync=True)
40
+ camera_target = traitlets.List(default_value=[0, 0, 0]).tag(sync=True)
41
+
42
+ # === Interaction ===
43
+ selected_points = traitlets.List(default_value=[]).tag(sync=True)
44
+ hovered_point = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True)
45
+ selection_mode = traitlets.Unicode(default_value="click").tag(sync=True)
46
+
47
+ # === Tooltip ===
48
+ show_tooltip = traitlets.Bool(default_value=True).tag(sync=True)
49
+ tooltip_fields = traitlets.List(default_value=["label", "x", "y", "z"]).tag(sync=True)
50
+
51
+ # === Performance ===
52
+ use_instancing = traitlets.Bool(default_value=True).tag(sync=True)
53
+ point_budget = traitlets.Int(default_value=100000).tag(sync=True)
54
+
55
+ # === Distance Metrics and Connections ===
56
+ distance_metric = traitlets.Unicode(default_value="euclidean").tag(sync=True)
57
+ show_connections = traitlets.Bool(default_value=False).tag(sync=True)
58
+ k_neighbors = traitlets.Int(default_value=0).tag(sync=True)
59
+ distance_threshold = traitlets.Float(default_value=None, allow_none=True).tag(sync=True)
60
+ reference_point = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
61
+ connection_color = traitlets.Unicode(default_value="#ffffff").tag(sync=True)
62
+ connection_opacity = traitlets.Float(default_value=0.3).tag(sync=True)
63
+
64
+ # === UI Visibility ===
65
+ show_toolbar = traitlets.Bool(default_value=False).tag(sync=True)
66
+ show_settings = traitlets.Bool(default_value=False).tag(sync=True)
67
+ show_properties = traitlets.Bool(default_value=False).tag(sync=True)
68
+
69
+ # === Backend Configuration ===
70
+ backend = traitlets.Unicode(default_value="qdrant").tag(sync=True)
71
+ backend_config = traitlets.Dict(default_value={}).tag(sync=True)
72
+ connection_status = traitlets.Unicode(default_value="disconnected").tag(sync=True)
73
+
74
+ # === Query Parameters ===
75
+ query_type = traitlets.Unicode(default_value="text").tag(sync=True)
76
+ query_input = traitlets.Unicode(default_value="").tag(sync=True)
77
+ query_limit = traitlets.Int(default_value=100).tag(sync=True)
78
+ query_error = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
79
+
80
+ # === Embedding Configuration ===
81
+ embedding_config = traitlets.Dict(default_value={}).tag(sync=True)
82
+
83
+ # === Internal Triggers ===
84
+ _execute_query = traitlets.Int(default_value=0).tag(sync=True)
@@ -0,0 +1,206 @@
1
+ """UI module aggregation - loads CSS and JS components."""
2
+
3
+ from pathlib import Path
4
+
5
+ _UI_DIR = Path(__file__).parent
6
+ _STATIC_DIR = _UI_DIR.parent / "static"
7
+ _BACKENDS_DIR = _UI_DIR.parent / "backends"
8
+
9
+
10
+ def _read_file(path: Path) -> str:
11
+ """Read file contents."""
12
+ return path.read_text(encoding="utf-8")
13
+
14
+
15
+ def get_css() -> str:
16
+ """Get aggregated CSS."""
17
+ return _read_file(_UI_DIR / "styles.css")
18
+
19
+
20
+ def get_esm() -> str:
21
+ """Get aggregated ESM JavaScript.
22
+
23
+ Combines all UI components into a single module.
24
+ """
25
+ # Read component files
26
+ icons_js = _read_file(_STATIC_DIR / "icons.js")
27
+ constants_js = _read_file(_UI_DIR / "constants.js")
28
+ toolbar_js = _read_file(_UI_DIR / "toolbar.js")
29
+ settings_js = _read_file(_UI_DIR / "settings.js")
30
+ properties_js = _read_file(_UI_DIR / "properties.js")
31
+ canvas_js = _read_file(_UI_DIR / "canvas.js")
32
+
33
+ # Read browser-side backend clients
34
+ qdrant_client = _read_file(_BACKENDS_DIR / "qdrant" / "client.js")
35
+ pinecone_client = _read_file(_BACKENDS_DIR / "pinecone" / "client.js")
36
+ weaviate_client = _read_file(_BACKENDS_DIR / "weaviate" / "client.js")
37
+
38
+ # Build combined ESM
39
+ esm = f"""
40
+ // === Auto-generated ESM bundle for anywidget-vector ===
41
+ import * as THREE from "https://esm.sh/three@0.160.0";
42
+ import {{ OrbitControls }} from "https://esm.sh/three@0.160.0/addons/controls/OrbitControls.js";
43
+
44
+ // === Icons ===
45
+ {_strip_imports_exports(icons_js)}
46
+
47
+ // === Constants ===
48
+ {_strip_imports_exports(constants_js)}
49
+
50
+ // === Backend Clients ===
51
+ // Qdrant
52
+ {_rename_functions(qdrant_client, "qdrant")}
53
+
54
+ // Pinecone
55
+ {_rename_functions(pinecone_client, "pinecone")}
56
+
57
+ // Weaviate
58
+ {_rename_functions(weaviate_client, "weaviate")}
59
+
60
+ // Unified query executor
61
+ async function executeBackendQuery(model) {{
62
+ const backend = model.get("backend");
63
+ const query = model.get("query_input");
64
+ const config = model.get("backend_config") || {{}};
65
+
66
+ let response, points;
67
+
68
+ if (backend === "qdrant") {{
69
+ response = await qdrantExecute(query, config);
70
+ points = qdrantToPoints(response);
71
+ }} else if (backend === "pinecone") {{
72
+ response = await pineconeExecute(query, config);
73
+ points = pineconeToPoints(response);
74
+ }} else if (backend === "weaviate") {{
75
+ const className = config.className || config.class || "Vector";
76
+ response = await weaviateExecute(query, config);
77
+ points = weaviateToPoints(response, className);
78
+ }} else {{
79
+ throw new Error(`Browser backend not supported: ${{backend}}`);
80
+ }}
81
+
82
+ return points;
83
+ }}
84
+
85
+ // === Toolbar ===
86
+ {_strip_imports_exports(toolbar_js)}
87
+
88
+ // === Settings Panel ===
89
+ {_strip_imports_exports(settings_js)}
90
+
91
+ // === Properties Panel ===
92
+ {_strip_imports_exports(properties_js)}
93
+
94
+ // === Canvas (Three.js) ===
95
+ {_strip_imports_exports(canvas_js)}
96
+
97
+ // === Main Render Function ===
98
+ function render({{ model, el }}) {{
99
+ const wrapper = document.createElement("div");
100
+ wrapper.className = "avs-wrapper";
101
+ wrapper.style.width = model.get("width") + "px";
102
+ el.appendChild(wrapper);
103
+
104
+ let settingsPanel = null;
105
+ let propertiesPanel = null;
106
+ let toolbarUI = null;
107
+
108
+ if (model.get("show_toolbar")) {{
109
+ toolbarUI = createToolbar(model, {{
110
+ onRunQuery: () => runQuery(),
111
+ onToggleSettings: () => settingsPanel?.toggle(),
112
+ onToggleProperties: () => propertiesPanel?.toggle(),
113
+ }});
114
+ wrapper.appendChild(toolbarUI.element);
115
+ }}
116
+
117
+ const main = document.createElement("div");
118
+ main.className = "avs-main";
119
+ main.style.height = model.get("height") + "px";
120
+ wrapper.appendChild(main);
121
+
122
+ const canvasContainer = document.createElement("div");
123
+ canvasContainer.className = "avs-canvas-container";
124
+ canvasContainer.style.width = "100%";
125
+ canvasContainer.style.height = "100%";
126
+ main.appendChild(canvasContainer);
127
+
128
+ if (model.get("show_settings")) {{
129
+ settingsPanel = createSettingsPanel(model, {{
130
+ onClose: () => settingsPanel?.close(),
131
+ }});
132
+ main.appendChild(settingsPanel.element);
133
+ }}
134
+
135
+ if (model.get("show_properties")) {{
136
+ propertiesPanel = createPropertiesPanel(model, {{
137
+ onClose: () => propertiesPanel?.close(),
138
+ }});
139
+ main.appendChild(propertiesPanel.element);
140
+ }}
141
+
142
+ const canvas = createCanvas(model, canvasContainer, {{}});
143
+
144
+ async function runQuery() {{
145
+ if (toolbarUI) toolbarUI.setLoading(true);
146
+
147
+ try {{
148
+ model.set("query_error", null);
149
+ model.set("connection_status", "connecting");
150
+ model.save_changes();
151
+
152
+ const backend = model.get("backend");
153
+ const backendInfo = BACKENDS[backend];
154
+
155
+ if (backendInfo && backendInfo.side === "browser") {{
156
+ const points = await executeBackendQuery(model);
157
+ model.set("points", points);
158
+ model.set("connection_status", "connected");
159
+ model.save_changes();
160
+ }} else {{
161
+ model.set("_execute_query", Date.now());
162
+ model.save_changes();
163
+ }}
164
+ }} catch (err) {{
165
+ model.set("query_error", err.message);
166
+ model.set("connection_status", "error");
167
+ model.save_changes();
168
+ console.error("Query error:", err);
169
+ }} finally {{
170
+ if (toolbarUI) toolbarUI.setLoading(false);
171
+ }}
172
+ }}
173
+
174
+ return () => {{ canvas.cleanup(); }};
175
+ }}
176
+
177
+ export default {{ render }};
178
+ """
179
+ return esm
180
+
181
+
182
+ def _strip_imports_exports(code: str) -> str:
183
+ """Remove import and export statements from JS code."""
184
+ lines = []
185
+ for line in code.split("\n"):
186
+ stripped = line.strip()
187
+ if stripped.startswith("import "):
188
+ continue
189
+ if stripped.startswith("export function "):
190
+ line = line.replace("export function ", "function ")
191
+ elif stripped.startswith("export async function "):
192
+ line = line.replace("export async function ", "async function ")
193
+ elif stripped.startswith("export const "):
194
+ line = line.replace("export const ", "const ")
195
+ elif stripped.startswith("export default"):
196
+ continue
197
+ lines.append(line)
198
+ return "\n".join(lines)
199
+
200
+
201
+ def _rename_functions(code: str, prefix: str) -> str:
202
+ """Strip imports/exports and rename functions with prefix."""
203
+ code = _strip_imports_exports(code)
204
+ code = code.replace("function executeQuery", f"async function {prefix}Execute")
205
+ code = code.replace("function toPoints", f"function {prefix}ToPoints")
206
+ return code