Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
099ff26
redesign: overhaul frontend UX with approachable AI styling
IrinaMBejan Apr 10, 2026
e481fcd
Merge remote-tracking branch 'origin/main' into redesign_ux
IrinaMBejan Apr 10, 2026
48b758b
feat: add local chat for testing resources and rename endpoint → API …
IrinaMBejan Apr 10, 2026
e436f9a
feat: add PII Filter toggle to publish wizard and update usage limit …
IrinaMBejan Apr 10, 2026
4496ef2
chore: remove redundant tags helper text from publish wizard
IrinaMBejan Apr 10, 2026
17da4d8
fix(chat): harden LocalChatHandler with timeouts, error classificatio…
IonesioJunior Apr 10, 2026
b5a690f
feat(policies): implement PII filter policy end-to-end (OME-234) (#138)
IonesioJunior Apr 10, 2026
705eebc
feat(endpoints): persist per-endpoint system prompt override (OME-235…
IonesioJunior Apr 10, 2026
a21d307
fix(sidebar): make resource search trustworthy (OME-236) (#135)
IonesioJunior Apr 10, 2026
3e9884e
feat(chat): add no-models empty state and submit keyboard shortcut (O…
IonesioJunior Apr 10, 2026
b9e4dc9
backend(chat): add system prompt support + extract error classification
IonesioJunior Apr 11, 2026
7447dc5
backend(endpoints): async parallelism + expose system_prompt in response
IonesioJunior Apr 11, 2026
4994f30
backend(prepaid): concurrent-safe quota decrement + tenant-scoped pur…
IonesioJunior Apr 11, 2026
48cb72e
frontend(endpoints-store): cache freshness, dedup, systemPrompt, inva…
IonesioJunior Apr 11, 2026
b7c945c
frontend(chat): endpoint-based selectors + system prompt passthrough …
IonesioJunior Apr 11, 2026
39dd8a1
frontend(sidebar): extract SidebarNavItem, precompute nav, use useSto…
IonesioJunior Apr 11, 2026
e0a915f
frontend(go-live): remove inline create-resource, fix computed name, …
IonesioJunior Apr 11, 2026
4e73871
frontend: remove mock data and dead create-resource dialogs
IonesioJunior Apr 11, 2026
7eb6179
frontend: extract inbox formatters, fix null marketplace check, misc …
IonesioJunior Apr 11, 2026
aa801b4
fix(endpoints): use slug for routing, add API type badges
IonesioJunior Apr 14, 2026
64cee25
feat(datasets): block deletion of datasets in use by endpoints
IonesioJunior Apr 14, 2026
6a09c97
feat(chat): improve data-only and combined endpoint UX
IonesioJunior Apr 14, 2026
e131e3b
fix(backend): remove document cap, fix similarity threshold checks, f…
IonesioJunior Apr 14, 2026
7228e6e
feat(chat): replace local chat with endpoint-based preview
IonesioJunior May 7, 2026
20ba74b
feat(pii-filter): replace regex-based PII with AI model-powered redac…
IonesioJunior May 7, 2026
6678b7f
chore: add GitNexus index config to .gitignore and CLAUDE.md
IonesioJunior May 7, 2026
a622761
chore: merge origin/main into redesign_ux
IonesioJunior May 7, 2026
c055cbe
feat(ui): remove inbox component from redesign_ux
IonesioJunior May 7, 2026
68b2829
fix(chat): render assistant responses as markdown
IonesioJunior May 7, 2026
0385b76
refactor(ui): redesign UX across pages and components
IonesioJunior May 11, 2026
eddd8ca
feat(chat): orchestrate multi-source RAG client-side
IonesioJunior May 11, 2026
2393a10
refactor: drop dead code, share parseTags, slim usePolicyCreation
IonesioJunior May 11, 2026
5ccc673
Merge remote-tracking branch 'origin/main' into redesign_ux
IonesioJunior May 22, 2026
f9286dc
Merge remote-tracking branch 'origin/main' into redesign_ux
itstauq Jun 5, 2026
ac12844
ci: fix advisory commit lint and migration heads
itstauq Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/commit-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ jobs:
else
rows+="| \`${sha:0:7}\` | fail | ${safe_subject} |"$'\n'
failures+="- \`${sha:0:7}\` ${subject}"$'\n'
echo "FAIL ${sha:0:7} ${subject}"
echo "::error title=Non-conventional commit ${sha:0:7}::${subject}"
echo "WARN ${sha:0:7} ${subject}"
echo "::warning title=Non-conventional commit ${sha:0:7}::${subject}"
bad=$((bad+1))
fi
done < <(git log --reverse --format='%H%x09%s' "$BASE_SHA..$HEAD_SHA")
Expand Down Expand Up @@ -81,7 +81,6 @@ jobs:

if [ "$bad" -gt 0 ]; then
echo ""
echo "${bad} commit(s) do not follow Conventional Commits:"
echo "${bad} commit(s) do not follow Conventional Commits (advisory only):"
printf '%s' "$failures"
exit 1
fi
102 changes: 102 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,105 @@ When integrating a backend API endpoint into the frontend:
- Use bun for all frontend package operations
- Backend uses UV for Python package management
- The app serves frontend static files and redirects root to `/syft-space-server`

<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **redesign_ux** (2333 symbols, 5605 relationships, 171 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

## Always Do

- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`.

## When Debugging

1. `gitnexus_query({query: "<error or symptom>"})` — find execution flows related to the issue
2. `gitnexus_context({name: "<suspect function>"})` — see all callers, callees, and process participation
3. `READ gitnexus://repo/redesign_ux/process/{processName}` — trace the full execution flow step by step
4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed

## When Refactoring

- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`.
- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code.
- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed.

## Never Do

- NEVER edit a function, class, or method without first running `gitnexus_impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph.
- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope.

## Tools Quick Reference

| Tool | When to use | Command |
|------|-------------|---------|
| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` |
| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` |
| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` |
| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` |
| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` |
| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` |

## Impact Risk Levels

| Depth | Meaning | Action |
|-------|---------|--------|
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
| d=3 | MAY NEED TESTING — transitive | Test if critical path |

## Resources

| Resource | Use for |
|----------|---------|
| `gitnexus://repo/redesign_ux/context` | Codebase overview, check index freshness |
| `gitnexus://repo/redesign_ux/clusters` | All functional areas |
| `gitnexus://repo/redesign_ux/processes` | All execution flows |
| `gitnexus://repo/redesign_ux/process/{name}` | Step-by-step execution trace |

## Self-Check Before Finishing

Before completing any code modification task, verify:
1. `gitnexus_impact` was run for all modified symbols
2. No HIGH/CRITICAL risk warnings were ignored
3. `gitnexus_detect_changes()` confirms changes match expected scope
4. All d=1 (WILL BREAK) dependents were updated

## Keeping the Index Fresh

After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it:

```bash
npx gitnexus analyze
```

If the index previously included embeddings, preserve them by adding `--embeddings`:

```bash
npx gitnexus analyze --embeddings
```

To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.**

> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`.

## CLI

| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |

<!-- gitnexus:end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""add system_prompt to endpoints

Revision ID: e5f6a7b8c9d0
Revises: cd1cf206c880
Create Date: 2026-04-10 12:00:00.000000

"""

from collections.abc import Sequence

import sqlalchemy as sa
import sqlmodel
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "e5f6a7b8c9d0"
down_revision: str | None = "cd1cf206c880"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Add nullable ``system_prompt`` column to the ``endpoints`` table."""
with op.batch_alter_table("endpoints") as batch_op:
batch_op.add_column(
sa.Column(
"system_prompt",
sqlmodel.sql.sqltypes.AutoString(),
nullable=True,
)
)


def downgrade() -> None:
"""Drop the ``system_prompt`` column from the ``endpoints`` table."""
with op.batch_alter_table("endpoints") as batch_op:
batch_op.drop_column("system_prompt")
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""merge migration heads

Revision ID: f0a1b2c3d4e5
Revises: d9b3cb044bc7, e5f6a7b8c9d0
Create Date: 2026-06-05 15:35:00.000000

"""

from collections.abc import Sequence

# revision identifiers, used by Alembic.
revision: str = "f0a1b2c3d4e5"
down_revision: tuple[str, str] | None = ("d9b3cb044bc7", "e5f6a7b8c9d0")
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
pass


def downgrade() -> None:
pass
15 changes: 15 additions & 0 deletions backend/syft_space/components/datasets/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
ProvisionerStateRepository,
)
from syft_space.components.datasets.repository import DatasetRepository
from syft_space.components.endpoints.repository import EndpointRepository
from syft_space.components.datasets.schemas import (
BrowseResponse,
CreateDatasetRequest,
Expand All @@ -50,17 +51,20 @@ def __init__(
registry: DatasetTypeRegistry,
repository: DatasetRepository,
provisioner_state_repository: ProvisionerStateRepository,
endpoint_repository: EndpointRepository | None = None,
):
"""Initialize the dataset handler.

Args:
registry: Dataset type registry
repository: Dataset repository
provisioner_state_repository: Provisioner state repository
endpoint_repository: Endpoint repository (used to guard against deleting datasets in use)
"""
self.registry = registry
self.repository = repository
self.provisioner_state_repository = provisioner_state_repository
self.endpoint_repository = endpoint_repository

# ============== Private Provisioner Lifecycle Methods ==============

Expand Down Expand Up @@ -548,6 +552,17 @@ async def delete_dataset(self, name: str, tenant: Tenant) -> dict:
if not dataset:
raise HTTPException(status_code=404, detail=f"Dataset '{name}' not found")

if self.endpoint_repository:
attached = await self.endpoint_repository.get_by_dataset_id(
dataset.id, tenant.id
)
if attached:
names = ", ".join(f"'{e.name}'" for e in attached)
raise HTTPException(
status_code=409,
detail=f"Cannot delete data source '{name}': it is used by {len(attached)} API(s): {names}. Remove it from those APIs first.",
)

if dataset.provisioner_state_id:
logger.info(
f"Dataset '{name}' was linked to provisioner, keeping provisioner running"
Expand Down
8 changes: 8 additions & 0 deletions backend/syft_space/components/endpoints/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ class Endpoint(SQLModel, table=True):
description="Whether endpoint is archived (no new purchases, still queryable)",
)
tags: str = Field(default="", description="Comma-separated tags")
system_prompt: str | None = Field(
default=None,
description=(
"Custom system prompt to override the model's default at request time. "
"When set, it is prepended (or replaces the first system message) on "
"every chat invocation."
),
)
published_to: list[str] = Field(
default_factory=list,
sa_column=Column(JSON),
Expand Down
1 change: 1 addition & 0 deletions backend/syft_space/components/endpoints/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ async def create_endpoint(
response_type=request.response_type,
published=request.published,
tags=request.tags,
system_prompt=request.system_prompt,
tenant_id=tenant.id,
)

Expand Down
16 changes: 12 additions & 4 deletions backend/syft_space/components/endpoints/query_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Query endpoint handler — RAG pipeline with policy enforcement."""

from typing import Any

from fastapi import HTTPException
from loguru import logger

Expand Down Expand Up @@ -201,7 +203,12 @@ async def query_endpoint(
response_type in [ResponseType.SUMMARY, ResponseType.BOTH]
and endpoint.model_id
):
summary = await self._chat_with_model(endpoint, request, references)
summary, _model_instance, _model_id = await self._chat_with_model(
endpoint, request, references
)
if "pii_filter" in configs_by_type:
policy_context.metadata["model_instance"] = _model_instance
policy_context.metadata["model_id"] = _model_id

# Create response
query_response = QueryEndpointResponse(
Expand All @@ -228,7 +235,7 @@ async def query_endpoint(
detail=f"Policy '{e.policy_type}' blocked request: {e.details}",
) from e

# Extract payment receipt header if present (set by MppAccountingPolicy post-hook)
# Extract payment receipt header if present (set by MPP payment policy post-hook)
payment_receipt = policy_context.metadata.get("payment_receipt_header")

final_response = QueryEndpointResponse.model_validate(
Expand Down Expand Up @@ -327,7 +334,7 @@ async def _chat_with_model(
endpoint: Endpoint,
request: AuthenticatedQueryRequest,
references: ReferencesResponse | None,
) -> SummaryResponse:
) -> tuple[SummaryResponse, Any, str]:
"""Chat with the model linked to this endpoint."""
Comment on lines 334 to 338
model = await self.model_repository.get_by_id(
endpoint.model_id, endpoint.tenant_id
Expand Down Expand Up @@ -395,7 +402,7 @@ async def _chat_with_model(
)
raise HTTPException(status_code=500, detail="Model returned no messages")

return SummaryResponse(
summary = SummaryResponse(
id=chat_result.id,
model=chat_result.model,
message=MessageResponse(
Expand All @@ -411,3 +418,4 @@ async def _chat_with_model(
),
provider_info=ProviderInfo(api_version="v1"),
)
return summary, model_instance, str(model.id)
7 changes: 7 additions & 0 deletions backend/syft_space/components/endpoints/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ async def update_by_slug(
name: str | None = None,
summary: str | None = None,
description: str | None = None,
system_prompt: str | None = None,
) -> Endpoint | None:
"""Update an endpoint by slug within a tenant.

Expand All @@ -119,6 +120,9 @@ async def update_by_slug(
name: New endpoint name
summary: Updated summary
description: Updated markdown description
system_prompt: Updated custom system prompt. An empty string
clears the override (falls back to model default); ``None``
leaves the existing value untouched.

Returns:
Updated endpoint if found, None otherwise
Expand Down Expand Up @@ -146,6 +150,9 @@ async def update_by_slug(
endpoint.summary = summary
if description is not None:
endpoint.description = description
if system_prompt is not None:
# Empty string clears the override; any non-empty value sets it
endpoint.system_prompt = system_prompt or None

endpoint.updated_at = datetime.now(timezone.utc)
session.add(endpoint)
Expand Down
38 changes: 38 additions & 0 deletions backend/syft_space/components/endpoints/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,44 @@ async def query_endpoint(

return response

@router.post("/{slug}/preview", response_model=QueryEndpointResponse)
async def preview_endpoint(
slug: str,
request: QueryEndpointRequest,
tenant: Tenant = Depends(get_tenant_dependency),
handler: PublishEndpointHandler = Depends(get_publish_handler),
qhandler: QueryEndpointHandler = Depends(get_query_handler),
) -> QueryEndpointResponse | JSONResponse:
"""Preview an endpoint as the space owner (admin auth, full policy pipeline).

Identical to /query but uses admin auth so the owner can test exactly
what external users experience — policies, filters, and all.
"""
marketplace = await handler.marketplace_repository.get_default(tenant.id)
sender_email = marketplace.email if marketplace else "owner@localhost.local"

auth_request = AuthenticatedQueryRequest.from_request(request, sender_email)

try:
response, payment_receipt = await qhandler.query_endpoint(
slug, auth_request, tenant, x_payment=None
)
except PaymentRequiredError as e:
return JSONResponse(
status_code=402,
content={"detail": e.description or "Payment required"},
headers={"WWW-Authenticate": e.www_authenticate},
)

if payment_receipt:
return JSONResponse(
status_code=200,
content=response.model_dump(),
headers={"Payment-Receipt": payment_receipt},
)

return response

# ── Publish routes (PublishEndpointHandler) ──────────────────

@router.post("/validate-slug", response_model=SlugAvailabilityResponse)
Expand Down
Loading
Loading