brandind and misc fixes
This commit is contained in:
parent
f813a807e7
commit
fa554b8980
65 changed files with 3732 additions and 3246 deletions
|
|
@ -145,3 +145,7 @@ Note: The maintained wrapper `hindsight_client.py` and `README.md` are preserved
|
||||||
- PostgreSQL with pgvector extension
|
- PostgreSQL with pgvector extension
|
||||||
- Schema managed via Alembic migrations in `hindsight-api/alembic/`, db migrations happen during api startup, no manual commands
|
- Schema managed via Alembic migrations in `hindsight-api/alembic/`, db migrations happen during api startup, no manual commands
|
||||||
- Key tables: `banks`, `memory_units`, `documents`, `entities`, `entity_links`
|
- Key tables: `banks`, `memory_units`, `documents`, `entities`, `entity_links`
|
||||||
|
|
||||||
|
# Branding
|
||||||
|
## Colors
|
||||||
|
- Primary: gradient from #0074d9 to #009296
|
||||||
|
|
|
||||||
30
README.md
30
README.md
|
|
@ -4,10 +4,11 @@
|
||||||
|
|
||||||
[Documentation](https://vectorize-io.github.io/hindsight) • [Paper](#coming-soon) • [Examples](https://github.com/vectorize-io/hindsight-cookbook)
|
[Documentation](https://vectorize-io.github.io/hindsight) • [Paper](#coming-soon) • [Examples](https://github.com/vectorize-io/hindsight-cookbook)
|
||||||
|
|
||||||
[](https://github.com/vectorize-io/hindsight/actions/workflows/test.yml)
|
[](https://github.com/vectorize-io/hindsight/actions/workflows/release.yml)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://pypi.org/project/hindsight-api/)
|
||||||
[](https://pypi.org/project/hindsight-client/)
|
[](https://pypi.org/project/hindsight-client/)
|
||||||
[](https://www.npmjs.com/package/@vectorize-io/hindsight-client)
|
[](https://www.npmjs.com/package/@vectorize-io/hindsight-client)
|
||||||
[](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
|
[](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -53,12 +54,10 @@ Memories in Hindsight are stored in banks (e.g. memory banks). When memories are
|
||||||
```bash
|
```bash
|
||||||
export OPENAI_API_KEY=your-key
|
export OPENAI_API_KEY=your-key
|
||||||
|
|
||||||
docker run -p 8888:8888 -p 9999:9999 \
|
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
|
||||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
|
||||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||||
ghcr.io/vectorize-io/hindsight
|
ghcr.io/vectorize-io/hindsight:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
API: http://localhost:8888
|
API: http://localhost:8888
|
||||||
|
|
@ -208,29 +207,18 @@ client.reflect(bank_id="my-bank", query="What should I know about Alice?")
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
[Examples Repo]([./examples](https://github.com/vectorize-io/hindsight-cookbook)) includes:
|
|
||||||
|
|
||||||
- Basic usage
|
|
||||||
- Multi-session conversations
|
|
||||||
- Temporal queries
|
|
||||||
- Entity reasoning
|
|
||||||
- Opinion tracking
|
|
||||||
- Production setup (Docker Compose + monitoring)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
**Documentation:** [vectorize-io.github.io/hindsight](https://vectorize-io.github.io/hindsight)
|
**Documentation:**
|
||||||
|
- [https://hindsight.vectorize.io](https://hindsight.vectorize.io)
|
||||||
|
|
||||||
**Clients:**
|
**Clients:**
|
||||||
- [Python](http://hindsight.vectorize.io/sdks/python)
|
- [Python](http://hindsight.vectorize.io/sdks/python)
|
||||||
- [Node.js](http://hindsight.vectorize.io/sdks/nodejs)
|
- [Node.js](http://hindsight.vectorize.io/sdks/nodejs)
|
||||||
- [REST API](http://hindsight.vectorize.io/api-reference)
|
- [REST API](https://hindsight.vectorize.io/api-reference)
|
||||||
|
- [CLI](https://hindsight.vectorize.io/sdks/cli)
|
||||||
|
|
||||||
**Community:**
|
**Community:**
|
||||||
- [Slack](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
|
- [Slack](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "🚀 Starting Hindsight..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Service flags (default to true if not set)
|
# Service flags (default to true if not set)
|
||||||
ENABLE_API="${HINDSIGHT_ENABLE_API:-true}"
|
ENABLE_API="${HINDSIGHT_ENABLE_API:-true}"
|
||||||
ENABLE_CP="${HINDSIGHT_ENABLE_CP:-true}"
|
ENABLE_CP="${HINDSIGHT_ENABLE_CP:-true}"
|
||||||
|
|
@ -31,16 +28,14 @@ if [ "$ENABLE_API" = "true" ]; then
|
||||||
PIDS+=($API_PID)
|
PIDS+=($API_PID)
|
||||||
|
|
||||||
# Wait for API to be ready
|
# Wait for API to be ready
|
||||||
echo "⏳ Waiting for API..."
|
|
||||||
for i in {1..60}; do
|
for i in {1..60}; do
|
||||||
if curl -sf http://localhost:8888/health &>/dev/null; then
|
if curl -sf http://localhost:8888/health &>/dev/null; then
|
||||||
echo "✅ API is ready"
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
echo "⏭️ API disabled (HINDSIGHT_ENABLE_API=false)"
|
echo "API disabled (HINDSIGHT_ENABLE_API=false)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start Control Plane if enabled
|
# Start Control Plane if enabled
|
||||||
|
|
@ -51,7 +46,7 @@ if [ "$ENABLE_CP" = "true" ]; then
|
||||||
CP_PID=$!
|
CP_PID=$!
|
||||||
PIDS+=($CP_PID)
|
PIDS+=($CP_PID)
|
||||||
else
|
else
|
||||||
echo "⏭️ Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
|
echo "Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Print status
|
# Print status
|
||||||
|
|
|
||||||
|
|
@ -672,11 +672,15 @@ class DeleteResponse(BaseModel):
|
||||||
"""Response model for delete operations."""
|
"""Response model for delete operations."""
|
||||||
model_config = ConfigDict(json_schema_extra={
|
model_config = ConfigDict(json_schema_extra={
|
||||||
"example": {
|
"example": {
|
||||||
"success": True
|
"success": True,
|
||||||
|
"message": "Deleted successfully",
|
||||||
|
"deleted_count": 10
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
success: bool
|
success: bool
|
||||||
|
message: Optional[str] = None
|
||||||
|
deleted_count: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
def create_app(memory: MemoryEngine, initialize_memory: bool = True) -> FastAPI:
|
||||||
|
|
@ -1696,6 +1700,31 @@ def _register_routes(app: FastAPI):
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete(
|
||||||
|
"/v1/default/banks/{bank_id}",
|
||||||
|
response_model=DeleteResponse,
|
||||||
|
summary="Delete memory bank",
|
||||||
|
description="Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. "
|
||||||
|
"This is a destructive operation that cannot be undone.",
|
||||||
|
operation_id="delete_bank",
|
||||||
|
tags=["Banks"]
|
||||||
|
)
|
||||||
|
async def api_delete_bank(bank_id: str):
|
||||||
|
"""Delete an entire memory bank and all its data."""
|
||||||
|
try:
|
||||||
|
result = await app.state.memory.delete_bank(bank_id)
|
||||||
|
return DeleteResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"Bank '{bank_id}' and all associated data deleted successfully",
|
||||||
|
deleted_count=result.get("memory_units_deleted", 0) + result.get("entities_deleted", 0) + result.get("documents_deleted", 0)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
|
||||||
|
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}: {error_detail}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
"/v1/default/banks/{bank_id}/memories",
|
"/v1/default/banks/{bank_id}/memories",
|
||||||
response_model=RetainResponse,
|
response_model=RetainResponse,
|
||||||
|
|
|
||||||
89
hindsight-api/hindsight_api/banner.py
Normal file
89
hindsight-api/hindsight_api/banner.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
"""
|
||||||
|
Banner display for Hindsight API startup.
|
||||||
|
|
||||||
|
Shows the logo and tagline with gradient colors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Gradient colors: #0074d9 -> #009296
|
||||||
|
GRADIENT_START = (0, 116, 217) # #0074d9
|
||||||
|
GRADIENT_END = (0, 146, 150) # #009296
|
||||||
|
|
||||||
|
# Pre-generated logo (generated by test-logo.py)
|
||||||
|
LOGO = """\
|
||||||
|
\033[38;2;9;127;184m\u2584\033[0m\033[48;2;8;130;178m\033[38;2;5;133;186m\u2584\033[0m \033[48;2;10;143;160m\033[38;2;10;143;165m\u2584\033[0m\033[38;2;7;140;156m\u2584\033[0m
|
||||||
|
\033[38;2;8;125;192m\u2584\033[0m \033[38;2;3;132;191m\u2580\033[0m\033[38;2;2;133;192m\u2584\033[0m \033[38;2;3;132;180m\u2584\033[0m\033[38;2;1;137;184m\u2584\033[0m\033[38;2;3;133;174m\u2584\033[0m \033[38;2;3;142;176m\u2584\033[0m\033[38;2;4;142;169m\u2580\033[0m \033[38;2;10;144;164m\u2584\033[0m
|
||||||
|
\033[38;2;6;121;195m\u2580\033[0m\033[38;2;5;128;203m\u2580\033[0m\033[48;2;5;124;195m\033[38;2;3;125;200m\u2584\033[0m\033[38;2;2;126;196m\u2584\033[0m\033[48;2;3;128;188m\033[38;2;1;131;196m\u2584\033[0m\033[48;2;0;152;219m\033[38;2;2;131;191m\u2584\033[0m\033[38;2;1;141;196m\u2580\033[0m\033[38;2;1;135;183m\u2580\033[0m\033[38;2;1;148;198m\u2580\033[0m\033[48;2;1;156;202m\033[38;2;2;135;180m\u2584\033[0m\033[48;2;4;134;169m\033[38;2;1;137;177m\u2584\033[0m\033[38;2;3;138;173m\u2584\033[0m\033[48;2;6;137;165m\033[38;2;2;140;170m\u2584\033[0m\033[38;2;7;144;169m\u2580\033[0m\033[38;2;7;139;158m\u2580\033[0m
|
||||||
|
\033[48;2;2;128;202m\033[38;2;2;124;201m\u2584\033[0m\033[48;2;1;130;201m\033[38;2;0;135;212m\u2584\033[0m\033[38;2;2;128;196m\u2584\033[0m \033[48;2;2;142;204m\033[38;2;7;138;199m\u2584\033[0m \033[38;2;1;135;186m\u2584\033[0m\033[48;2;1;142;186m\033[38;2;2;144;194m\u2584\033[0m\033[48;2;3;138;176m\033[38;2;2;134;176m\u2584\033[0m
|
||||||
|
\033[48;2;8;118;200m\033[38;2;8;121;209m\u2584\033[0m\033[38;2;3;121;203m\u2580\033[0m \033[38;2;3;122;192m\u2580\033[0m\033[38;2;1;138;216m\u2580\033[0m\033[48;2;0;138;210m\033[38;2;3;128;198m\u2584\033[0m\033[48;2;0;126;188m\033[38;2;2;131;198m\u2584\033[0m\033[48;2;0;142;205m\033[38;2;3;132;193m\u2584\033[0m\033[38;2;1;140;196m\u2580\033[0m \033[38;2;4;134;175m\u2580\033[0m\033[48;2;13;135;167m\033[38;2;8;136;174m\u2584\033[0m """
|
||||||
|
|
||||||
|
|
||||||
|
def _interpolate_color(start: tuple, end: tuple, t: float) -> tuple:
|
||||||
|
"""Interpolate between two RGB colors."""
|
||||||
|
return (
|
||||||
|
int(start[0] + (end[0] - start[0]) * t),
|
||||||
|
int(start[1] + (end[1] - start[1]) * t),
|
||||||
|
int(start[2] + (end[2] - start[2]) * t),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gradient_text(text: str, start: tuple = GRADIENT_START, end: tuple = GRADIENT_END) -> str:
|
||||||
|
"""Render text with a gradient color effect."""
|
||||||
|
result = []
|
||||||
|
length = len(text)
|
||||||
|
for i, char in enumerate(text):
|
||||||
|
if char == ' ':
|
||||||
|
result.append(' ')
|
||||||
|
else:
|
||||||
|
t = i / max(length - 1, 1)
|
||||||
|
r, g, b = _interpolate_color(start, end, t)
|
||||||
|
result.append(f"\033[38;2;{r};{g};{b}m{char}")
|
||||||
|
result.append("\033[0m")
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def print_banner():
|
||||||
|
"""Print the Hindsight startup banner."""
|
||||||
|
print(LOGO)
|
||||||
|
tagline = gradient_text("Hindsight: Agent Memory That Works Like Human Memory")
|
||||||
|
print(f"\n {tagline}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def color(text: str, t: float = 0.0) -> str:
|
||||||
|
"""Color text using gradient position (0.0 = start, 1.0 = end)."""
|
||||||
|
r, g, b = _interpolate_color(GRADIENT_START, GRADIENT_END, t)
|
||||||
|
return f"\033[38;2;{r};{g};{b}m{text}\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def color_start(text: str) -> str:
|
||||||
|
"""Color text with gradient start color (#0074d9)."""
|
||||||
|
return color(text, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def color_end(text: str) -> str:
|
||||||
|
"""Color text with gradient end color (#009296)."""
|
||||||
|
return color(text, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def color_mid(text: str) -> str:
|
||||||
|
"""Color text with gradient middle color."""
|
||||||
|
return color(text, 0.5)
|
||||||
|
|
||||||
|
|
||||||
|
def dim(text: str) -> str:
|
||||||
|
"""Dim/gray text."""
|
||||||
|
return f"\033[38;2;128;128;128m{text}\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def print_startup_info(host: str, port: int, database_url: str, llm_provider: str,
|
||||||
|
llm_model: str, embeddings_provider: str, reranker_provider: str,
|
||||||
|
mcp_enabled: bool = False):
|
||||||
|
"""Print styled startup information."""
|
||||||
|
print(color_start("Starting Hindsight API..."))
|
||||||
|
print(f" {dim('URL:')} {color(f'http://{host}:{port}', 0.2)}")
|
||||||
|
print(f" {dim('Database:')} {color(database_url, 0.4)}")
|
||||||
|
print(f" {dim('LLM:')} {color(f'{llm_provider} / {llm_model}', 0.6)}")
|
||||||
|
print(f" {dim('Embeddings:')} {color(embeddings_provider, 0.8)}")
|
||||||
|
print(f" {dim('Reranker:')} {color(reranker_provider, 1.0)}")
|
||||||
|
if mcp_enabled:
|
||||||
|
print(f" {dim('MCP:')} {color_end('enabled at /mcp')}")
|
||||||
|
print()
|
||||||
|
|
@ -32,8 +32,8 @@ ENV_MCP_ENABLED = "HINDSIGHT_API_MCP_ENABLED"
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
DEFAULT_DATABASE_URL = "pg0"
|
DEFAULT_DATABASE_URL = "pg0"
|
||||||
DEFAULT_LLM_PROVIDER = "groq"
|
DEFAULT_LLM_PROVIDER = "openai"
|
||||||
DEFAULT_LLM_MODEL = "openai/gpt-oss-20b"
|
DEFAULT_LLM_MODEL = "gpt-5-mini"
|
||||||
|
|
||||||
DEFAULT_EMBEDDINGS_PROVIDER = "local"
|
DEFAULT_EMBEDDINGS_PROVIDER = "local"
|
||||||
DEFAULT_EMBEDDINGS_LOCAL_MODEL = "BAAI/bge-small-en-v1.5"
|
DEFAULT_EMBEDDINGS_LOCAL_MODEL = "BAAI/bge-small-en-v1.5"
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,35 @@ class LLMProvider:
|
||||||
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url, max_retries=0)
|
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url, max_retries=0)
|
||||||
self._gemini_client = None
|
self._gemini_client = None
|
||||||
else:
|
else:
|
||||||
self._client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url, max_retries=0)
|
# Only pass base_url if it's set (OpenAI uses default URL otherwise)
|
||||||
|
client_kwargs = {"api_key": self.api_key, "max_retries": 0}
|
||||||
|
if self.base_url:
|
||||||
|
client_kwargs["base_url"] = self.base_url
|
||||||
|
self._client = AsyncOpenAI(**client_kwargs)
|
||||||
self._gemini_client = None
|
self._gemini_client = None
|
||||||
|
|
||||||
logger.info(
|
async def verify_connection(self) -> None:
|
||||||
f"Initialized LLM: provider={self.provider}, model={self.model}, base_url={self.base_url}"
|
"""
|
||||||
|
Verify that the LLM provider is configured correctly by making a simple test call.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the connection test fails.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"Verifying LLM: provider={self.provider}, model={self.model}, base_url={self.base_url or 'default'}...")
|
||||||
|
await self.call(
|
||||||
|
messages=[{"role": "user", "content": "Say 'ok'"}],
|
||||||
|
max_completion_tokens=10,
|
||||||
|
max_retries=2,
|
||||||
|
initial_backoff=0.5,
|
||||||
|
max_backoff=2.0,
|
||||||
)
|
)
|
||||||
|
# If we get here without exception, the connection is working
|
||||||
|
logger.info(f"LLM verified: {self.provider}/{self.model}")
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"LLM connection verification failed for {self.provider}/{self.model}: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
async def call(
|
async def call(
|
||||||
self,
|
self,
|
||||||
|
|
@ -149,7 +172,12 @@ class LLMProvider:
|
||||||
|
|
||||||
if max_completion_tokens is not None:
|
if max_completion_tokens is not None:
|
||||||
call_params["max_completion_tokens"] = max_completion_tokens
|
call_params["max_completion_tokens"] = max_completion_tokens
|
||||||
if temperature is not None:
|
# Check if model supports reasoning parameter (o1, o3, gpt-5 families)
|
||||||
|
model_lower = self.model.lower()
|
||||||
|
is_reasoning_model = any(x in model_lower for x in ["gpt-5", "o1", "o3"])
|
||||||
|
|
||||||
|
# GPT-5/o1/o3 family doesn't support custom temperature (only default 1)
|
||||||
|
if temperature is not None and not is_reasoning_model:
|
||||||
call_params["temperature"] = temperature
|
call_params["temperature"] = temperature
|
||||||
|
|
||||||
# Provider-specific parameters
|
# Provider-specific parameters
|
||||||
|
|
@ -216,7 +244,8 @@ class LLMProvider:
|
||||||
except APIConnectionError as e:
|
except APIConnectionError as e:
|
||||||
last_exception = e
|
last_exception = e
|
||||||
if attempt < max_retries:
|
if attempt < max_retries:
|
||||||
logger.warning(f"Connection error, retrying... (attempt {attempt + 1}/{max_retries + 1})")
|
status_code = getattr(e, 'status_code', None) or getattr(getattr(e, 'response', None), 'status_code', None)
|
||||||
|
logger.warning(f"Connection error, retrying... (attempt {attempt + 1}/{max_retries + 1}) - status_code={status_code}, message={e}")
|
||||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||||
await asyncio.sleep(backoff)
|
await asyncio.sleep(backoff)
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -453,12 +453,17 @@ class MemoryEngine:
|
||||||
# Query analyzer load is sync and CPU-bound
|
# Query analyzer load is sync and CPU-bound
|
||||||
await loop.run_in_executor(None, self.query_analyzer.load)
|
await loop.run_in_executor(None, self.query_analyzer.load)
|
||||||
|
|
||||||
|
async def verify_llm():
|
||||||
|
"""Verify LLM connection is working."""
|
||||||
|
await self._llm_config.verify_connection()
|
||||||
|
|
||||||
# Run pg0 and all model initializations in parallel
|
# Run pg0 and all model initializations in parallel
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
start_pg0(),
|
start_pg0(),
|
||||||
init_embeddings(),
|
init_embeddings(),
|
||||||
init_cross_encoder(),
|
init_cross_encoder(),
|
||||||
init_query_analyzer(),
|
init_query_analyzer(),
|
||||||
|
verify_llm(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run database migrations if enabled
|
# Run database migrations if enabled
|
||||||
|
|
@ -1791,10 +1796,14 @@ class MemoryEngine:
|
||||||
# Delete entities (cascades to unit_entities, entity_cooccurrences, memory_links with entity_id)
|
# Delete entities (cascades to unit_entities, entity_cooccurrences, memory_links with entity_id)
|
||||||
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
|
await conn.execute("DELETE FROM entities WHERE bank_id = $1", bank_id)
|
||||||
|
|
||||||
|
# Delete the bank profile itself
|
||||||
|
await conn.execute("DELETE FROM banks WHERE bank_id = $1", bank_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"memory_units_deleted": units_count,
|
"memory_units_deleted": units_count,
|
||||||
"entities_deleted": entities_count,
|
"entities_deleted": entities_count,
|
||||||
"documents_deleted": documents_count
|
"documents_deleted": documents_count,
|
||||||
|
"bank_deleted": True
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1839,10 +1848,11 @@ class MemoryEngine:
|
||||||
""", *query_params)
|
""", *query_params)
|
||||||
|
|
||||||
# Get links, filtering to only include links between units of the selected agent
|
# Get links, filtering to only include links between units of the selected agent
|
||||||
|
# Use DISTINCT ON with LEAST/GREATEST to deduplicate bidirectional links
|
||||||
unit_ids = [row['id'] for row in units]
|
unit_ids = [row['id'] for row in units]
|
||||||
if unit_ids:
|
if unit_ids:
|
||||||
links = await conn.fetch("""
|
links = await conn.fetch("""
|
||||||
SELECT
|
SELECT DISTINCT ON (LEAST(ml.from_unit_id, ml.to_unit_id), GREATEST(ml.from_unit_id, ml.to_unit_id), ml.link_type, COALESCE(ml.entity_id, '00000000-0000-0000-0000-000000000000'::uuid))
|
||||||
ml.from_unit_id,
|
ml.from_unit_id,
|
||||||
ml.to_unit_id,
|
ml.to_unit_id,
|
||||||
ml.link_type,
|
ml.link_type,
|
||||||
|
|
@ -1851,7 +1861,7 @@ class MemoryEngine:
|
||||||
FROM memory_links ml
|
FROM memory_links ml
|
||||||
LEFT JOIN entities e ON ml.entity_id = e.id
|
LEFT JOIN entities e ON ml.entity_id = e.id
|
||||||
WHERE ml.from_unit_id = ANY($1::uuid[]) AND ml.to_unit_id = ANY($1::uuid[])
|
WHERE ml.from_unit_id = ANY($1::uuid[]) AND ml.to_unit_id = ANY($1::uuid[])
|
||||||
ORDER BY ml.link_type, ml.weight DESC
|
ORDER BY LEAST(ml.from_unit_id, ml.to_unit_id), GREATEST(ml.from_unit_id, ml.to_unit_id), ml.link_type, COALESCE(ml.entity_id, '00000000-0000-0000-0000-000000000000'::uuid), ml.weight DESC
|
||||||
""", unit_ids)
|
""", unit_ids)
|
||||||
else:
|
else:
|
||||||
links = []
|
links = []
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,27 @@ async def create_temporal_links_batch_per_fact(
|
||||||
# Filter and create links in memory (much faster than N queries)
|
# Filter and create links in memory (much faster than N queries)
|
||||||
link_gen_start = time_mod.time()
|
link_gen_start = time_mod.time()
|
||||||
links = compute_temporal_links(new_units, all_candidates, time_window_hours)
|
links = compute_temporal_links(new_units, all_candidates, time_window_hours)
|
||||||
|
|
||||||
|
# Also compute temporal links WITHIN the new batch (new units to each other)
|
||||||
|
if len(new_units) > 1:
|
||||||
|
# Convert new_units dict to candidate format for within-batch linking
|
||||||
|
new_unit_items = list(new_units.items())
|
||||||
|
for i, (unit_id, event_date) in enumerate(new_unit_items):
|
||||||
|
unit_event_date_norm = _normalize_datetime(event_date)
|
||||||
|
|
||||||
|
# Compare with other new units (only those after this one to avoid duplicates)
|
||||||
|
for j in range(i + 1, len(new_unit_items)):
|
||||||
|
other_id, other_event_date = new_unit_items[j]
|
||||||
|
other_event_date_norm = _normalize_datetime(other_event_date)
|
||||||
|
|
||||||
|
# Check if within time window
|
||||||
|
time_diff_hours = abs((unit_event_date_norm - other_event_date_norm).total_seconds() / 3600)
|
||||||
|
if time_diff_hours <= time_window_hours:
|
||||||
|
weight = max(0.3, 1.0 - (time_diff_hours / time_window_hours))
|
||||||
|
# Create bidirectional links
|
||||||
|
links.append((unit_id, other_id, 'temporal', weight, None))
|
||||||
|
links.append((other_id, unit_id, 'temporal', weight, None))
|
||||||
|
|
||||||
_log(log_buffer, f" [7.3] Generate {len(links)} temporal links: {time_mod.time() - link_gen_start:.3f}s")
|
_log(log_buffer, f" [7.3] Generate {len(links)} temporal links: {time_mod.time() - link_gen_start:.3f}s")
|
||||||
|
|
||||||
if links:
|
if links:
|
||||||
|
|
@ -514,9 +535,38 @@ async def create_semantic_links_batch(
|
||||||
|
|
||||||
for idx in sorted_indices:
|
for idx in sorted_indices:
|
||||||
similar_id = existing_ids[idx]
|
similar_id = existing_ids[idx]
|
||||||
similarity = float(similarities[idx])
|
# Clamp to [0, 1] to handle floating point precision issues
|
||||||
|
similarity = float(min(1.0, max(0.0, similarities[idx])))
|
||||||
all_links.append((unit_id, similar_id, 'semantic', similarity, None))
|
all_links.append((unit_id, similar_id, 'semantic', similarity, None))
|
||||||
|
|
||||||
|
# Also compute similarities WITHIN the new batch (new units to each other)
|
||||||
|
# Apply the same top_k limit per unit as we do for existing units
|
||||||
|
if len(unit_ids) > 1:
|
||||||
|
new_embeddings_matrix = np.array(embeddings)
|
||||||
|
|
||||||
|
for i, unit_id in enumerate(unit_ids):
|
||||||
|
# Compute similarities with all OTHER new units
|
||||||
|
other_indices = [j for j in range(len(unit_ids)) if j != i]
|
||||||
|
if not other_indices:
|
||||||
|
continue
|
||||||
|
|
||||||
|
other_embeddings = new_embeddings_matrix[other_indices]
|
||||||
|
similarities = np.dot(other_embeddings, new_embeddings_matrix[i])
|
||||||
|
|
||||||
|
# Find top-k above threshold (same logic as existing units)
|
||||||
|
above_threshold = np.where(similarities >= threshold)[0]
|
||||||
|
|
||||||
|
if len(above_threshold) > 0:
|
||||||
|
# Sort by similarity (descending) and take top-k
|
||||||
|
sorted_local_indices = above_threshold[np.argsort(-similarities[above_threshold])][:top_k]
|
||||||
|
|
||||||
|
for local_idx in sorted_local_indices:
|
||||||
|
other_idx = other_indices[local_idx]
|
||||||
|
other_id = unit_ids[other_idx]
|
||||||
|
# Clamp to [0, 1] to handle floating point precision issues
|
||||||
|
similarity = float(min(1.0, max(0.0, similarities[local_idx])))
|
||||||
|
all_links.append((unit_id, other_id, 'semantic', similarity, None))
|
||||||
|
|
||||||
_log(log_buffer, f" [8.2] Compute similarities & generate {len(all_links)} semantic links: {time_mod.time() - compute_start:.3f}s")
|
_log(log_buffer, f" [8.2] Compute similarities & generate {len(all_links)} semantic links: {time_mod.time() - compute_start:.3f}s")
|
||||||
|
|
||||||
if all_links:
|
if all_links:
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ from . import MemoryEngine
|
||||||
from .api import create_app
|
from .api import create_app
|
||||||
from .config import get_config, HindsightConfig
|
from .config import get_config, HindsightConfig
|
||||||
|
|
||||||
|
from .banner import print_banner
|
||||||
|
print()
|
||||||
|
print_banner()
|
||||||
|
|
||||||
# Filter deprecation warnings from third-party libraries
|
# Filter deprecation warnings from third-party libraries
|
||||||
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
|
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
|
||||||
warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated")
|
warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated")
|
||||||
|
|
@ -184,15 +188,19 @@ def main():
|
||||||
if args.ssl_certfile:
|
if args.ssl_certfile:
|
||||||
uvicorn_config["ssl_certfile"] = args.ssl_certfile
|
uvicorn_config["ssl_certfile"] = args.ssl_certfile
|
||||||
|
|
||||||
print(f"\nStarting Hindsight API...")
|
|
||||||
print(f" URL: http://{args.host}:{args.port}")
|
|
||||||
print(f" Database: {config.database_url}")
|
from .banner import print_startup_info
|
||||||
print(f" LLM: {config.llm_provider} / {config.llm_model}")
|
print_startup_info(
|
||||||
print(f" Embeddings: {config.embeddings_provider}")
|
host=args.host,
|
||||||
print(f" Reranker: {config.reranker_provider}")
|
port=args.port,
|
||||||
if config.mcp_enabled:
|
database_url=config.database_url,
|
||||||
print(f" MCP: enabled at /mcp")
|
llm_provider=config.llm_provider,
|
||||||
print()
|
llm_model=config.llm_model,
|
||||||
|
embeddings_provider=config.embeddings_provider,
|
||||||
|
reranker_provider=config.reranker_provider,
|
||||||
|
mcp_enabled=config.mcp_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
uvicorn.run(**uvicorn_config)
|
uvicorn.run(**uvicorn_config)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -257,16 +257,17 @@ class EmbeddedPostgres:
|
||||||
last_error = stderr or f"pg0 start returned exit code {returncode}"
|
last_error = stderr or f"pg0 start returned exit code {returncode}"
|
||||||
if attempt < max_retries:
|
if attempt < max_retries:
|
||||||
delay = retry_delay * (2 ** (attempt - 1))
|
delay = retry_delay * (2 ** (attempt - 1))
|
||||||
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||||
logger.info(f"Retrying in {delay:.1f}s...")
|
logger.debug(f"Retrying in {delay:.1f}s...")
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||||
|
|
||||||
# All retries exhausted - use constructed URI as fallback
|
# All retries exhausted - fail
|
||||||
uri = f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
|
raise RuntimeError(
|
||||||
logger.warning(f"All pg0 start attempts failed, using constructed URI: {uri}")
|
f"Failed to start embedded PostgreSQL after {max_retries} attempts. "
|
||||||
return uri
|
f"Last error: {last_error.strip() if last_error else 'unknown'}"
|
||||||
|
)
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Stop the PostgreSQL server."""
|
"""Stop the PostgreSQL server."""
|
||||||
|
|
|
||||||
131
hindsight-api/tests/test_llm_provider.py
Normal file
131
hindsight-api/tests/test_llm_provider.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
"""
|
||||||
|
Test LLM provider with different models and providers.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from hindsight_api.engine.llm_wrapper import LLMProvider
|
||||||
|
|
||||||
|
|
||||||
|
# Model matrix: (provider, model)
|
||||||
|
MODEL_MATRIX = [
|
||||||
|
# OpenAI models
|
||||||
|
("openai", "gpt-4o-mini"),
|
||||||
|
("openai", "gpt-5-mini"),
|
||||||
|
# Groq models
|
||||||
|
("groq", "llama-3.3-70b-versatile"),
|
||||||
|
("groq", "openai/gpt-oss-120b"),
|
||||||
|
# Gemini models
|
||||||
|
("gemini", "gemini-2.0-flash"),
|
||||||
|
("gemini", "gemini-2.5-flash-preview-05-20"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_key_for_provider(provider: str) -> str | None:
|
||||||
|
"""Get API key for provider from environment variables."""
|
||||||
|
# Try provider-specific env vars first
|
||||||
|
provider_key_map = {
|
||||||
|
"openai": ["OPENAI_API_KEY", "HINDSIGHT_API_LLM_API_KEY"],
|
||||||
|
"groq": ["GROQ_API_KEY", "HINDSIGHT_API_LLM_API_KEY"],
|
||||||
|
"gemini": ["GEMINI_API_KEY", "GOOGLE_API_KEY", "HINDSIGHT_API_LLM_API_KEY"],
|
||||||
|
}
|
||||||
|
|
||||||
|
for env_var in provider_key_map.get(provider, []):
|
||||||
|
key = os.getenv(env_var)
|
||||||
|
if key:
|
||||||
|
# For HINDSIGHT_API_LLM_API_KEY, only use if provider matches
|
||||||
|
if env_var == "HINDSIGHT_API_LLM_API_KEY":
|
||||||
|
configured_provider = os.getenv("HINDSIGHT_API_LLM_PROVIDER", "").lower()
|
||||||
|
if configured_provider == provider:
|
||||||
|
return key
|
||||||
|
else:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("provider,model", MODEL_MATRIX)
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_llm_provider_call(provider: str, model: str):
|
||||||
|
"""
|
||||||
|
Test LLM provider can make a basic call with different models.
|
||||||
|
Skips if the required API key is not available.
|
||||||
|
"""
|
||||||
|
api_key = get_api_key_for_provider(provider)
|
||||||
|
if not api_key:
|
||||||
|
pytest.skip(f"Skipping {provider}/{model}: no API key available")
|
||||||
|
|
||||||
|
llm = LLMProvider(
|
||||||
|
provider=provider,
|
||||||
|
api_key=api_key,
|
||||||
|
base_url="",
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test basic call
|
||||||
|
response = await llm.call(
|
||||||
|
messages=[{"role": "user", "content": "Say 'hello' and nothing else."}],
|
||||||
|
max_completion_tokens=50,
|
||||||
|
temperature=0.1,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n{provider}/{model} response: {response}")
|
||||||
|
assert response is not None, f"{provider}/{model} returned None"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("provider,model", MODEL_MATRIX)
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_llm_provider_verify_connection(provider: str, model: str):
|
||||||
|
"""
|
||||||
|
Test LLM provider verify_connection method with different models.
|
||||||
|
Skips if the required API key is not available.
|
||||||
|
"""
|
||||||
|
api_key = get_api_key_for_provider(provider)
|
||||||
|
if not api_key:
|
||||||
|
pytest.skip(f"Skipping {provider}/{model}: no API key available")
|
||||||
|
|
||||||
|
llm = LLMProvider(
|
||||||
|
provider=provider,
|
||||||
|
api_key=api_key,
|
||||||
|
base_url="",
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test verify_connection
|
||||||
|
await llm.verify_connection()
|
||||||
|
print(f"\n{provider}/{model} connection verified")
|
||||||
|
|
||||||
|
|
||||||
|
# Models that support large output (65000+ tokens)
|
||||||
|
LARGE_OUTPUT_MODELS = [
|
||||||
|
("openai", "gpt-5-mini"),
|
||||||
|
("gemini", "gemini-2.0-flash"),
|
||||||
|
("gemini", "gemini-2.5-flash-preview-05-20"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("provider,model", LARGE_OUTPUT_MODELS)
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_llm_provider_large_output(provider: str, model: str):
|
||||||
|
"""
|
||||||
|
Test LLM provider with large max_completion_tokens (65000).
|
||||||
|
Only tests models that support large outputs.
|
||||||
|
Skips if the required API key is not available.
|
||||||
|
"""
|
||||||
|
api_key = get_api_key_for_provider(provider)
|
||||||
|
if not api_key:
|
||||||
|
pytest.skip(f"Skipping {provider}/{model}: no API key available")
|
||||||
|
|
||||||
|
llm = LLMProvider(
|
||||||
|
provider=provider,
|
||||||
|
api_key=api_key,
|
||||||
|
base_url="",
|
||||||
|
model=model,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test call with large max_completion_tokens
|
||||||
|
response = await llm.call(
|
||||||
|
messages=[{"role": "user", "content": "Say 'ok'"}],
|
||||||
|
max_completion_tokens=65000,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n{provider}/{model} large output response: {response}")
|
||||||
|
assert response is not None, f"{provider}/{model} returned None"
|
||||||
|
|
@ -3,7 +3,7 @@ Test retain function and chunk storage.
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone, timedelta
|
||||||
from hindsight_api.engine.memory_engine import Budget
|
from hindsight_api.engine.memory_engine import Budget
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -1595,3 +1595,133 @@ async def test_all_link_types_together(memory):
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await memory.delete_bank(bank_id)
|
await memory.delete_bank(bank_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_semantic_links_within_same_batch(memory):
|
||||||
|
"""
|
||||||
|
Test that semantic links are created between facts retained in the SAME batch.
|
||||||
|
|
||||||
|
This is a regression test - semantic links should connect similar facts
|
||||||
|
even when they are retained together in a single call.
|
||||||
|
"""
|
||||||
|
bank_id = f"test_semantic_batch_{datetime.now(timezone.utc).timestamp()}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Retain multiple semantically similar facts in ONE batch
|
||||||
|
contents = [
|
||||||
|
{"content": "Alice is an expert in Python programming and machine learning.", "context": "team skills"},
|
||||||
|
{"content": "Bob specializes in Python development and data science.", "context": "team skills"},
|
||||||
|
{"content": "Charlie works with Python for backend API development.", "context": "team skills"},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = await memory.retain_batch_async(
|
||||||
|
bank_id=bank_id,
|
||||||
|
contents=contents
|
||||||
|
)
|
||||||
|
|
||||||
|
# Flatten the list of lists
|
||||||
|
unit_ids = [uid for sublist in result for uid in sublist]
|
||||||
|
|
||||||
|
assert len(unit_ids) >= 3, f"Should have created at least 3 facts, got {len(unit_ids)}"
|
||||||
|
logger.info(f"Created {len(unit_ids)} facts in single batch")
|
||||||
|
|
||||||
|
# Query semantic links between these units
|
||||||
|
async with memory._pool.acquire() as conn:
|
||||||
|
semantic_links = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT from_unit_id, to_unit_id, weight
|
||||||
|
FROM memory_links
|
||||||
|
WHERE from_unit_id::text = ANY($1)
|
||||||
|
AND to_unit_id::text = ANY($1)
|
||||||
|
AND link_type = 'semantic'
|
||||||
|
""",
|
||||||
|
unit_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Found {len(semantic_links)} semantic links within the batch")
|
||||||
|
|
||||||
|
# All three facts mention Python - they should be linked to each other
|
||||||
|
assert len(semantic_links) > 0, (
|
||||||
|
"REGRESSION: Semantic links should be created between similar facts "
|
||||||
|
"retained in the same batch, but none were found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log the links for debugging
|
||||||
|
for link in semantic_links:
|
||||||
|
logger.info(f" Semantic link: {str(link['from_unit_id'])[:8]}... -> {str(link['to_unit_id'])[:8]}... (weight: {link['weight']:.3f})")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await memory.delete_bank(bank_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_temporal_links_within_same_batch(memory):
|
||||||
|
"""
|
||||||
|
Test that temporal links are created between facts retained in the SAME batch.
|
||||||
|
|
||||||
|
This is a regression test - temporal links should connect facts with nearby
|
||||||
|
event dates even when they are retained together in a single call.
|
||||||
|
"""
|
||||||
|
bank_id = f"test_temporal_batch_{datetime.now(timezone.utc).timestamp()}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Retain multiple facts with nearby timestamps in ONE batch
|
||||||
|
base_date = datetime(2024, 6, 15, 10, 0, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
contents = [
|
||||||
|
{
|
||||||
|
"content": "Morning standup: Alice presented the sprint goals.",
|
||||||
|
"context": "daily meeting",
|
||||||
|
"event_date": base_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Bob demoed the new feature after standup.",
|
||||||
|
"context": "daily meeting",
|
||||||
|
"event_date": base_date + timedelta(hours=1) # 1 hour later
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Charlie reviewed the pull requests in the afternoon.",
|
||||||
|
"context": "daily meeting",
|
||||||
|
"event_date": base_date + timedelta(hours=4) # 4 hours later
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = await memory.retain_batch_async(
|
||||||
|
bank_id=bank_id,
|
||||||
|
contents=contents
|
||||||
|
)
|
||||||
|
|
||||||
|
# Flatten the list of lists
|
||||||
|
unit_ids = [uid for sublist in result for uid in sublist]
|
||||||
|
|
||||||
|
assert len(unit_ids) >= 3, f"Should have created at least 3 facts, got {len(unit_ids)}"
|
||||||
|
logger.info(f"Created {len(unit_ids)} facts in single batch")
|
||||||
|
|
||||||
|
# Query temporal links between these units
|
||||||
|
async with memory._pool.acquire() as conn:
|
||||||
|
temporal_links = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT from_unit_id, to_unit_id, weight
|
||||||
|
FROM memory_links
|
||||||
|
WHERE from_unit_id::text = ANY($1)
|
||||||
|
AND to_unit_id::text = ANY($1)
|
||||||
|
AND link_type = 'temporal'
|
||||||
|
""",
|
||||||
|
unit_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Found {len(temporal_links)} temporal links within the batch")
|
||||||
|
|
||||||
|
# All three facts are within 24 hours - they should be linked to each other
|
||||||
|
assert len(temporal_links) > 0, (
|
||||||
|
"REGRESSION: Temporal links should be created between facts with nearby dates "
|
||||||
|
"retained in the same batch, but none were found"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log the links for debugging
|
||||||
|
for link in temporal_links:
|
||||||
|
logger.info(f" Temporal link: {str(link['from_unit_id'])[:8]}... -> {str(link['to_unit_id'])[:8]}... (weight: {link['weight']:.3f})")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await memory.delete_bank(bank_id)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ clap = { version = "4.5", features = ["derive", "env"] }
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
|
# HTTP client (for timeout configuration)
|
||||||
|
reqwest = "0.12"
|
||||||
|
|
||||||
# Serialization (for config and output formatting)
|
# Serialization (for config and output formatting)
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use std::collections::HashMap;
|
||||||
// Types not defined in OpenAPI spec (TODO: add to openapi.json)
|
// Types not defined in OpenAPI spec (TODO: add to openapi.json)
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct AgentStats {
|
pub struct AgentStats {
|
||||||
pub agent_id: String,
|
pub bank_id: String,
|
||||||
pub total_nodes: i32,
|
pub total_nodes: i32,
|
||||||
pub total_links: i32,
|
pub total_links: i32,
|
||||||
pub total_documents: i32,
|
pub total_documents: i32,
|
||||||
|
|
@ -38,7 +38,7 @@ pub struct Operation {
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct OperationsResponse {
|
pub struct OperationsResponse {
|
||||||
pub agent_id: String,
|
pub bank_id: String,
|
||||||
pub operations: Vec<Operation>,
|
pub operations: Vec<Operation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +66,13 @@ pub struct ApiClient {
|
||||||
impl ApiClient {
|
impl ApiClient {
|
||||||
pub fn new(base_url: String) -> Result<Self> {
|
pub fn new(base_url: String) -> Result<Self> {
|
||||||
let runtime = std::sync::Arc::new(tokio::runtime::Runtime::new()?);
|
let runtime = std::sync::Arc::new(tokio::runtime::Runtime::new()?);
|
||||||
let client = AsyncClient::new(&base_url);
|
|
||||||
|
// Create HTTP client with 2-minute timeout
|
||||||
|
let http_client = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(120))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let client = AsyncClient::new_with_client(&base_url, http_client);
|
||||||
Ok(ApiClient { client, runtime })
|
Ok(ApiClient { client, runtime })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,6 +237,13 @@ impl ApiClient {
|
||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_bank(&self, bank_id: &str, _verbose: bool) -> Result<types::DeleteResponse> {
|
||||||
|
self.runtime.block_on(async {
|
||||||
|
let response = self.client.delete_bank(bank_id).await?;
|
||||||
|
Ok(response.into_inner())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export types from the generated client for use in commands
|
// Re-export types from the generated client for use in commands
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ pub fn list(client: &ApiClient, verbose: bool, output_format: OutputFormat) -> R
|
||||||
|
|
||||||
let response = client.list_agents(verbose);
|
let response = client.list_agents(verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -36,23 +36,23 @@ pub fn list(client: &ApiClient, verbose: bool, output_format: OutputFormat) -> R
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profile(client: &ApiClient, bank_id: &str, verbose: bool, output_format: OutputFormat) -> Result<()> {
|
pub fn disposition(client: &ApiClient, bank_id: &str, verbose: bool, output_format: OutputFormat) -> Result<()> {
|
||||||
let spinner = if output_format == OutputFormat::Pretty {
|
let spinner = if output_format == OutputFormat::Pretty {
|
||||||
Some(ui::create_spinner("Fetching profile..."))
|
Some(ui::create_spinner("Fetching disposition..."))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = client.get_profile(bank_id, verbose);
|
let response = client.get_profile(bank_id, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(profile) => {
|
Ok(profile) => {
|
||||||
if output_format == OutputFormat::Pretty {
|
if output_format == OutputFormat::Pretty {
|
||||||
ui::print_profile(&profile);
|
ui::print_disposition(&profile);
|
||||||
} else {
|
} else {
|
||||||
output::print_output(&profile, output_format)?;
|
output::print_output(&profile, output_format)?;
|
||||||
}
|
}
|
||||||
|
|
@ -71,92 +71,69 @@ pub fn stats(client: &ApiClient, bank_id: &str, verbose: bool, output_format: Ou
|
||||||
|
|
||||||
let response = client.get_stats(bank_id, verbose);
|
let response = client.get_stats(bank_id, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(stats) => {
|
Ok(stats) => {
|
||||||
if output_format == OutputFormat::Pretty {
|
if output_format == OutputFormat::Pretty {
|
||||||
ui::print_info(&format!("Statistics for bank '{}'", bank_id));
|
ui::print_section_header(&format!("Statistics: {}", bank_id));
|
||||||
|
|
||||||
|
println!(" {} {}", ui::dim("memory units:"), ui::gradient_start(&stats.total_nodes.to_string()));
|
||||||
|
println!(" {} {}", ui::dim("links:"), ui::gradient_mid(&stats.total_links.to_string()));
|
||||||
|
println!(" {} {}", ui::dim("documents:"), ui::gradient_end(&stats.total_documents.to_string()));
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
println!(" 📊 Overview");
|
println!("{}", ui::gradient_text("─── Memory Units by Type ───"));
|
||||||
println!(" Total Memory Units: {}", stats.total_nodes);
|
|
||||||
println!(" Total Links: {}", stats.total_links);
|
|
||||||
println!(" Total Documents: {}", stats.total_documents);
|
|
||||||
println!();
|
|
||||||
|
|
||||||
println!(" 🧠 Memory Units by Type");
|
|
||||||
let mut fact_types: Vec<_> = stats.nodes_by_fact_type.iter().collect();
|
let mut fact_types: Vec<_> = stats.nodes_by_fact_type.iter().collect();
|
||||||
fact_types.sort_by_key(|(k, _)| *k);
|
fact_types.sort_by_key(|(k, _)| *k);
|
||||||
for (fact_type, count) in fact_types {
|
for (i, (fact_type, count)) in fact_types.iter().enumerate() {
|
||||||
let icon = match fact_type.as_str() {
|
let t = i as f32 / fact_types.len().max(1) as f32;
|
||||||
"world" => "🌍",
|
println!(" {:<10} {}", fact_type, ui::gradient(&count.to_string(), t));
|
||||||
"agent" => "🤖",
|
|
||||||
"opinion" => "💭",
|
|
||||||
_ => "•"
|
|
||||||
};
|
|
||||||
println!(" {} {:<10} {}", icon, fact_type, count);
|
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
println!(" 🔗 Links by Type");
|
println!("{}", ui::gradient_text("─── Links by Type ───"));
|
||||||
let mut link_types: Vec<_> = stats.links_by_link_type.iter().collect();
|
let mut link_types: Vec<_> = stats.links_by_link_type.iter().collect();
|
||||||
link_types.sort_by_key(|(k, _)| *k);
|
link_types.sort_by_key(|(k, _)| *k);
|
||||||
for (link_type, count) in link_types {
|
for (i, (link_type, count)) in link_types.iter().enumerate() {
|
||||||
let icon = match link_type.as_str() {
|
let t = i as f32 / link_types.len().max(1) as f32;
|
||||||
"temporal" => "⏰",
|
println!(" {:<10} {}", link_type, ui::gradient(&count.to_string(), t));
|
||||||
"semantic" => "🔤",
|
|
||||||
"entity" => "🏷️",
|
|
||||||
_ => "•"
|
|
||||||
};
|
|
||||||
println!(" {} {:<10} {}", icon, link_type, count);
|
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
println!(" 🔗 Links by Fact Type");
|
println!("{}", ui::gradient_text("─── Links by Fact Type ───"));
|
||||||
let mut fact_type_links: Vec<_> = stats.links_by_fact_type.iter().collect();
|
let mut fact_type_links: Vec<_> = stats.links_by_fact_type.iter().collect();
|
||||||
fact_type_links.sort_by_key(|(k, _)| *k);
|
fact_type_links.sort_by_key(|(k, _)| *k);
|
||||||
for (fact_type, count) in fact_type_links {
|
for (i, (fact_type, count)) in fact_type_links.iter().enumerate() {
|
||||||
let icon = match fact_type.as_str() {
|
let t = i as f32 / fact_type_links.len().max(1) as f32;
|
||||||
"world" => "🌍",
|
println!(" {:<10} {}", fact_type, ui::gradient(&count.to_string(), t));
|
||||||
"agent" => "🤖",
|
|
||||||
"opinion" => "💭",
|
|
||||||
_ => "•"
|
|
||||||
};
|
|
||||||
println!(" {} {:<10} {}", icon, fact_type, count);
|
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
if !stats.links_breakdown.is_empty() {
|
if !stats.links_breakdown.is_empty() {
|
||||||
println!(" 📈 Detailed Link Breakdown");
|
println!("{}", ui::gradient_text("─── Detailed Link Breakdown ───"));
|
||||||
let mut fact_types: Vec<_> = stats.links_breakdown.iter().collect();
|
let mut fact_types: Vec<_> = stats.links_breakdown.iter().collect();
|
||||||
fact_types.sort_by_key(|(k, _)| *k);
|
fact_types.sort_by_key(|(k, _)| *k);
|
||||||
for (fact_type, link_types) in fact_types {
|
for (fact_type, link_types) in fact_types {
|
||||||
let icon = match fact_type.as_str() {
|
println!(" {}", fact_type);
|
||||||
"world" => "🌍",
|
|
||||||
"agent" => "🤖",
|
|
||||||
"opinion" => "💭",
|
|
||||||
_ => "•"
|
|
||||||
};
|
|
||||||
println!(" {} {}", icon, fact_type);
|
|
||||||
let mut sorted_links: Vec<_> = link_types.iter().collect();
|
let mut sorted_links: Vec<_> = link_types.iter().collect();
|
||||||
sorted_links.sort_by_key(|(k, _)| *k);
|
sorted_links.sort_by_key(|(k, _)| *k);
|
||||||
for (link_type, count) in sorted_links {
|
for (link_type, count) in sorted_links {
|
||||||
println!(" - {:<10} {}", link_type, count);
|
println!(" {:<10} {}", ui::dim(link_type), count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
if stats.pending_operations > 0 || stats.failed_operations > 0 {
|
if stats.pending_operations > 0 || stats.failed_operations > 0 {
|
||||||
println!(" ⚙️ Operations");
|
println!("{}", ui::gradient_text("─── Operations ───"));
|
||||||
if stats.pending_operations > 0 {
|
if stats.pending_operations > 0 {
|
||||||
println!(" ⏳ Pending: {}", stats.pending_operations);
|
println!(" {} {}", ui::dim("pending:"), stats.pending_operations);
|
||||||
}
|
}
|
||||||
if stats.failed_operations > 0 {
|
if stats.failed_operations > 0 {
|
||||||
println!(" ❌ Failed: {}", stats.failed_operations);
|
println!(" {} {}", ui::dim("failed:"), stats.failed_operations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -177,8 +154,8 @@ pub fn update_name(client: &ApiClient, bank_id: &str, name: &str, verbose: bool,
|
||||||
|
|
||||||
let response = client.update_agent_name(bank_id, name, verbose);
|
let response = client.update_agent_name(bank_id, name, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -216,8 +193,8 @@ pub fn update_background(
|
||||||
|
|
||||||
let response = client.add_background(bank_id, content, !no_update_disposition, verbose);
|
let response = client.add_background(bank_id, content, !no_update_disposition, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -244,3 +221,57 @@ pub fn update_background(
|
||||||
Err(e) => Err(e)
|
Err(e) => Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete(
|
||||||
|
client: &ApiClient,
|
||||||
|
bank_id: &str,
|
||||||
|
yes: bool,
|
||||||
|
verbose: bool,
|
||||||
|
output_format: OutputFormat
|
||||||
|
) -> Result<()> {
|
||||||
|
// Confirmation prompt unless -y flag is used
|
||||||
|
if !yes && output_format == OutputFormat::Pretty {
|
||||||
|
let message = format!(
|
||||||
|
"Are you sure you want to delete bank '{}' and ALL its data? This cannot be undone.",
|
||||||
|
bank_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let confirmed = ui::prompt_confirmation(&message)?;
|
||||||
|
|
||||||
|
if !confirmed {
|
||||||
|
ui::print_info("Operation cancelled");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let spinner = if output_format == OutputFormat::Pretty {
|
||||||
|
Some(ui::create_spinner("Deleting bank..."))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client.delete_bank(bank_id, verbose);
|
||||||
|
|
||||||
|
if let Some(mut sp) = spinner {
|
||||||
|
sp.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Ok(result) => {
|
||||||
|
if output_format == OutputFormat::Pretty {
|
||||||
|
if result.success {
|
||||||
|
ui::print_success(&format!("Bank '{}' deleted successfully", bank_id));
|
||||||
|
if let Some(count) = result.deleted_count {
|
||||||
|
println!(" Items deleted: {}", count);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui::print_error("Failed to delete bank");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output::print_output(&result, output_format)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,14 @@ pub fn list(
|
||||||
|
|
||||||
let response = client.list_documents(agent_id, query.as_deref(), Some(limit), Some(offset), verbose);
|
let response = client.list_documents(agent_id, query.as_deref(), Some(limit), Some(offset), verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(docs_response) => {
|
Ok(docs_response) => {
|
||||||
if output_format == OutputFormat::Pretty {
|
if output_format == OutputFormat::Pretty {
|
||||||
ui::print_info(&format!("Documents for agent '{}' (total: {})", agent_id, docs_response.total));
|
ui::print_info(&format!("Documents for bank '{}' (total: {})", agent_id, docs_response.total));
|
||||||
for doc in &docs_response.items {
|
for doc in &docs_response.items {
|
||||||
let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("unknown");
|
let id = doc.get("id").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
let created = doc.get("created_at").and_then(|v| v.as_str()).unwrap_or("unknown");
|
let created = doc.get("created_at").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
|
|
@ -65,8 +65,8 @@ pub fn get(
|
||||||
|
|
||||||
let response = client.get_document(agent_id, document_id, verbose);
|
let response = client.get_document(agent_id, document_id, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -102,8 +102,8 @@ pub fn delete(
|
||||||
|
|
||||||
let response = client.delete_document(agent_id, document_id, verbose);
|
let response = client.delete_document(agent_id, document_id, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ pub fn list(
|
||||||
|
|
||||||
let response = client.list_entities(bank_id, Some(limit), verbose)?;
|
let response = client.list_entities(bank_id, Some(limit), verbose)?;
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
if output_format == OutputFormat::Pretty {
|
if output_format == OutputFormat::Pretty {
|
||||||
|
|
@ -66,8 +66,8 @@ pub fn get(
|
||||||
|
|
||||||
let response = client.get_entity(bank_id, entity_id, verbose)?;
|
let response = client.get_entity(bank_id, entity_id, verbose)?;
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
if output_format == OutputFormat::Pretty {
|
if output_format == OutputFormat::Pretty {
|
||||||
|
|
@ -84,17 +84,6 @@ pub fn get(
|
||||||
println!("Last seen: {}", last_seen);
|
println!("Last seen: {}", last_seen);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show observations (always included)
|
|
||||||
if !response.observations.is_empty() {
|
|
||||||
println!("\nObservations ({}):", response.observations.len());
|
|
||||||
for obs in &response.observations {
|
|
||||||
println!(" - {}", obs.text);
|
|
||||||
if let Some(mentioned_at) = &obs.mentioned_at {
|
|
||||||
println!(" Mentioned at: {}", mentioned_at);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
} else {
|
} else {
|
||||||
output::print_output(&response, output_format)?;
|
output::print_output(&response, output_format)?;
|
||||||
|
|
@ -118,8 +107,8 @@ pub fn regenerate(
|
||||||
|
|
||||||
let response = client.regenerate_entity(bank_id, entity_id, verbose)?;
|
let response = client.regenerate_entity(bank_id, entity_id, verbose)?;
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
if output_format == OutputFormat::Pretty {
|
if output_format == OutputFormat::Pretty {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,15 @@ use ratatui::{
|
||||||
Frame, Terminal,
|
Frame, Terminal,
|
||||||
};
|
};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::sync::mpsc::{self, Receiver, TryRecvError};
|
||||||
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
// Brand gradient colors: #0074d9 -> #009296
|
||||||
|
const BRAND_START: Color = Color::Rgb(0, 116, 217); // #0074d9
|
||||||
|
const BRAND_END: Color = Color::Rgb(0, 146, 150); // #009296
|
||||||
|
const BRAND_MID: Color = Color::Rgb(0, 131, 183); // Midpoint
|
||||||
|
|
||||||
/// Main view types (like k9s contexts)
|
/// Main view types (like k9s contexts)
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
enum View {
|
enum View {
|
||||||
|
|
@ -61,6 +68,12 @@ enum InputMode {
|
||||||
Query,
|
Query,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Query result from background thread
|
||||||
|
enum QueryResult {
|
||||||
|
Recall(Result<Vec<RecallResult>, String>),
|
||||||
|
Reflect(Result<String, String>),
|
||||||
|
}
|
||||||
|
|
||||||
/// Application state
|
/// Application state
|
||||||
struct App {
|
struct App {
|
||||||
client: ApiClient,
|
client: ApiClient,
|
||||||
|
|
@ -114,6 +127,9 @@ struct App {
|
||||||
auto_refresh_enabled: bool,
|
auto_refresh_enabled: bool,
|
||||||
last_refresh: Instant,
|
last_refresh: Instant,
|
||||||
refresh_interval: Duration,
|
refresh_interval: Duration,
|
||||||
|
|
||||||
|
// Background query receiver
|
||||||
|
query_receiver: Option<Receiver<QueryResult>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
@ -160,6 +176,8 @@ impl App {
|
||||||
auto_refresh_enabled: true,
|
auto_refresh_enabled: true,
|
||||||
last_refresh: Instant::now(),
|
last_refresh: Instant::now(),
|
||||||
refresh_interval: Duration::from_secs(5),
|
refresh_interval: Duration::from_secs(5),
|
||||||
|
|
||||||
|
query_receiver: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Select first item by default
|
// Select first item by default
|
||||||
|
|
@ -288,58 +306,106 @@ impl App {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_query(&mut self) -> Result<()> {
|
fn execute_query(&mut self) {
|
||||||
if let View::Query(bank_id) = &self.view {
|
if let View::Query(bank_id) = &self.view {
|
||||||
if self.query_text.is_empty() {
|
if self.query_text.is_empty() {
|
||||||
self.error_message = "Query cannot be empty".to_string();
|
self.error_message = "Query cannot be empty".to_string();
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.loading = true;
|
self.loading = true;
|
||||||
self.error_message.clear();
|
self.error_message.clear();
|
||||||
|
self.input_mode = InputMode::Normal;
|
||||||
|
|
||||||
match self.query_mode {
|
// Create channel for receiving results
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
self.query_receiver = Some(rx);
|
||||||
|
|
||||||
|
// Clone data for the thread
|
||||||
|
let client = self.client.clone();
|
||||||
|
let bank_id = bank_id.clone();
|
||||||
|
let query_mode = self.query_mode.clone();
|
||||||
|
let query_text = self.query_text.clone();
|
||||||
|
let query_budget = self.query_budget.clone();
|
||||||
|
let query_max_tokens = self.query_max_tokens;
|
||||||
|
|
||||||
|
// Spawn background thread
|
||||||
|
thread::spawn(move || {
|
||||||
|
match query_mode {
|
||||||
QueryMode::Recall => {
|
QueryMode::Recall => {
|
||||||
let request = RecallRequest {
|
let request = RecallRequest {
|
||||||
query: self.query_text.clone(),
|
query: query_text,
|
||||||
types: None,
|
types: None,
|
||||||
budget: Some(self.query_budget.clone()),
|
budget: Some(query_budget),
|
||||||
max_tokens: self.query_max_tokens,
|
max_tokens: query_max_tokens,
|
||||||
trace: false,
|
trace: false,
|
||||||
query_timestamp: None,
|
query_timestamp: None,
|
||||||
include: None,
|
include: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = self.client.recall(bank_id, &request, false)?;
|
let result = client.recall(&bank_id, &request, false)
|
||||||
self.query_results = response.results;
|
.map(|r| r.results)
|
||||||
|
.map_err(|e| e.to_string());
|
||||||
|
|
||||||
if !self.query_results.is_empty() {
|
let _ = tx.send(QueryResult::Recall(result));
|
||||||
self.query_results_state.select(Some(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.loading = false;
|
|
||||||
self.status_message = format!("Found {} results", self.query_results.len());
|
|
||||||
}
|
}
|
||||||
QueryMode::Reflect => {
|
QueryMode::Reflect => {
|
||||||
let request = ReflectRequest {
|
let request = ReflectRequest {
|
||||||
query: self.query_text.clone(),
|
query: query_text,
|
||||||
budget: Some(self.query_budget.clone()),
|
budget: Some(query_budget),
|
||||||
context: None,
|
context: None,
|
||||||
include: None,
|
include: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = self.client.reflect(bank_id, &request, false)?;
|
let result = client.reflect(&bank_id, &request, false)
|
||||||
self.query_response = response.text;
|
.map(|r| r.text)
|
||||||
|
.map_err(|e| e.to_string());
|
||||||
|
|
||||||
|
let _ = tx.send(QueryResult::Reflect(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_query_result(&mut self) {
|
||||||
|
if let Some(receiver) = &self.query_receiver {
|
||||||
|
match receiver.try_recv() {
|
||||||
|
Ok(QueryResult::Recall(Ok(results))) => {
|
||||||
|
self.query_results = results;
|
||||||
|
if !self.query_results.is_empty() {
|
||||||
|
self.query_results_state.select(Some(0));
|
||||||
|
}
|
||||||
|
self.loading = false;
|
||||||
|
self.status_message = format!("Found {} results", self.query_results.len());
|
||||||
|
self.query_receiver = None;
|
||||||
|
}
|
||||||
|
Ok(QueryResult::Recall(Err(e))) => {
|
||||||
|
self.error_message = format!("Recall failed: {}", e);
|
||||||
|
self.loading = false;
|
||||||
|
self.query_receiver = None;
|
||||||
|
}
|
||||||
|
Ok(QueryResult::Reflect(Ok(text))) => {
|
||||||
|
self.query_response = text;
|
||||||
self.loading = false;
|
self.loading = false;
|
||||||
self.status_message = "Reflection complete".to_string();
|
self.status_message = "Reflection complete".to_string();
|
||||||
|
self.query_receiver = None;
|
||||||
|
}
|
||||||
|
Ok(QueryResult::Reflect(Err(e))) => {
|
||||||
|
self.error_message = format!("Reflect failed: {}", e);
|
||||||
|
self.loading = false;
|
||||||
|
self.query_receiver = None;
|
||||||
|
}
|
||||||
|
Err(TryRecvError::Empty) => {
|
||||||
|
// Still waiting for result
|
||||||
|
}
|
||||||
|
Err(TryRecvError::Disconnected) => {
|
||||||
|
self.error_message = "Query thread disconnected".to_string();
|
||||||
|
self.loading = false;
|
||||||
|
self.query_receiver = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.input_mode = InputMode::Normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_query_mode(&mut self) {
|
fn toggle_query_mode(&mut self) {
|
||||||
|
|
@ -700,64 +766,64 @@ fn render_control_bar(f: &mut Frame, app: &App, area: Rect) {
|
||||||
// Build contextual shortcuts based on view and input mode
|
// Build contextual shortcuts based on view and input mode
|
||||||
let shortcuts = match (&app.view, &app.input_mode) {
|
let shortcuts = match (&app.view, &app.input_mode) {
|
||||||
(View::Banks, InputMode::Normal) => vec![
|
(View::Banks, InputMode::Normal) => vec![
|
||||||
("Enter", "Select", Color::Cyan),
|
("Enter", "Select", BRAND_START),
|
||||||
("R", "Refresh", Color::Yellow),
|
("R", "Refresh", BRAND_MID),
|
||||||
("?", "Help", Color::Magenta),
|
("?", "Help", BRAND_END),
|
||||||
("q", "Quit", Color::Red),
|
("q", "Quit", Color::Red),
|
||||||
],
|
],
|
||||||
(View::Memories(_), InputMode::Normal) => vec![
|
(View::Memories(_), InputMode::Normal) => vec![
|
||||||
("Enter", "View", Color::Cyan),
|
("Enter", "View", BRAND_START),
|
||||||
("/", "Query", Color::Green),
|
("/", "Query", BRAND_MID),
|
||||||
("←→", "Scroll", Color::Cyan),
|
("←→", "Scroll", BRAND_START),
|
||||||
("n", "Next", Color::Green),
|
("n", "Next", BRAND_MID),
|
||||||
("p", "Prev", Color::Green),
|
("p", "Prev", BRAND_MID),
|
||||||
("Esc", "Back", Color::Yellow),
|
("Esc", "Back", BRAND_END),
|
||||||
("R", "Refresh", Color::Yellow),
|
("R", "Refresh", BRAND_END),
|
||||||
("?", "Help", Color::Magenta),
|
("?", "Help", BRAND_END),
|
||||||
("q", "Quit", Color::Red),
|
("q", "Quit", Color::Red),
|
||||||
],
|
],
|
||||||
(View::Entities(_), InputMode::Normal) => vec![
|
(View::Entities(_), InputMode::Normal) => vec![
|
||||||
("Enter", "View", Color::Cyan),
|
("Enter", "View", BRAND_START),
|
||||||
("/", "Query", Color::Green),
|
("/", "Query", BRAND_MID),
|
||||||
("←→", "Scroll", Color::Cyan),
|
("←→", "Scroll", BRAND_START),
|
||||||
("Esc", "Back", Color::Yellow),
|
("Esc", "Back", BRAND_END),
|
||||||
("R", "Refresh", Color::Yellow),
|
("R", "Refresh", BRAND_END),
|
||||||
("?", "Help", Color::Magenta),
|
("?", "Help", BRAND_END),
|
||||||
("q", "Quit", Color::Red),
|
("q", "Quit", Color::Red),
|
||||||
],
|
],
|
||||||
(View::Documents(_), InputMode::Normal) => vec![
|
(View::Documents(_), InputMode::Normal) => vec![
|
||||||
("Enter", "View", Color::Cyan),
|
("Enter", "View", BRAND_START),
|
||||||
("/", "Query", Color::Green),
|
("/", "Query", BRAND_MID),
|
||||||
("←→", "Scroll", Color::Cyan),
|
("←→", "Scroll", BRAND_START),
|
||||||
("Del", "Delete", Color::Red),
|
("Del", "Delete", Color::Red),
|
||||||
("Esc", "Back", Color::Yellow),
|
("Esc", "Back", BRAND_END),
|
||||||
("R", "Refresh", Color::Yellow),
|
("R", "Refresh", BRAND_END),
|
||||||
("?", "Help", Color::Magenta),
|
("?", "Help", BRAND_END),
|
||||||
("q", "Quit", Color::Red),
|
("q", "Quit", Color::Red),
|
||||||
],
|
],
|
||||||
(View::Query(_), InputMode::Normal) => {
|
(View::Query(_), InputMode::Normal) => {
|
||||||
let mut shortcuts = vec![
|
let mut shortcuts = vec![
|
||||||
("/", "Query", Color::Green),
|
("/", "Query", BRAND_MID),
|
||||||
("m", "Mode", Color::Cyan),
|
("m", "Mode", BRAND_START),
|
||||||
];
|
];
|
||||||
if app.query_mode == QueryMode::Recall {
|
if app.query_mode == QueryMode::Recall {
|
||||||
shortcuts.push(("←→", "Scroll", Color::Cyan));
|
shortcuts.push(("←→", "Scroll", BRAND_START));
|
||||||
}
|
}
|
||||||
shortcuts.extend_from_slice(&[
|
shortcuts.extend_from_slice(&[
|
||||||
("b", "Budget", Color::Yellow),
|
("b", "Budget", BRAND_END),
|
||||||
("+/-", "Tokens", Color::Yellow),
|
("+/-", "Tokens", BRAND_END),
|
||||||
("Esc", "Back", Color::Yellow),
|
("Esc", "Back", BRAND_END),
|
||||||
("?", "Help", Color::Magenta),
|
("?", "Help", BRAND_END),
|
||||||
("q", "Quit", Color::Red),
|
("q", "Quit", Color::Red),
|
||||||
]);
|
]);
|
||||||
shortcuts
|
shortcuts
|
||||||
},
|
},
|
||||||
(View::Query(_), InputMode::Query) => vec![
|
(View::Query(_), InputMode::Query) => vec![
|
||||||
("Enter", "Execute", Color::Green),
|
("Enter", "Execute", BRAND_MID),
|
||||||
("Esc", "Cancel", Color::Red),
|
("Esc", "Cancel", Color::Red),
|
||||||
],
|
],
|
||||||
_ => vec![
|
_ => vec![
|
||||||
("?", "Help", Color::Magenta),
|
("?", "Help", BRAND_END),
|
||||||
("q", "Quit", Color::Red),
|
("q", "Quit", Color::Red),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
@ -789,9 +855,9 @@ fn render_control_bar(f: &mut Frame, app: &App, area: Rect) {
|
||||||
let context_widget = Paragraph::new(context_info)
|
let context_widget = Paragraph::new(context_info)
|
||||||
.block(Block::default()
|
.block(Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Cyan))
|
.border_style(Style::default().fg(BRAND_START))
|
||||||
.title(" Context "))
|
.title(" Context "))
|
||||||
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
.style(Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
f.render_widget(context_widget, columns[0]);
|
f.render_widget(context_widget, columns[0]);
|
||||||
|
|
||||||
|
|
@ -827,7 +893,7 @@ fn render_control_bar(f: &mut Frame, app: &App, area: Rect) {
|
||||||
let shortcuts_widget = Paragraph::new(shortcut_lines)
|
let shortcuts_widget = Paragraph::new(shortcut_lines)
|
||||||
.block(Block::default()
|
.block(Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Cyan))
|
.border_style(Style::default().fg(BRAND_START))
|
||||||
.title(" Shortcuts "))
|
.title(" Shortcuts "))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
|
@ -844,7 +910,7 @@ fn render_header(f: &mut Frame, app: &App, area: Rect) {
|
||||||
let title = format!("Hindsight Explorer - {}{}", app.view.title(), bank_info);
|
let title = format!("Hindsight Explorer - {}{}", app.view.title(), bank_info);
|
||||||
|
|
||||||
let header = Paragraph::new(title)
|
let header = Paragraph::new(title)
|
||||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(Block::default().borders(Borders::ALL));
|
.block(Block::default().borders(Borders::ALL));
|
||||||
|
|
||||||
|
|
@ -859,11 +925,11 @@ fn render_footer(f: &mut Frame, app: &App, area: Rect) {
|
||||||
Span::raw(&app.error_message),
|
Span::raw(&app.error_message),
|
||||||
])
|
])
|
||||||
} else if app.loading {
|
} else if app.loading {
|
||||||
Line::from(Span::styled(" Loading...", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)))
|
Line::from(Span::styled(" Loading...", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)))
|
||||||
} else if !app.status_message.is_empty() {
|
} else if !app.status_message.is_empty() {
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled(&app.status_message, Style::default().fg(Color::Green)),
|
Span::styled(&app.status_message, Style::default().fg(BRAND_MID)),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
Line::from("")
|
Line::from("")
|
||||||
|
|
@ -928,7 +994,7 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
|
||||||
let metadata = Paragraph::new(metadata_text)
|
let metadata = Paragraph::new(metadata_text)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Memory Metadata"))
|
.block(Block::default().borders(Borders::ALL).title("Memory Metadata"))
|
||||||
.style(Style::default().fg(Color::Cyan));
|
.style(Style::default().fg(BRAND_START));
|
||||||
|
|
||||||
f.render_widget(metadata, chunks[0]);
|
f.render_widget(metadata, chunks[0]);
|
||||||
|
|
||||||
|
|
@ -946,7 +1012,7 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let mut items = vec![
|
let mut items = vec![
|
||||||
// Header row
|
// Header row
|
||||||
ListItem::new(format!("{:<10} {:<18} {:<18} {}", "TYPE", "MENTIONED AT", "OCCURRED AT", "TEXT"))
|
ListItem::new(format!("{:<10} {:<18} {:<18} {}", "TYPE", "MENTIONED AT", "OCCURRED AT", "TEXT"))
|
||||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||||
];
|
];
|
||||||
|
|
||||||
// Data rows
|
// Data rows
|
||||||
|
|
@ -1002,7 +1068,7 @@ fn render_entities(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
|
||||||
let metadata = Paragraph::new(metadata_text)
|
let metadata = Paragraph::new(metadata_text)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Entity Details (Esc to close)"))
|
.block(Block::default().borders(Borders::ALL).title("Entity Details (Esc to close)"))
|
||||||
.style(Style::default().fg(Color::Cyan))
|
.style(Style::default().fg(BRAND_START))
|
||||||
.wrap(Wrap { trim: false });
|
.wrap(Wrap { trim: false });
|
||||||
|
|
||||||
f.render_widget(metadata, area);
|
f.render_widget(metadata, area);
|
||||||
|
|
@ -1011,7 +1077,7 @@ fn render_entities(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let mut items = vec![
|
let mut items = vec![
|
||||||
// Header row
|
// Header row
|
||||||
ListItem::new(format!("{:<40} {:<15} {:<10}", "NAME", "TYPE", "MENTIONS"))
|
ListItem::new(format!("{:<40} {:<15} {:<10}", "NAME", "TYPE", "MENTIONS"))
|
||||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||||
];
|
];
|
||||||
|
|
||||||
// Data rows
|
// Data rows
|
||||||
|
|
@ -1071,7 +1137,7 @@ fn render_documents(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
|
||||||
let metadata = Paragraph::new(metadata_text)
|
let metadata = Paragraph::new(metadata_text)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Document Metadata"))
|
.block(Block::default().borders(Borders::ALL).title("Document Metadata"))
|
||||||
.style(Style::default().fg(Color::Cyan));
|
.style(Style::default().fg(BRAND_START));
|
||||||
|
|
||||||
f.render_widget(metadata, chunks[0]);
|
f.render_widget(metadata, chunks[0]);
|
||||||
|
|
||||||
|
|
@ -1091,7 +1157,7 @@ fn render_documents(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let mut items = vec![
|
let mut items = vec![
|
||||||
// Header row
|
// Header row
|
||||||
ListItem::new(format!("{:<40} {:<20} {}", "ID", "TYPE", "CREATED"))
|
ListItem::new(format!("{:<40} {:<20} {}", "ID", "TYPE", "CREATED"))
|
||||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||||
];
|
];
|
||||||
|
|
||||||
// Data rows
|
// Data rows
|
||||||
|
|
@ -1137,7 +1203,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
|
||||||
// Query input
|
// Query input
|
||||||
let query_style = if app.input_mode == InputMode::Query {
|
let query_style = if app.input_mode == InputMode::Query {
|
||||||
Style::default().fg(Color::Yellow)
|
Style::default().fg(BRAND_END)
|
||||||
} else {
|
} else {
|
||||||
Style::default()
|
Style::default()
|
||||||
};
|
};
|
||||||
|
|
@ -1154,6 +1220,38 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
|
||||||
f.render_widget(query, chunks[0]);
|
f.render_widget(query, chunks[0]);
|
||||||
|
|
||||||
|
// Show loading indicator if loading
|
||||||
|
if app.loading {
|
||||||
|
let loading_text = match app.query_mode {
|
||||||
|
QueryMode::Recall => "Searching memories...",
|
||||||
|
QueryMode::Reflect => "Reflecting on memories...",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create animated dots based on time
|
||||||
|
let dots = ".".repeat(((std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() / 500) % 4) as usize);
|
||||||
|
|
||||||
|
let loading_lines = vec![
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(" ", Style::default()),
|
||||||
|
Span::styled(format!("{}{}", loading_text, dots), Style::default().fg(BRAND_MID).add_modifier(Modifier::BOLD)),
|
||||||
|
]),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(Span::styled(" Please wait while we process your query...", Style::default().fg(Color::DarkGray))),
|
||||||
|
];
|
||||||
|
|
||||||
|
let loading_widget = Paragraph::new(loading_lines)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title(format!("{} in progress", mode_label)))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
f.render_widget(loading_widget, chunks[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Results or Response based on mode
|
// Results or Response based on mode
|
||||||
match app.query_mode {
|
match app.query_mode {
|
||||||
QueryMode::Recall => {
|
QueryMode::Recall => {
|
||||||
|
|
@ -1180,7 +1278,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
|
||||||
let metadata = Paragraph::new(metadata_text)
|
let metadata = Paragraph::new(metadata_text)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Recall Result Metadata"))
|
.block(Block::default().borders(Borders::ALL).title("Recall Result Metadata"))
|
||||||
.style(Style::default().fg(Color::Cyan));
|
.style(Style::default().fg(BRAND_START));
|
||||||
|
|
||||||
f.render_widget(metadata, recall_chunks[0]);
|
f.render_widget(metadata, recall_chunks[0]);
|
||||||
|
|
||||||
|
|
@ -1196,7 +1294,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let mut items = vec![
|
let mut items = vec![
|
||||||
// Header row
|
// Header row
|
||||||
ListItem::new(format!("{:<10} {:<18} {:<18} {}", "TYPE", "OCCURRED START", "OCCURRED END", "TEXT"))
|
ListItem::new(format!("{:<10} {:<18} {:<18} {}", "TYPE", "OCCURRED START", "OCCURRED END", "TEXT"))
|
||||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
.style(Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))
|
||||||
];
|
];
|
||||||
|
|
||||||
// Data rows
|
// Data rows
|
||||||
|
|
@ -1248,17 +1346,17 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
|
||||||
fn render_help(f: &mut Frame, area: Rect) {
|
fn render_help(f: &mut Frame, area: Rect) {
|
||||||
let help_text = vec![
|
let help_text = vec![
|
||||||
Line::from(Span::styled("Hindsight Explorer - Keyboard Shortcuts", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))),
|
Line::from(Span::styled("Hindsight Explorer - Keyboard Shortcuts", Style::default().fg(BRAND_START).add_modifier(Modifier::BOLD))),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("Navigation Flow", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
Span::styled("Navigation Flow", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)),
|
||||||
]),
|
]),
|
||||||
Line::from(" 1. Start by selecting a bank (Enter)"),
|
Line::from(" 1. Start by selecting a bank (Enter)"),
|
||||||
Line::from(" 2. View memories, entities, or documents for that bank"),
|
Line::from(" 2. View memories, entities, or documents for that bank"),
|
||||||
Line::from(" 3. Press / from any view to query (recall/reflect)"),
|
Line::from(" 3. Press / from any view to query (recall/reflect)"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("Basic Navigation", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
Span::styled("Basic Navigation", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)),
|
||||||
]),
|
]),
|
||||||
Line::from(" ↑/↓, j/k - Navigate up/down in lists"),
|
Line::from(" ↑/↓, j/k - Navigate up/down in lists"),
|
||||||
Line::from(" ←/→, h/l - Scroll text left/right in tables"),
|
Line::from(" ←/→, h/l - Scroll text left/right in tables"),
|
||||||
|
|
@ -1266,7 +1364,7 @@ fn render_help(f: &mut Frame, area: Rect) {
|
||||||
Line::from(" Esc - Go back / close detail view"),
|
Line::from(" Esc - Go back / close detail view"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("Query View", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
Span::styled("Query View", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)),
|
||||||
]),
|
]),
|
||||||
Line::from(" / - Start or edit query (from any non-bank view)"),
|
Line::from(" / - Start or edit query (from any non-bank view)"),
|
||||||
Line::from(" m - Toggle mode (Recall ↔ Reflect)"),
|
Line::from(" m - Toggle mode (Recall ↔ Reflect)"),
|
||||||
|
|
@ -1275,7 +1373,7 @@ fn render_help(f: &mut Frame, area: Rect) {
|
||||||
Line::from(" Enter - Execute query"),
|
Line::from(" Enter - Execute query"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled("General", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
Span::styled("General", Style::default().fg(BRAND_END).add_modifier(Modifier::BOLD)),
|
||||||
]),
|
]),
|
||||||
Line::from(" R - Refresh current view"),
|
Line::from(" R - Refresh current view"),
|
||||||
Line::from(" ? - Toggle this help screen"),
|
Line::from(" ? - Toggle this help screen"),
|
||||||
|
|
@ -1399,7 +1497,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result<()> {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if matches!(app.view, View::Query(_)) {
|
if matches!(app.view, View::Query(_)) {
|
||||||
app.execute_query()?;
|
app.execute_query();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
|
|
@ -1422,6 +1520,9 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for query results from background thread
|
||||||
|
app.check_query_result();
|
||||||
|
|
||||||
// Auto-refresh check
|
// Auto-refresh check
|
||||||
app.do_auto_refresh()?;
|
app.do_auto_refresh()?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ pub fn recall(
|
||||||
|
|
||||||
let response = client.recall(agent_id, &request, verbose);
|
let response = client.recall(agent_id, &request, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -104,8 +104,8 @@ pub fn reflect(
|
||||||
|
|
||||||
let response = client.reflect(agent_id, &request, verbose);
|
let response = client.reflect(agent_id, &request, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -154,8 +154,8 @@ pub fn retain(
|
||||||
|
|
||||||
let response = client.retain(agent_id, &request, r#async, verbose);
|
let response = client.retain(agent_id, &request, r#async, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -274,8 +274,8 @@ pub fn retain_files(
|
||||||
|
|
||||||
let response = client.retain(agent_id, &request, r#async, verbose);
|
let response = client.retain(agent_id, &request, r#async, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -312,8 +312,8 @@ pub fn delete(
|
||||||
|
|
||||||
let response = client.delete_memory(agent_id, unit_id, verbose);
|
let response = client.delete_memory(agent_id, unit_id, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -345,12 +345,12 @@ pub fn clear(
|
||||||
if !yes && output_format == OutputFormat::Pretty {
|
if !yes && output_format == OutputFormat::Pretty {
|
||||||
let message = if let Some(ft) = &fact_type {
|
let message = if let Some(ft) = &fact_type {
|
||||||
format!(
|
format!(
|
||||||
"Are you sure you want to clear all '{}' memories for agent '{}'? This cannot be undone.",
|
"Are you sure you want to clear all '{}' memories for bank '{}'? This cannot be undone.",
|
||||||
ft, agent_id
|
ft, agent_id
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"Are you sure you want to clear ALL memories for agent '{}'? This cannot be undone.",
|
"Are you sure you want to clear ALL memories for bank '{}'? This cannot be undone.",
|
||||||
agent_id
|
agent_id
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -377,8 +377,8 @@ pub fn clear(
|
||||||
|
|
||||||
let response = client.clear_memories(agent_id, fact_type.as_deref(), verbose);
|
let response = client.clear_memories(agent_id, fact_type.as_deref(), verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ pub fn list(
|
||||||
|
|
||||||
let response = client.list_operations(agent_id, verbose);
|
let response = client.list_operations(agent_id, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
@ -62,8 +62,8 @@ pub fn cancel(
|
||||||
|
|
||||||
let response = client.cancel_operation(agent_id, operation_id, verbose);
|
let response = client.cancel_operation(agent_id, operation_id, verbose);
|
||||||
|
|
||||||
if let Some(sp) = spinner {
|
if let Some(mut sp) = spinner {
|
||||||
sp.finish_and_clear();
|
sp.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
|
|
|
||||||
5
hindsight-cli/src/logo.ansi
Normal file
5
hindsight-cli/src/logo.ansi
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
[38;2;9;127;184m▄[0m[48;2;8;130;178m[38;2;5;133;186m▄[0m [48;2;10;143;160m[38;2;10;143;165m▄[0m[38;2;7;140;156m▄[0m
|
||||||
|
[38;2;8;125;192m▄[0m [38;2;3;132;191m▀[0m[38;2;2;133;192m▄[0m [38;2;3;132;180m▄[0m[38;2;1;137;184m▄[0m[38;2;3;133;174m▄[0m [38;2;3;142;176m▄[0m[38;2;4;142;169m▀[0m [38;2;10;144;164m▄[0m
|
||||||
|
[38;2;6;121;195m▀[0m[38;2;5;128;203m▀[0m[48;2;5;124;195m[38;2;3;125;200m▄[0m[38;2;2;126;196m▄[0m[48;2;3;128;188m[38;2;1;131;196m▄[0m[48;2;0;152;219m[38;2;2;131;191m▄[0m[38;2;1;141;196m▀[0m[38;2;1;135;183m▀[0m[38;2;1;148;198m▀[0m[48;2;1;156;202m[38;2;2;135;180m▄[0m[48;2;4;134;169m[38;2;1;137;177m▄[0m[38;2;3;138;173m▄[0m[48;2;6;137;165m[38;2;2;140;170m▄[0m[38;2;7;144;169m▀[0m[38;2;7;139;158m▀[0m
|
||||||
|
[48;2;2;128;202m[38;2;2;124;201m▄[0m[48;2;1;130;201m[38;2;0;135;212m▄[0m[38;2;2;128;196m▄[0m [48;2;2;142;204m[38;2;7;138;199m▄[0m [38;2;1;135;186m▄[0m[48;2;1;142;186m[38;2;2;144;194m▄[0m[48;2;3;138;176m[38;2;2;134;176m▄[0m
|
||||||
|
[48;2;8;118;200m[38;2;8;121;209m▄[0m[38;2;3;121;203m▀[0m [38;2;3;122;192m▀[0m[38;2;1;138;216m▀[0m[48;2;0;138;210m[38;2;3;128;198m▄[0m[48;2;0;126;188m[38;2;2;131;198m▄[0m[48;2;0;142;205m[38;2;3;132;193m▄[0m[38;2;1;140;196m▀[0m [38;2;4;134;175m▀[0m[48;2;13;135;167m[38;2;8;136;174m▄[0m
|
||||||
|
|
@ -34,6 +34,7 @@ impl From<Format> for OutputFormat {
|
||||||
#[command(name = "hindsight")]
|
#[command(name = "hindsight")]
|
||||||
#[command(about = "Hindsight CLI - Semantic memory system", long_about = None)]
|
#[command(about = "Hindsight CLI - Semantic memory system", long_about = None)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
|
#[command(before_help = get_before_help())]
|
||||||
#[command(after_help = get_after_help())]
|
#[command(after_help = get_after_help())]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Output format (pretty, json, yaml)
|
/// Output format (pretty, json, yaml)
|
||||||
|
|
@ -60,6 +61,10 @@ fn get_after_help() -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_before_help() -> &'static str {
|
||||||
|
ui::get_logo()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Manage banks (list, profile, stats)
|
/// Manage banks (list, profile, stats)
|
||||||
|
|
@ -100,8 +105,8 @@ enum BankCommands {
|
||||||
/// List all banks
|
/// List all banks
|
||||||
List,
|
List,
|
||||||
|
|
||||||
/// Get bank profile (disposition + background)
|
/// Get bank disposition and background
|
||||||
Profile {
|
Disposition {
|
||||||
/// Bank ID
|
/// Bank ID
|
||||||
bank_id: String,
|
bank_id: String,
|
||||||
},
|
},
|
||||||
|
|
@ -133,6 +138,16 @@ enum BankCommands {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
no_update_disposition: bool,
|
no_update_disposition: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Delete a bank and all its data
|
||||||
|
Delete {
|
||||||
|
/// Bank ID
|
||||||
|
bank_id: String,
|
||||||
|
|
||||||
|
/// Skip confirmation prompt
|
||||||
|
#[arg(short = 'y', long)]
|
||||||
|
yes: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
|
@ -378,12 +393,15 @@ fn run() -> Result<()> {
|
||||||
Commands::Explore => commands::explore::run(&client),
|
Commands::Explore => commands::explore::run(&client),
|
||||||
Commands::Bank(bank_cmd) => match bank_cmd {
|
Commands::Bank(bank_cmd) => match bank_cmd {
|
||||||
BankCommands::List => commands::bank::list(&client, verbose, output_format),
|
BankCommands::List => commands::bank::list(&client, verbose, output_format),
|
||||||
BankCommands::Profile { bank_id } => commands::bank::profile(&client, &bank_id, verbose, output_format),
|
BankCommands::Disposition { bank_id } => commands::bank::disposition(&client, &bank_id, verbose, output_format),
|
||||||
BankCommands::Stats { bank_id } => commands::bank::stats(&client, &bank_id, verbose, output_format),
|
BankCommands::Stats { bank_id } => commands::bank::stats(&client, &bank_id, verbose, output_format),
|
||||||
BankCommands::Name { bank_id, name } => commands::bank::update_name(&client, &bank_id, &name, verbose, output_format),
|
BankCommands::Name { bank_id, name } => commands::bank::update_name(&client, &bank_id, &name, verbose, output_format),
|
||||||
BankCommands::Background { bank_id, content, no_update_disposition } => {
|
BankCommands::Background { bank_id, content, no_update_disposition } => {
|
||||||
commands::bank::update_background(&client, &bank_id, &content, no_update_disposition, verbose, output_format)
|
commands::bank::update_background(&client, &bank_id, &content, no_update_disposition, verbose, output_format)
|
||||||
}
|
}
|
||||||
|
BankCommands::Delete { bank_id, yes } => {
|
||||||
|
commands::bank::delete(&client, &bank_id, yes, verbose, output_format)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Commands::Memory(memory_cmd) => match memory_cmd {
|
Commands::Memory(memory_cmd) => match memory_cmd {
|
||||||
|
|
|
||||||
|
|
@ -4,80 +4,132 @@ use hindsight_client::types::ChunkData;
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
/// The logo as ANSI-colored text, generated by test-logo.py
|
||||||
|
const LOGO: &str = include_str!("logo.ansi");
|
||||||
|
|
||||||
|
// Gradient colors: #0074d9 -> #009296
|
||||||
|
const GRADIENT_START: (u8, u8, u8) = (0, 116, 217); // #0074d9
|
||||||
|
const GRADIENT_END: (u8, u8, u8) = (0, 146, 150); // #009296
|
||||||
|
|
||||||
|
/// Interpolate between two RGB colors
|
||||||
|
fn interpolate_color(start: (u8, u8, u8), end: (u8, u8, u8), t: f32) -> (u8, u8, u8) {
|
||||||
|
(
|
||||||
|
(start.0 as f32 + (end.0 as f32 - start.0 as f32) * t) as u8,
|
||||||
|
(start.1 as f32 + (end.1 as f32 - start.1 as f32) * t) as u8,
|
||||||
|
(start.2 as f32 + (end.2 as f32 - start.2 as f32) * t) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color text using gradient position (0.0 = start, 1.0 = end)
|
||||||
|
pub fn gradient(text: &str, t: f32) -> String {
|
||||||
|
let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t);
|
||||||
|
format!("\x1b[38;2;{};{};{}m{}\x1b[0m", r, g, b, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color text with gradient start color (#0074d9)
|
||||||
|
pub fn gradient_start(text: &str) -> String {
|
||||||
|
gradient(text, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color text with gradient end color (#009296)
|
||||||
|
pub fn gradient_end(text: &str) -> String {
|
||||||
|
gradient(text, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color text with gradient middle color
|
||||||
|
pub fn gradient_mid(text: &str) -> String {
|
||||||
|
gradient(text, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply gradient across entire text string
|
||||||
|
pub fn gradient_text(text: &str) -> String {
|
||||||
|
let chars: Vec<char> = text.chars().collect();
|
||||||
|
let len = chars.len();
|
||||||
|
if len == 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let mut result = String::new();
|
||||||
|
for (i, ch) in chars.iter().enumerate() {
|
||||||
|
if *ch == ' ' {
|
||||||
|
result.push(' ');
|
||||||
|
} else {
|
||||||
|
let t = i as f32 / (len - 1).max(1) as f32;
|
||||||
|
let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t);
|
||||||
|
result.push_str(&format!("\x1b[38;2;{};{};{}m{}", r, g, b, ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push_str("\x1b[0m");
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dim/gray text
|
||||||
|
pub fn dim(text: &str) -> String {
|
||||||
|
format!("\x1b[38;2;128;128;128m{}\x1b[0m", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_logo() -> &'static str {
|
||||||
|
LOGO
|
||||||
|
}
|
||||||
|
|
||||||
pub fn print_section_header(title: &str) {
|
pub fn print_section_header(title: &str) {
|
||||||
println!();
|
println!();
|
||||||
println!("{}", format!("━━━ {} ━━━", title).bright_yellow().bold());
|
println!("{}", gradient_text(&format!("━━━ {} ━━━", title)));
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_fact(fact: &RecallResult, show_activation: bool) {
|
pub fn print_fact(fact: &RecallResult, _show_activation: bool) {
|
||||||
let fact_type = fact.type_.as_deref().unwrap_or("unknown");
|
let fact_type = fact.type_.as_deref().unwrap_or("unknown");
|
||||||
|
|
||||||
let type_color = match fact_type {
|
// Use gradient positions for different fact types
|
||||||
"world" => "cyan",
|
let type_t = match fact_type {
|
||||||
"agent" => "magenta",
|
"world" => 0.0,
|
||||||
"opinion" => "yellow",
|
"agent" => 0.5,
|
||||||
_ => "white",
|
"opinion" => 1.0,
|
||||||
|
_ => 0.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
let prefix = match fact_type {
|
println!("{}", gradient(&format!("[{}]", fact_type.to_uppercase()), type_t));
|
||||||
"world" => "🌍",
|
|
||||||
"agent" => "🤖",
|
|
||||||
"opinion" => "💭",
|
|
||||||
_ => "📝",
|
|
||||||
};
|
|
||||||
|
|
||||||
print!("{} ", prefix);
|
|
||||||
print!("{}", format!("[{}]", fact_type.to_uppercase()).color(type_color).bold());
|
|
||||||
|
|
||||||
// Note: activation field not available in generated SearchResult
|
|
||||||
// The API doesn't return it in the current schema
|
|
||||||
if show_activation {
|
|
||||||
// Placeholder for when activation is added to the API schema
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!(" {}", fact.text);
|
println!(" {}", fact.text);
|
||||||
|
|
||||||
// Show context if available
|
// Show context if available
|
||||||
if let Some(context) = &fact.context {
|
if let Some(context) = &fact.context {
|
||||||
println!(" {}: {}", "Context".bright_black(), context.bright_black());
|
println!(" {} {}", dim("context:"), dim(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show temporal information
|
// Show temporal information
|
||||||
if let Some(occurred_start) = &fact.occurred_start {
|
if let Some(occurred_start) = &fact.occurred_start {
|
||||||
if let Some(occurred_end) = &fact.occurred_end {
|
if let Some(occurred_end) = &fact.occurred_end {
|
||||||
println!(" {}: {} - {}", "Date".bright_black(), occurred_start.bright_black(), occurred_end.bright_black());
|
println!(" {} {} - {}", dim("date:"), dim(occurred_start), dim(occurred_end));
|
||||||
} else {
|
} else {
|
||||||
println!(" {}: {}", "Date".bright_black(), occurred_start.bright_black());
|
println!(" {} {}", dim("date:"), dim(occurred_start));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show document ID if available
|
// Show document ID if available
|
||||||
if let Some(document_id) = &fact.document_id {
|
if let Some(document_id) = &fact.document_id {
|
||||||
println!(" {}: {}", "Document".bright_black(), document_id.bright_black());
|
println!(" {} {}", dim("document:"), dim(document_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_chunk(chunk: &ChunkData) {
|
pub fn print_chunk(chunk: &ChunkData) {
|
||||||
println!(" {}", "─── Source Chunk ───".bright_blue());
|
println!(" {}", gradient_mid("─── Source Chunk ───"));
|
||||||
|
|
||||||
// Split text into lines and indent each line
|
// Split text into lines and indent each line
|
||||||
for line in chunk.text.lines() {
|
for line in chunk.text.lines() {
|
||||||
println!(" {}", line.bright_white());
|
println!(" {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if chunk.truncated {
|
if chunk.truncated {
|
||||||
println!(" {}", "[Truncated due to token limit]".bright_yellow());
|
println!(" {}", gradient_end("[Truncated due to token limit]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(" {}: {} | {}: {}",
|
println!(" {} {} | {} {}",
|
||||||
"Chunk ID".bright_black(),
|
dim("Chunk ID:"),
|
||||||
chunk.id.bright_black(),
|
dim(&chunk.id),
|
||||||
"Index".bright_black(),
|
dim("Index:"),
|
||||||
chunk.chunk_index.to_string().bright_black()
|
dim(&chunk.chunk_index.to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
|
@ -88,10 +140,10 @@ pub fn print_search_results(response: &RecallResponse, show_trace: bool, show_ch
|
||||||
print_section_header(&format!("Search Results ({})", results.len()));
|
print_section_header(&format!("Search Results ({})", results.len()));
|
||||||
|
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
println!("{}", " No results found.".bright_black());
|
println!(" {}", dim("No results found."));
|
||||||
} else {
|
} else {
|
||||||
for (i, fact) in results.iter().enumerate() {
|
for (i, fact) in results.iter().enumerate() {
|
||||||
println!("{}", format!(" Result #{}", i + 1).bright_black());
|
println!(" {}", dim(&format!("Result #{}", i + 1)));
|
||||||
print_fact(fact, true);
|
print_fact(fact, true);
|
||||||
|
|
||||||
// Show chunk if available and requested
|
// Show chunk if available and requested
|
||||||
|
|
@ -115,56 +167,120 @@ pub fn print_search_results(response: &RecallResponse, show_trace: bool, show_ch
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_think_response(response: &ReflectResponse) {
|
pub fn print_think_response(response: &ReflectResponse) {
|
||||||
println!();
|
print_section_header("Reflection");
|
||||||
println!("{}", response.text.bright_white());
|
|
||||||
|
println!("{}", response.text);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
if !response.based_on.is_empty() {
|
if !response.based_on.is_empty() {
|
||||||
println!("{}", format!("Based on {} memory units", response.based_on.len()).bright_black());
|
println!("{}", dim(&format!("Based on {} memory units", response.based_on.len())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_trace_info(trace: &serde_json::Map<String, serde_json::Value>) {
|
pub fn print_trace_info(trace: &serde_json::Map<String, serde_json::Value>) {
|
||||||
print_section_header("Trace Information");
|
print_section_header("Trace");
|
||||||
|
|
||||||
if let Some(time) = trace.get("total_time").and_then(|v| v.as_f64()) {
|
if let Some(time) = trace.get("total_time").and_then(|v| v.as_f64()) {
|
||||||
println!(" ⏱️ Total time: {}", format!("{:.2}ms", time).bright_green());
|
println!(" {} {}", dim("total time:"), gradient_start(&format!("{:.2}ms", time)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(count) = trace.get("activation_count").and_then(|v| v.as_i64()) {
|
if let Some(count) = trace.get("activation_count").and_then(|v| v.as_i64()) {
|
||||||
println!(" 📊 Activation count: {}", count.to_string().bright_green());
|
println!(" {} {}", dim("activation count:"), gradient_end(&count.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_success(message: &str) {
|
pub fn print_success(message: &str) {
|
||||||
println!("{} {}", "✓".bright_green().bold(), message.bright_white());
|
println!("{}", gradient_start(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_error(message: &str) {
|
pub fn print_error(message: &str) {
|
||||||
eprintln!("{} {}", "✗".bright_red().bold(), message.bright_red());
|
eprintln!("{} {}", "error:".bright_red().bold(), message.bright_red());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_warning(message: &str) {
|
pub fn print_warning(message: &str) {
|
||||||
println!("{} {}", "⚠".bright_yellow().bold(), message.bright_yellow());
|
println!("{} {}", gradient_end("warning:"), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_info(message: &str) {
|
pub fn print_info(message: &str) {
|
||||||
println!("{} {}", "ℹ".bright_blue().bold(), message.bright_white());
|
println!("{}", gradient_start(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_spinner(message: &str) -> ProgressBar {
|
/// Animated gradient spinner that shows text with moving gradient colors
|
||||||
let pb = ProgressBar::new_spinner();
|
pub struct GradientSpinner {
|
||||||
pb.set_style(
|
message: String,
|
||||||
ProgressStyle::default_spinner()
|
running: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||||
.template("{spinner:.cyan} {msg}")
|
handle: Option<std::thread::JoinHandle<()>>,
|
||||||
.unwrap()
|
}
|
||||||
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
|
|
||||||
);
|
impl GradientSpinner {
|
||||||
pb.set_message(message.to_string());
|
pub fn new(message: &str) -> Self {
|
||||||
pb.enable_steady_tick(std::time::Duration::from_millis(80));
|
let message = message.to_string();
|
||||||
pb
|
let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
||||||
|
|
||||||
|
let msg_clone = message.clone();
|
||||||
|
let running_clone = running.clone();
|
||||||
|
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
let chars: Vec<char> = msg_clone.chars().collect();
|
||||||
|
let len = chars.len();
|
||||||
|
let num_frames = 30;
|
||||||
|
let mut current_frame = 0usize;
|
||||||
|
|
||||||
|
while running_clone.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
current_frame = (current_frame + 1) % num_frames;
|
||||||
|
let offset = current_frame as f32 / num_frames as f32;
|
||||||
|
|
||||||
|
// Build the gradient string
|
||||||
|
let mut result = String::from("\r");
|
||||||
|
for (i, ch) in chars.iter().enumerate() {
|
||||||
|
if *ch == ' ' {
|
||||||
|
result.push(' ');
|
||||||
|
} else {
|
||||||
|
let base_t = if len > 1 { i as f32 / (len - 1) as f32 } else { 0.0 };
|
||||||
|
let t = (base_t + offset) % 1.0;
|
||||||
|
let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t);
|
||||||
|
result.push_str(&format!("\x1b[38;2;{};{};{}m{}", r, g, b, ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push_str("\x1b[0m");
|
||||||
|
|
||||||
|
print!("{}", result);
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(80));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
message,
|
||||||
|
running,
|
||||||
|
handle: Some(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(&mut self) {
|
||||||
|
self.running.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
if let Some(handle) = self.handle.take() {
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
|
// Clear the line
|
||||||
|
print!("\r{}\r", " ".repeat(self.message.len() + 10));
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for GradientSpinner {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.running.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
self.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_spinner(message: &str) -> GradientSpinner {
|
||||||
|
GradientSpinner::new(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
|
pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
|
||||||
|
|
@ -180,7 +296,7 @@ pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prompt_confirmation(message: &str) -> io::Result<bool> {
|
pub fn prompt_confirmation(message: &str) -> io::Result<bool> {
|
||||||
print!("{} {} [y/N]: ", "?".bright_blue().bold(), message);
|
print!("{} [y/N]: ", gradient_start(message));
|
||||||
io::stdout().flush()?;
|
io::stdout().flush()?;
|
||||||
|
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
|
|
@ -189,16 +305,16 @@ pub fn prompt_confirmation(message: &str) -> io::Result<bool> {
|
||||||
Ok(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes"))
|
Ok(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_profile(profile: &BankProfileResponse) {
|
pub fn print_disposition(profile: &BankProfileResponse) {
|
||||||
print_section_header(&format!("Bank Profile: {}", profile.bank_id));
|
print_section_header(&format!("Disposition: {}", profile.bank_id));
|
||||||
|
|
||||||
// Print name
|
// Print name
|
||||||
println!("{} {}", "Name:".bright_cyan().bold(), profile.name.bright_white());
|
println!("{} {}", dim("Name:"), gradient_start(&profile.name));
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Print background if available
|
// Print background if available
|
||||||
if !profile.background.is_empty() {
|
if !profile.background.is_empty() {
|
||||||
println!("{}", "Background:".bright_yellow());
|
println!("{}", gradient_mid("Background:"));
|
||||||
for line in profile.background.lines() {
|
for line in profile.background.lines() {
|
||||||
println!("{}", line);
|
println!("{}", line);
|
||||||
}
|
}
|
||||||
|
|
@ -206,38 +322,30 @@ pub fn print_profile(profile: &BankProfileResponse) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print disposition traits
|
// Print disposition traits
|
||||||
println!("{}", "─── Disposition Traits ───".bright_yellow());
|
println!("{}", gradient_text("─── Disposition Traits ───"));
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// New 3-trait disposition system (values 1-5)
|
// New 3-trait disposition system (values 1-5)
|
||||||
let traits: [(_, i64, _, _, _); 3] = [
|
let traits: [(_, i64, f32, _); 3] = [
|
||||||
("Skepticism", profile.disposition.skepticism.get() as i64, "🔍", "cyan", "1=trusting, 5=skeptical"),
|
("Skepticism", profile.disposition.skepticism.get() as i64, 0.0, "1=trusting, 5=skeptical"),
|
||||||
("Literalism", profile.disposition.literalism.get() as i64, "📋", "yellow", "1=flexible, 5=literal"),
|
("Literalism", profile.disposition.literalism.get() as i64, 0.5, "1=flexible, 5=literal"),
|
||||||
("Empathy", profile.disposition.empathy.get() as i64, "💚", "green", "1=detached, 5=empathetic"),
|
("Empathy", profile.disposition.empathy.get() as i64, 1.0, "1=detached, 5=empathetic"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (name, value, emoji, color, desc) in &traits {
|
for (name, value, t, desc) in &traits {
|
||||||
// Scale 1-5 to bar visualization (each point = 8 chars, total 40)
|
// Scale 1-5 to bar visualization (each point = 8 chars, total 40)
|
||||||
let bar_length = 40;
|
let bar_length = 40;
|
||||||
let filled = ((*value - 1) * 10) as usize; // 1->0, 2->10, 3->20, 4->30, 5->40
|
let filled = ((*value - 1) * 10) as usize; // 1->0, 2->10, 3->20, 4->30, 5->40
|
||||||
let empty = bar_length - filled;
|
let empty = bar_length - filled;
|
||||||
|
|
||||||
let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
|
let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
|
||||||
let colored_bar = match *color {
|
|
||||||
"green" => bar.bright_green(),
|
|
||||||
"yellow" => bar.bright_yellow(),
|
|
||||||
"cyan" => bar.bright_cyan(),
|
|
||||||
"magenta" => bar.bright_magenta(),
|
|
||||||
_ => bar.bright_white(),
|
|
||||||
};
|
|
||||||
|
|
||||||
println!(" {} {:<12} [{}] {}/5",
|
println!(" {:<12} [{}] {}/5",
|
||||||
emoji,
|
|
||||||
name,
|
name,
|
||||||
colored_bar,
|
gradient(&bar, *t),
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
println!(" {}", desc.bright_black());
|
println!(" {}", dim(desc));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
|
|
||||||
190
hindsight-control-plane/package-lock.json
generated
190
hindsight-control-plane/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "hindsight-control-plane",
|
"name": "hindsight-control-plane",
|
||||||
"version": "0.0.21",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hindsight-control-plane",
|
"name": "hindsight-control-plane",
|
||||||
"version": "0.0.21",
|
"version": "0.1.2",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
|
|
@ -15,8 +15,11 @@
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@types/cytoscape": "^3.21.9",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
|
|
@ -33,19 +36,19 @@
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-chrono": "^2.9.1",
|
"react-chrono": "^2.9.1",
|
||||||
"react-cytoscape": "^1.0.6",
|
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react18-json-view": "^0.2.9",
|
"react18-json-view": "^0.2.9",
|
||||||
"recharts": "^3.5.1",
|
"recharts": "^3.5.1",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"three": "^0.182.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../hindsight-clients/typescript": {
|
"../hindsight-clients/typescript": {
|
||||||
"name": "@vectorize-io/hindsight-client",
|
"name": "@vectorize-io/hindsight-client",
|
||||||
"version": "0.0.21",
|
"version": "0.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "^0.88.0",
|
"@hey-api/openapi-ts": "^0.88.0",
|
||||||
|
|
@ -5045,6 +5048,39 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-slider": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -5061,6 +5097,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -5324,6 +5389,12 @@
|
||||||
"tailwindcss": "4.1.17"
|
"tailwindcss": "4.1.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cytoscape": {
|
||||||
|
"version": "3.21.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz",
|
||||||
|
"integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-array": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
|
@ -6219,31 +6290,13 @@
|
||||||
},
|
},
|
||||||
"node_modules/cytoscape": {
|
"node_modules/cytoscape": {
|
||||||
"version": "3.33.1",
|
"version": "3.33.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||||
|
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cytoscape-cola": {
|
|
||||||
"version": "2.5.1",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"webcola": "^3.4.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"cytoscape": "^3.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cytoscape-dagre": {
|
|
||||||
"version": "2.5.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"dagre": "^0.8.5"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"cytoscape": "^3.2.22"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
|
@ -6265,18 +6318,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-dispatch": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/d3-drag": {
|
|
||||||
"version": "1.2.5",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"d3-dispatch": "1",
|
|
||||||
"d3-selection": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/d3-ease": {
|
"node_modules/d3-ease": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
|
@ -6307,10 +6348,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-path": {
|
|
||||||
"version": "1.0.9",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/d3-scale": {
|
"node_modules/d3-scale": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
|
@ -6327,17 +6364,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-selection": {
|
|
||||||
"version": "1.4.2",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/d3-shape": {
|
|
||||||
"version": "1.3.7",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"d3-path": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/d3-time": {
|
"node_modules/d3-time": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
|
@ -6362,18 +6388,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/d3-timer": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/dagre": {
|
|
||||||
"version": "0.8.5",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"graphlib": "^2.1.8",
|
|
||||||
"lodash": "^4.17.15"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
|
|
@ -7215,17 +7229,6 @@
|
||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -7389,13 +7392,6 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/graphlib": {
|
|
||||||
"version": "2.1.8",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"lodash": "^4.17.15"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-bigints": {
|
"node_modules/has-bigints": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -8044,10 +8040,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
|
||||||
"version": "4.17.21",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|
@ -8550,18 +8542,6 @@
|
||||||
"styled-components": "^6.0.0"
|
"styled-components": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-cytoscape": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cytoscape": "^3.2.5",
|
|
||||||
"cytoscape-cola": "^2.0.0",
|
|
||||||
"cytoscape-dagre": "^2.1.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -9315,6 +9295,12 @@
|
||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.182.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
||||||
|
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tiny-invariant": {
|
"node_modules/tiny-invariant": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
|
@ -9713,16 +9699,6 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webcola": {
|
|
||||||
"version": "3.4.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"d3-dispatch": "^1.0.3",
|
|
||||||
"d3-drag": "^1.0.4",
|
|
||||||
"d3-shape": "^1.3.5",
|
|
||||||
"d3-timer": "^1.0.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,11 @@
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-radio-group": "^1.3.8",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@types/cytoscape": "^3.21.9",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
|
|
@ -37,13 +40,13 @@
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-chrono": "^2.9.1",
|
"react-chrono": "^2.9.1",
|
||||||
"react-cytoscape": "^1.0.6",
|
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react18-json-view": "^0.2.9",
|
"react18-json-view": "^0.2.9",
|
||||||
"recharts": "^3.5.1",
|
"recharts": "^3.5.1",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"three": "^0.182.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
hindsight-control-plane/public/favicon.png
Normal file
BIN
hindsight-control-plane/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
hindsight-control-plane/public/logo.png
Normal file
BIN
hindsight-control-plane/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
|
|
@ -1,3 +1,4 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&display=swap');
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(0.9911 0 0);
|
--background: oklch(0.9911 0 0);
|
||||||
|
|
@ -6,8 +7,9 @@
|
||||||
--card-foreground: oklch(0.2046 0 0);
|
--card-foreground: oklch(0.2046 0 0);
|
||||||
--popover: oklch(0.9911 0 0);
|
--popover: oklch(0.9911 0 0);
|
||||||
--popover-foreground: oklch(0.4386 0 0);
|
--popover-foreground: oklch(0.4386 0 0);
|
||||||
--primary: oklch(0.8348 0.1302 160.9080);
|
--primary: oklch(0.55 0.19 250);
|
||||||
--primary-foreground: oklch(0.2626 0.0147 166.4589);
|
--primary-foreground: oklch(0.98 0.01 250);
|
||||||
|
--primary-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||||
--secondary: oklch(0.9940 0 0);
|
--secondary: oklch(0.9940 0 0);
|
||||||
--secondary-foreground: oklch(0.2046 0 0);
|
--secondary-foreground: oklch(0.2046 0 0);
|
||||||
--muted: oklch(0.9461 0 0);
|
--muted: oklch(0.9461 0 0);
|
||||||
|
|
@ -18,23 +20,23 @@
|
||||||
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
|
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
|
||||||
--border: oklch(0.9037 0 0);
|
--border: oklch(0.9037 0 0);
|
||||||
--input: oklch(0.9731 0 0);
|
--input: oklch(0.9731 0 0);
|
||||||
--ring: oklch(0.8348 0.1302 160.9080);
|
--ring: oklch(0.55 0.19 250);
|
||||||
--chart-1: oklch(0.8348 0.1302 160.9080);
|
--chart-1: oklch(0.55 0.19 250);
|
||||||
--chart-2: oklch(0.6231 0.1880 259.8145);
|
--chart-2: oklch(0.6231 0.1880 259.8145);
|
||||||
--chart-3: oklch(0.6056 0.2189 292.7172);
|
--chart-3: oklch(0.6056 0.2189 292.7172);
|
||||||
--chart-4: oklch(0.7686 0.1647 70.0804);
|
--chart-4: oklch(0.7686 0.1647 70.0804);
|
||||||
--chart-5: oklch(0.6959 0.1491 162.4796);
|
--chart-5: oklch(0.6959 0.1491 162.4796);
|
||||||
--sidebar: oklch(0.9911 0 0);
|
--sidebar: oklch(0.9911 0 0);
|
||||||
--sidebar-foreground: oklch(0.5452 0 0);
|
--sidebar-foreground: oklch(0.5452 0 0);
|
||||||
--sidebar-primary: oklch(0.8348 0.1302 160.9080);
|
--sidebar-primary: oklch(0.55 0.19 250);
|
||||||
--sidebar-primary-foreground: oklch(0.2626 0.0147 166.4589);
|
--sidebar-primary-foreground: oklch(0.98 0.01 250);
|
||||||
--sidebar-accent: oklch(0.9461 0 0);
|
--sidebar-accent: oklch(0.9461 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.2435 0 0);
|
--sidebar-accent-foreground: oklch(0.2435 0 0);
|
||||||
--sidebar-border: oklch(0.9037 0 0);
|
--sidebar-border: oklch(0.9037 0 0);
|
||||||
--sidebar-ring: oklch(0.8348 0.1302 160.9080);
|
--sidebar-ring: oklch(0.55 0.19 250);
|
||||||
--font-sans: 'Avenir Book', 'Avenir', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
--font-heading: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
--font-mono: monospace;
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
--shadow-x: 0px;
|
--shadow-x: 0px;
|
||||||
--shadow-y: 1px;
|
--shadow-y: 1px;
|
||||||
|
|
@ -61,8 +63,9 @@
|
||||||
--card-foreground: oklch(0.9288 0.0126 255.5078);
|
--card-foreground: oklch(0.9288 0.0126 255.5078);
|
||||||
--popover: oklch(0.2603 0 0);
|
--popover: oklch(0.2603 0 0);
|
||||||
--popover-foreground: oklch(0.7348 0 0);
|
--popover-foreground: oklch(0.7348 0 0);
|
||||||
--primary: oklch(0.4365 0.1044 156.7556);
|
--primary: oklch(0.60 0.17 250);
|
||||||
--primary-foreground: oklch(0.9213 0.0135 167.1556);
|
--primary-foreground: oklch(0.98 0.01 250);
|
||||||
|
--primary-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||||
--secondary: oklch(0.2603 0 0);
|
--secondary: oklch(0.2603 0 0);
|
||||||
--secondary-foreground: oklch(0.9851 0 0);
|
--secondary-foreground: oklch(0.9851 0 0);
|
||||||
--muted: oklch(0.2393 0 0);
|
--muted: oklch(0.2393 0 0);
|
||||||
|
|
@ -73,38 +76,20 @@
|
||||||
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
|
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
|
||||||
--border: oklch(0.2809 0 0);
|
--border: oklch(0.2809 0 0);
|
||||||
--input: oklch(0.2603 0 0);
|
--input: oklch(0.2603 0 0);
|
||||||
--ring: oklch(0.8003 0.1821 151.7110);
|
--ring: oklch(0.60 0.17 250);
|
||||||
--chart-1: oklch(0.8003 0.1821 151.7110);
|
--chart-1: oklch(0.60 0.17 250);
|
||||||
--chart-2: oklch(0.7137 0.1434 254.6240);
|
--chart-2: oklch(0.7137 0.1434 254.6240);
|
||||||
--chart-3: oklch(0.7090 0.1592 293.5412);
|
--chart-3: oklch(0.7090 0.1592 293.5412);
|
||||||
--chart-4: oklch(0.8369 0.1644 84.4286);
|
--chart-4: oklch(0.8369 0.1644 84.4286);
|
||||||
--chart-5: oklch(0.7845 0.1325 181.9120);
|
--chart-5: oklch(0.7845 0.1325 181.9120);
|
||||||
--sidebar: oklch(0.1822 0 0);
|
--sidebar: oklch(0.1822 0 0);
|
||||||
--sidebar-foreground: oklch(0.6301 0 0);
|
--sidebar-foreground: oklch(0.6301 0 0);
|
||||||
--sidebar-primary: oklch(0.4365 0.1044 156.7556);
|
--sidebar-primary: oklch(0.60 0.17 250);
|
||||||
--sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
|
--sidebar-primary-foreground: oklch(0.98 0.01 250);
|
||||||
--sidebar-accent: oklch(0.3132 0 0);
|
--sidebar-accent: oklch(0.3132 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||||
--sidebar-border: oklch(0.2809 0 0);
|
--sidebar-border: oklch(0.2809 0 0);
|
||||||
--sidebar-ring: oklch(0.8003 0.1821 151.7110);
|
--sidebar-ring: oklch(0.60 0.17 250);
|
||||||
--font-sans: 'Avenir Book', 'Avenir', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
|
||||||
--font-mono: monospace;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
--shadow-x: 0px;
|
|
||||||
--shadow-y: 1px;
|
|
||||||
--shadow-blur: 3px;
|
|
||||||
--shadow-spread: 0px;
|
|
||||||
--shadow-opacity: 0.17;
|
|
||||||
--shadow-color: #000000;
|
|
||||||
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
|
||||||
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
|
||||||
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
|
||||||
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
|
||||||
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
|
|
||||||
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
|
|
||||||
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
|
|
||||||
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|
@ -143,7 +128,7 @@
|
||||||
|
|
||||||
--font-sans: var(--font-sans);
|
--font-sans: var(--font-sans);
|
||||||
--font-mono: var(--font-mono);
|
--font-mono: var(--font-mono);
|
||||||
--font-serif: var(--font-serif);
|
--font-heading: var(--font-heading);
|
||||||
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
|
@ -171,3 +156,28 @@ body {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
letter-spacing: var(--tracking-normal);
|
letter-spacing: var(--tracking-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient utilities */
|
||||||
|
.bg-primary-gradient {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-primary-gradient {
|
||||||
|
border-image: var(--primary-gradient) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary-gradient {
|
||||||
|
background: var(--primary-gradient);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { BankProvider } from "@/lib/bank-context";
|
import { BankProvider } from "@/lib/bank-context";
|
||||||
|
import { ThemeProvider } from "@/lib/theme-context";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Hindsight Control Plane",
|
title: "Hindsight Control Plane",
|
||||||
description: "Control plane for the temporal semantic memory system",
|
description: "Control plane for the temporal semantic memory system",
|
||||||
|
icons: {
|
||||||
|
icon: "/favicon.png",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
@ -13,11 +17,13 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<body>
|
<body>
|
||||||
|
<ThemeProvider>
|
||||||
<BankProvider>
|
<BankProvider>
|
||||||
{children}
|
{children}
|
||||||
</BankProvider>
|
</BankProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@ import {
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Check, ChevronsUpDown, Plus, FileText } from 'lucide-react';
|
import { Check, ChevronsUpDown, Plus, FileText, Moon, Sun, Github } from 'lucide-react';
|
||||||
|
import { useTheme } from '@/lib/theme-context';
|
||||||
|
import Image from 'next/image';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
@ -36,6 +38,7 @@ function BankSelectorInner() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { currentBank, setCurrentBank, banks, loadBanks } = useBank();
|
const { currentBank, setCurrentBank, banks, loadBanks } = useBank();
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
||||||
const [newBankId, setNewBankId] = React.useState('');
|
const [newBankId, setNewBankId] = React.useState('');
|
||||||
|
|
@ -119,26 +122,34 @@ function BankSelectorInner() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary-gradient">
|
||||||
<div className="flex items-center gap-2.5 text-sm">
|
<div className="flex items-center gap-4 text-sm">
|
||||||
<span className="font-medium">Memory Bank:</span>
|
{/* Logo */}
|
||||||
|
<Image src="/logo.png" alt="Hindsight" width={40} height={40} className="h-10 w-auto" unoptimized />
|
||||||
|
|
||||||
|
{/* Separator */}
|
||||||
|
<div className="h-8 w-px bg-border" />
|
||||||
|
|
||||||
|
{/* Memory Bank Selector */}
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className="w-[300px] justify-between font-bold border-2 border-primary hover:bg-accent"
|
className="w-[250px] justify-between font-bold border-2 border-primary hover:bg-accent"
|
||||||
>
|
>
|
||||||
{currentBank || "Select a memory bank..."}
|
{currentBank || "Select a memory bank..."}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[300px] p-0">
|
<PopoverContent className="w-[250px] p-0">
|
||||||
<Command>
|
<Command>
|
||||||
|
{sortedBanks.length > 0 && (
|
||||||
<CommandInput placeholder="Search memory banks..." />
|
<CommandInput placeholder="Search memory banks..." />
|
||||||
|
)}
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No memory bank found.</CommandEmpty>
|
<CommandEmpty>No memory banks yet.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{sortedBanks.map((bank) => (
|
{sortedBanks.map((bank) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
|
@ -165,34 +176,73 @@ function BankSelectorInner() {
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
|
{/* Footer: Create new bank */}
|
||||||
|
<div className="border-t border-border p-1">
|
||||||
|
<button
|
||||||
|
className="w-full flex items-center gap-2 px-2 py-2 text-sm rounded-md hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
setCreateDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
<span>Create new bank</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
<Button
|
{/* Separator */}
|
||||||
variant="outline"
|
<div className="h-8 w-px bg-border" />
|
||||||
size="sm"
|
|
||||||
className="h-9 border-2 border-primary hover:bg-accent gap-1.5"
|
|
||||||
onClick={() => setCreateDialogOpen(true)}
|
|
||||||
title="Create new memory bank"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
<span>New Bank</span>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
|
{/* Add Document Button */}
|
||||||
{currentBank && (
|
{currentBank && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-9 border-2 border-secondary hover:bg-secondary/20 gap-1.5"
|
className="h-9 gap-1.5"
|
||||||
onClick={() => setDocDialogOpen(true)}
|
onClick={() => setDocDialogOpen(true)}
|
||||||
title="Add document to current bank"
|
title="Add document to current bank"
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
<span>New Document</span>
|
<span>Add Document</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Spacer */}
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
|
{/* GitHub Link */}
|
||||||
|
<a
|
||||||
|
href="https://github.com/vectorize-io/hindsight"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
||||||
|
title="View on GitHub"
|
||||||
|
>
|
||||||
|
<Github className="h-5 w-5" />
|
||||||
|
<span className="text-sm font-medium">GitHub</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Separator */}
|
||||||
|
<div className="h-8 w-px bg-border" />
|
||||||
|
|
||||||
|
{/* Dark Mode Toggle */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="h-9 w-9"
|
||||||
|
title={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
|
||||||
|
>
|
||||||
|
{theme === 'light' ? (
|
||||||
|
<Moon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Sun className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|
@ -333,17 +383,32 @@ function BankSelectorInner() {
|
||||||
export function BankSelector() {
|
export function BankSelector() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={
|
<Suspense fallback={
|
||||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary-gradient">
|
||||||
<div className="flex items-center gap-2.5 text-sm">
|
<div className="flex items-center gap-4 text-sm">
|
||||||
<span className="font-medium">Memory Bank:</span>
|
<Image src="/logo.png" alt="Hindsight" width={40} height={40} className="h-10 w-auto" unoptimized />
|
||||||
|
<div className="h-8 w-px bg-border" />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-[300px] justify-between font-bold border-2 border-primary"
|
className="w-[250px] justify-between font-bold border-2 border-primary"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
Loading...
|
Loading...
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<a
|
||||||
|
href="https://github.com/vectorize-io/hindsight"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-accent transition-colors text-muted-foreground"
|
||||||
|
>
|
||||||
|
<Github className="h-5 w-5" />
|
||||||
|
<span className="text-sm font-medium">GitHub</span>
|
||||||
|
</a>
|
||||||
|
<div className="h-8 w-px bg-border" />
|
||||||
|
<Button variant="ghost" size="icon" className="h-9 w-9" disabled>
|
||||||
|
<Moon className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||||
import { client } from '@/lib/api';
|
import { client } from '@/lib/api';
|
||||||
import { useBank } from '@/lib/bank-context';
|
import { useBank } from '@/lib/bank-context';
|
||||||
import cytoscape from 'cytoscape';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Copy, Check, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, Eye, EyeOff } from 'lucide-react';
|
||||||
import { Copy, Check, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { MemoryDetailPanel } from './memory-detail-panel';
|
import { MemoryDetailPanel } from './memory-detail-panel';
|
||||||
|
import { Graph2D, convertHindsightGraphData, GraphNode } from './graph-2d';
|
||||||
|
|
||||||
type FactType = 'world' | 'experience' | 'opinion';
|
type FactType = 'world' | 'experience' | 'opinion';
|
||||||
type ViewMode = 'graph' | 'table' | 'timeline';
|
type ViewMode = 'graph' | 'table' | 'timeline';
|
||||||
|
|
@ -23,16 +25,41 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('graph');
|
const [viewMode, setViewMode] = useState<ViewMode>('graph');
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [nodeLimit, setNodeLimit] = useState(50);
|
|
||||||
const [layout, setLayout] = useState('circle');
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
||||||
const [selectedTableMemory, setSelectedTableMemory] = useState<any>(null);
|
const [selectedTableMemory, setSelectedTableMemory] = useState<any>(null);
|
||||||
const itemsPerPage = 100;
|
const itemsPerPage = 100;
|
||||||
const cyRef = useRef<any>(null);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
// Graph controls state
|
||||||
|
const [showLabels, setShowLabels] = useState(true);
|
||||||
|
const [maxNodes, setMaxNodes] = useState<number | undefined>(50);
|
||||||
|
const [showControlPanel, setShowControlPanel] = useState(true);
|
||||||
|
const [visibleLinkTypes, setVisibleLinkTypes] = useState<Set<string>>(new Set(['semantic', 'temporal', 'entity', 'causal']));
|
||||||
|
|
||||||
|
const toggleLinkType = (type: string) => {
|
||||||
|
setVisibleLinkTypes(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(type)) {
|
||||||
|
next.delete(type);
|
||||||
|
} else {
|
||||||
|
next.add(type);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Esc key handler to deselect graph node
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && selectedGraphNode) {
|
||||||
|
setSelectedGraphNode(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [selectedGraphNode]);
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
const copyToClipboard = async (text: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -68,118 +95,98 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderGraph = () => {
|
// Filter table rows based on search query (text only)
|
||||||
if (!data || !containerRef.current || !data.nodes || !data.edges) return;
|
const filteredTableRows = useMemo(() => {
|
||||||
|
if (!data?.table_rows) return [];
|
||||||
|
if (!searchQuery) return data.table_rows;
|
||||||
|
|
||||||
if (cyRef.current) {
|
const query = searchQuery.toLowerCase();
|
||||||
cyRef.current.destroy();
|
return data.table_rows.filter((row: any) =>
|
||||||
}
|
row.text?.toLowerCase().includes(query)
|
||||||
|
|
||||||
const limitedNodes = (data.nodes || []).slice(0, nodeLimit);
|
|
||||||
const nodeIds = new Set(limitedNodes.map((n: any) => n.data.id));
|
|
||||||
const limitedEdges = (data.edges || []).filter((e: any) =>
|
|
||||||
nodeIds.has(e.data.source) && nodeIds.has(e.data.target)
|
|
||||||
);
|
);
|
||||||
|
}, [data, searchQuery]);
|
||||||
|
|
||||||
const layouts: any = {
|
// Get filtered node IDs for graph filtering
|
||||||
circle: {
|
const filteredNodeIds = useMemo(() => {
|
||||||
name: 'circle',
|
return new Set(filteredTableRows.map((row: any) => row.id));
|
||||||
animate: false,
|
}, [filteredTableRows]);
|
||||||
radius: 300,
|
|
||||||
spacingFactor: 1.5,
|
// Helper to get normalized link type
|
||||||
},
|
const getLinkTypeCategory = (type: string | undefined): string => {
|
||||||
grid: {
|
if (!type) return 'semantic';
|
||||||
name: 'grid',
|
if (type === 'semantic' || type === 'temporal' || type === 'entity') return type;
|
||||||
animate: false,
|
if (['causes', 'caused_by', 'enables', 'prevents'].includes(type)) return 'causal';
|
||||||
rows: Math.ceil(Math.sqrt(limitedNodes.length)),
|
return 'semantic';
|
||||||
cols: Math.ceil(Math.sqrt(limitedNodes.length)),
|
|
||||||
spacingFactor: 2,
|
|
||||||
},
|
|
||||||
cose: {
|
|
||||||
name: 'cose',
|
|
||||||
animate: false,
|
|
||||||
nodeRepulsion: 15000,
|
|
||||||
idealEdgeLength: 150,
|
|
||||||
edgeElasticity: 100,
|
|
||||||
nestingFactor: 1.2,
|
|
||||||
gravity: 1,
|
|
||||||
numIter: 1000,
|
|
||||||
initialTemp: 200,
|
|
||||||
coolingFactor: 0.95,
|
|
||||||
minTemp: 1.0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
cyRef.current = cytoscape({
|
// Convert data for Graph2D with filtering
|
||||||
container: containerRef.current,
|
const graph2DData = useMemo(() => {
|
||||||
elements: [
|
if (!data) return { nodes: [], links: [] };
|
||||||
...limitedNodes.map((n: any) => ({ data: n.data })),
|
const fullData = convertHindsightGraphData(data);
|
||||||
...limitedEdges.map((e: any) => ({ data: e.data })),
|
|
||||||
],
|
let nodes = fullData.nodes;
|
||||||
style: [
|
let links = fullData.links;
|
||||||
{
|
|
||||||
selector: 'node',
|
// Filter nodes based on search query
|
||||||
style: {
|
if (searchQuery) {
|
||||||
'background-color': 'data(color)' as any,
|
const filteredNodes = fullData.nodes.filter(node => filteredNodeIds.has(node.id));
|
||||||
label: 'data(label)' as any,
|
const filteredNodeIdSet = new Set(filteredNodes.map(n => n.id));
|
||||||
'text-valign': 'center',
|
nodes = filteredNodes;
|
||||||
'text-halign': 'center',
|
links = fullData.links.filter(link =>
|
||||||
'font-size': '10px',
|
filteredNodeIdSet.has(link.source) && filteredNodeIdSet.has(link.target)
|
||||||
'font-weight': 'bold',
|
);
|
||||||
'text-wrap': 'wrap',
|
}
|
||||||
'text-max-width': '100px',
|
|
||||||
width: 40,
|
// Filter links based on visible link types
|
||||||
height: 40,
|
links = links.filter(link => {
|
||||||
'border-width': 2,
|
const category = getLinkTypeCategory(link.type);
|
||||||
'border-color': '#333',
|
return visibleLinkTypes.has(category);
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: 'edge',
|
|
||||||
style: {
|
|
||||||
width: 1,
|
|
||||||
'line-color': 'data(color)' as any,
|
|
||||||
'line-style': 'data(lineStyle)' as any,
|
|
||||||
'target-arrow-shape': 'triangle',
|
|
||||||
'target-arrow-color': 'data(color)' as any,
|
|
||||||
'curve-style': 'bezier',
|
|
||||||
opacity: 0.6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
selector: 'node:selected',
|
|
||||||
style: {
|
|
||||||
'border-width': 4,
|
|
||||||
'border-color': '#000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as any,
|
|
||||||
layout: layouts[layout] || layouts.circle,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add click handler for nodes
|
return { nodes, links };
|
||||||
cyRef.current.on('tap', 'node', (evt: any) => {
|
}, [data, searchQuery, filteredNodeIds, visibleLinkTypes]);
|
||||||
const nodeId = evt.target.id();
|
|
||||||
// Find the corresponding table row data
|
// Calculate link stats for display
|
||||||
const nodeData = data.table_rows?.find((row: any) => row.id === nodeId);
|
const linkStats = useMemo(() => {
|
||||||
|
let semantic = 0, temporal = 0, entity = 0, causal = 0, total = 0;
|
||||||
|
const otherTypes: Record<string, number> = {};
|
||||||
|
graph2DData.links.forEach(l => {
|
||||||
|
total++;
|
||||||
|
const type = l.type || 'unknown';
|
||||||
|
if (type === 'semantic') semantic++;
|
||||||
|
else if (type === 'temporal') temporal++;
|
||||||
|
else if (type === 'entity') entity++;
|
||||||
|
else if (type === 'causes' || type === 'caused_by' || type === 'enables' || type === 'prevents') causal++;
|
||||||
|
else {
|
||||||
|
otherTypes[type] = (otherTypes[type] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Graph link stats:', { semantic, temporal, entity, causal, total });
|
||||||
|
if (Object.keys(otherTypes).length > 0) {
|
||||||
|
console.log('Other link types:', otherTypes);
|
||||||
|
}
|
||||||
|
return { semantic, temporal, entity, causal, total, otherTypes };
|
||||||
|
}, [graph2DData]);
|
||||||
|
|
||||||
|
// Handle node click in graph - show in panel
|
||||||
|
const handleGraphNodeClick = useCallback((node: GraphNode) => {
|
||||||
|
const nodeData = data?.table_rows?.find((row: any) => row.id === node.id);
|
||||||
if (nodeData) {
|
if (nodeData) {
|
||||||
setSelectedGraphNode(nodeData);
|
setSelectedGraphNode(nodeData);
|
||||||
}
|
}
|
||||||
});
|
}, [data]);
|
||||||
|
|
||||||
// Click on background to deselect
|
// Memoized color functions to prevent graph re-initialization
|
||||||
cyRef.current.on('tap', (evt: any) => {
|
// Uses brand colors: primary blue (#0074d9), teal (#009296), amber for entity, purple for causal
|
||||||
if (evt.target === cyRef.current) {
|
const nodeColorFn = useCallback((node: GraphNode) => node.color || '#0074d9', []);
|
||||||
setSelectedGraphNode(null);
|
const linkColorFn = useCallback((link: any) => {
|
||||||
|
if (link.type === 'temporal') return '#009296'; // Brand teal
|
||||||
|
if (link.type === 'entity') return '#f59e0b'; // Amber
|
||||||
|
if (link.type === 'causes' || link.type === 'caused_by' || link.type === 'enables' || link.type === 'prevents') {
|
||||||
|
return '#8b5cf6'; // Purple for causal
|
||||||
}
|
}
|
||||||
});
|
return '#0074d9'; // Brand primary blue for semantic
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (viewMode === 'graph' && data) {
|
|
||||||
renderGraph();
|
|
||||||
}
|
|
||||||
}, [viewMode, data, nodeLimit, layout]);
|
|
||||||
|
|
||||||
// Reset to first page when search query changes
|
// Reset to first page when search query changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -204,9 +211,20 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
</div>
|
</div>
|
||||||
) : data ? (
|
) : data ? (
|
||||||
<>
|
<>
|
||||||
|
{/* Always visible filter */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Filter memories by text..."
|
||||||
|
className="max-w-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{data.total_units} total memories
|
{searchQuery ? `${filteredTableRows.length} of ${data.total_units} memories` : `${data.total_units} total memories`}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
||||||
<button
|
<button
|
||||||
|
|
@ -243,134 +261,214 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === 'graph' && (
|
{viewMode === 'graph' && (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-0">
|
||||||
<div className={`relative transition-all ${selectedGraphNode ? 'w-2/3' : 'w-full'}`}>
|
{/* Graph */}
|
||||||
<div className="p-4 bg-card border-b-2 border-primary flex gap-4 items-center flex-wrap">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<Graph2D
|
||||||
<label className="font-semibold text-card-foreground">Limit nodes:</label>
|
data={graph2DData}
|
||||||
<Input
|
height={700}
|
||||||
type="number"
|
showLabels={showLabels}
|
||||||
value={nodeLimit}
|
onNodeClick={handleGraphNodeClick}
|
||||||
onChange={(e) => setNodeLimit(parseInt(e.target.value))}
|
maxNodes={maxNodes}
|
||||||
min="10"
|
nodeColorFn={nodeColorFn}
|
||||||
max="1000"
|
linkColorFn={linkColorFn}
|
||||||
step="10"
|
|
||||||
className="w-20"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="font-semibold text-card-foreground">Layout:</label>
|
|
||||||
<Select value={layout} onValueChange={setLayout}>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="circle">Circle (fast)</SelectItem>
|
|
||||||
<SelectItem value="grid">Grid (fast)</SelectItem>
|
|
||||||
<SelectItem value="cose">Force-directed (slow)</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground ml-auto">
|
|
||||||
Click on a node to view details
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ref={containerRef} className="w-full h-[800px] bg-background" />
|
|
||||||
<div className="absolute top-20 left-5 bg-card p-4 border-2 border-primary rounded-lg shadow-lg max-w-[250px]">
|
|
||||||
<h3 className="font-bold mb-2 border-b-2 border-primary pb-1 text-card-foreground">Legend</h3>
|
|
||||||
<h4 className="font-bold mt-2 mb-1 text-sm text-card-foreground">Link Types:</h4>
|
|
||||||
<div className="flex items-center my-2">
|
|
||||||
<div className="w-8 h-0.5 mr-2.5 bg-cyan-500 border-t border-dashed border-cyan-500" />
|
|
||||||
<span className="text-sm"><strong>Temporal</strong></span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center my-2">
|
|
||||||
<div className="w-8 h-0.5 mr-2.5 bg-pink-500" />
|
|
||||||
<span className="text-sm"><strong>Semantic</strong></span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center my-2">
|
|
||||||
<div className="w-8 h-0.5 mr-2.5 bg-yellow-500" />
|
|
||||||
<span className="text-sm"><strong>Entity</strong></span>
|
|
||||||
</div>
|
|
||||||
<h4 className="font-bold mt-2 mb-1 text-sm">Nodes:</h4>
|
|
||||||
<div className="flex items-center my-2">
|
|
||||||
<div className="w-5 h-5 mr-2.5 bg-gray-300 border border-gray-500 rounded" />
|
|
||||||
<span className="text-sm">No entities</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center my-2">
|
|
||||||
<div className="w-5 h-5 mr-2.5 bg-blue-300 border border-gray-500 rounded" />
|
|
||||||
<span className="text-sm">1 entity</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center my-2">
|
|
||||||
<div className="w-5 h-5 mr-2.5 bg-blue-500 border border-gray-500 rounded" />
|
|
||||||
<span className="text-sm">2+ entities</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Memory Detail Panel for Graph View - Fixed on Right */}
|
{/* Right Toggle Button */}
|
||||||
{selectedGraphNode && (
|
<button
|
||||||
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l-2 border-primary shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
|
onClick={() => setShowControlPanel(!showControlPanel)}
|
||||||
|
className="flex-shrink-0 w-5 h-[700px] bg-transparent hover:bg-muted/50 flex items-center justify-center transition-colors"
|
||||||
|
title={showControlPanel ? 'Hide panel' : 'Show panel'}
|
||||||
|
>
|
||||||
|
{showControlPanel ? (
|
||||||
|
<ChevronRight className="w-3 h-3 text-muted-foreground/60" />
|
||||||
|
) : (
|
||||||
|
<ChevronLeft className="w-3 h-3 text-muted-foreground/60" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Right Panel - Legend/Controls OR Memory Details */}
|
||||||
|
<div className={`${showControlPanel ? 'w-80' : 'w-0'} transition-all duration-300 overflow-hidden flex-shrink-0`}>
|
||||||
|
<div className="w-80 h-[700px] bg-card border-l border-border overflow-y-auto">
|
||||||
|
{selectedGraphNode ? (
|
||||||
|
/* Memory Detail View */
|
||||||
<MemoryDetailPanel
|
<MemoryDetailPanel
|
||||||
memory={selectedGraphNode}
|
memory={selectedGraphNode}
|
||||||
onClose={() => setSelectedGraphNode(null)}
|
onClose={() => setSelectedGraphNode(null)}
|
||||||
inPanel
|
inPanel
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
/* Legend & Controls View */
|
||||||
|
<div className="p-4 space-y-5">
|
||||||
|
{/* Legend & Stats */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold mb-3 text-foreground">Graph</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Nodes */}
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: '#0074d9' }} />
|
||||||
|
<span className="text-foreground">Nodes</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-foreground">
|
||||||
|
{Math.min(maxNodes ?? graph2DData.nodes.length, graph2DData.nodes.length)}/{graph2DData.nodes.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mt-2 mb-1">Links ({linkStats.total}) <span className="text-muted-foreground/60">· click to filter</span></div>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleLinkType('semantic')}
|
||||||
|
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||||
|
visibleLinkTypes.has('semantic') ? 'hover:bg-muted' : 'opacity-40 hover:opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-0.5 bg-[#0074d9]" />
|
||||||
|
<span className="text-foreground">Semantic</span>
|
||||||
|
</div>
|
||||||
|
<span className={`font-mono ${linkStats.semantic === 0 ? 'text-destructive' : 'text-foreground'}`}>
|
||||||
|
{linkStats.semantic}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleLinkType('temporal')}
|
||||||
|
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||||
|
visibleLinkTypes.has('temporal') ? 'hover:bg-muted' : 'opacity-40 hover:opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-0.5 bg-[#009296]" />
|
||||||
|
<span className="text-foreground">Temporal</span>
|
||||||
|
</div>
|
||||||
|
<span className={`font-mono ${linkStats.temporal === 0 ? 'text-destructive' : 'text-foreground'}`}>
|
||||||
|
{linkStats.temporal}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleLinkType('entity')}
|
||||||
|
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||||
|
visibleLinkTypes.has('entity') ? 'hover:bg-muted' : 'opacity-40 hover:opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-0.5 bg-[#f59e0b]" />
|
||||||
|
<span className="text-foreground">Entity</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-foreground">{linkStats.entity}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleLinkType('causal')}
|
||||||
|
className={`w-full flex items-center justify-between text-sm px-2 py-1 rounded transition-all ${
|
||||||
|
visibleLinkTypes.has('causal') ? 'hover:bg-muted' : 'opacity-40 hover:opacity-60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-0.5 bg-[#8b5cf6]" />
|
||||||
|
<span className="text-foreground">Causal</span>
|
||||||
|
</div>
|
||||||
|
<span className={`font-mono ${linkStats.causal === 0 ? 'text-muted-foreground' : 'text-foreground'}`}>
|
||||||
|
{linkStats.causal}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{Object.entries(linkStats.otherTypes || {}).map(([type, count]) => (
|
||||||
|
<div key={type} className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground capitalize ml-6">{type}</span>
|
||||||
|
<span className="font-mono text-muted-foreground">{count as number}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border" />
|
||||||
|
|
||||||
|
{/* Controls Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold mb-3 text-foreground">Display</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="show-labels" className="text-sm text-foreground">Show labels</Label>
|
||||||
|
<Switch
|
||||||
|
id="show-labels"
|
||||||
|
checked={showLabels}
|
||||||
|
onCheckedChange={setShowLabels}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border" />
|
||||||
|
|
||||||
|
{/* Limits Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold mb-3 text-foreground">Performance</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<Label className="text-sm text-foreground">Max nodes</Label>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{maxNodes ?? 'All'} / {graph2DData.nodes.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
value={[maxNodes ?? graph2DData.nodes.length]}
|
||||||
|
min={10}
|
||||||
|
max={Math.max(graph2DData.nodes.length, 10)}
|
||||||
|
step={10}
|
||||||
|
onValueChange={([v]) => setMaxNodes(v >= graph2DData.nodes.length ? undefined : v)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
All links between visible nodes are shown.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-border" />
|
||||||
|
|
||||||
|
{/* Hint */}
|
||||||
|
<div className="text-xs text-muted-foreground/60 text-center pt-2">
|
||||||
|
Click a node to see details
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewMode === 'table' && (
|
{viewMode === 'table' && (
|
||||||
<div>
|
<div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="px-5 mb-4">
|
<div className="pb-4">
|
||||||
<Input
|
{filteredTableRows.length > 0 ? (
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder="Search memories (text, context, ID)..."
|
|
||||||
className="max-w-2xl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="px-5 pb-5">
|
|
||||||
{data.table_rows && data.table_rows.length > 0 ? (
|
|
||||||
(() => {
|
(() => {
|
||||||
const filteredRows = data.table_rows.filter((row: any) => {
|
const totalPages = Math.ceil(filteredTableRows.length / itemsPerPage);
|
||||||
if (!searchQuery) return true;
|
|
||||||
const query = searchQuery.toLowerCase();
|
|
||||||
return (
|
|
||||||
row.text?.toLowerCase().includes(query) ||
|
|
||||||
row.context?.toLowerCase().includes(query) ||
|
|
||||||
row.id?.toLowerCase().includes(query)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalPages = Math.ceil(filteredRows.length / itemsPerPage);
|
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
const endIndex = startIndex + itemsPerPage;
|
const endIndex = startIndex + itemsPerPage;
|
||||||
const paginatedRows = filteredRows.slice(startIndex, endIndex);
|
const paginatedRows = filteredTableRows.slice(startIndex, endIndex);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
<Table>
|
<Table className="table-fixed">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="bg-muted/50">
|
<TableRow className="bg-muted/50">
|
||||||
<TableHead className="w-[80px]">ID</TableHead>
|
<TableHead className="w-[45%]">Memory</TableHead>
|
||||||
<TableHead>Text</TableHead>
|
<TableHead className="w-[20%]">Entities</TableHead>
|
||||||
<TableHead className="w-[150px]">Context</TableHead>
|
<TableHead className="w-[15%]">Occurred</TableHead>
|
||||||
<TableHead className="w-[100px]">Occurred</TableHead>
|
<TableHead className="w-[15%]">Mentioned</TableHead>
|
||||||
<TableHead className="w-[100px]">Mentioned</TableHead>
|
<TableHead className="w-[5%]"></TableHead>
|
||||||
<TableHead className="w-[60px]">Actions</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{paginatedRows.map((row: any, idx: number) => {
|
{paginatedRows.map((row: any, idx: number) => {
|
||||||
const occurredDisplay = row.occurred_start
|
const occurredDisplay = row.occurred_start
|
||||||
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
? new Date(row.occurred_start).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||||
: null;
|
: null;
|
||||||
const mentionedDisplay = row.mentioned_at
|
const mentionedDisplay = row.mentioned_at
|
||||||
? new Date(row.mentioned_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
? new Date(row.mentioned_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -381,46 +479,40 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
selectedTableMemory?.id === row.id ? 'bg-primary/10' : ''
|
selectedTableMemory?.id === row.id ? 'bg-primary/10' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<TableCell className="font-mono text-xs text-muted-foreground" title={row.id}>
|
<TableCell className="py-2">
|
||||||
{row.id?.substring(0, 8)}...
|
<div className="line-clamp-2 text-sm leading-snug">{row.text}</div>
|
||||||
|
{row.context && (
|
||||||
|
<div className="text-xs text-muted-foreground mt-0.5 truncate">{row.context}</div>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className="py-2">
|
||||||
<div className="line-clamp-2 text-sm">{row.text}</div>
|
{row.entities ? (
|
||||||
{row.entities && (
|
<div className="flex gap-1 flex-wrap">
|
||||||
<div className="flex gap-1 mt-1 flex-wrap">
|
{row.entities.split(', ').slice(0, 2).map((entity: string, i: number) => (
|
||||||
{row.entities.split(', ').slice(0, 3).map((entity: string, i: number) => (
|
<span
|
||||||
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-secondary text-secondary-foreground">
|
key={i}
|
||||||
|
className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium"
|
||||||
|
>
|
||||||
{entity}
|
{entity}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{row.entities.split(', ').length > 3 && (
|
{row.entities.split(', ').length > 2 && (
|
||||||
<span className="text-[10px] text-muted-foreground">
|
<span className="text-[10px] text-muted-foreground">
|
||||||
+{row.entities.split(', ').length - 3}
|
+{row.entities.split(', ').length - 2}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">-</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs text-muted-foreground truncate max-w-[150px]" title={row.context}>
|
<TableCell className="text-xs py-2">
|
||||||
{row.context || '-'}
|
{occurredDisplay || <span className="text-muted-foreground">-</span>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="text-xs py-2">
|
||||||
{occurredDisplay ? (
|
{mentionedDisplay || <span className="text-muted-foreground">-</span>}
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="h-3 w-3" />
|
|
||||||
{occurredDisplay}
|
|
||||||
</span>
|
|
||||||
) : '-'}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs">
|
<TableCell className="py-2">
|
||||||
{mentionedDisplay ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="h-3 w-3" />
|
|
||||||
{mentionedDisplay}
|
|
||||||
</span>
|
|
||||||
) : '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -428,7 +520,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-7 w-7 p-0"
|
className="h-6 w-6 p-0"
|
||||||
title="Copy ID"
|
title="Copy ID"
|
||||||
>
|
>
|
||||||
{copiedId === row.id ? (
|
{copiedId === row.id ? (
|
||||||
|
|
@ -447,9 +539,9 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
|
|
||||||
{/* Pagination Controls */}
|
{/* Pagination Controls */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
<div className="flex items-center justify-between mt-3 pt-3 border-t">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
Showing {startIndex + 1} to {Math.min(endIndex, filteredRows.length)} of {filteredRows.length}
|
{startIndex + 1}-{Math.min(endIndex, filteredTableRows.length)} of {filteredTableRows.length}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -457,20 +549,20 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCurrentPage(1)}
|
onClick={() => setCurrentPage(1)}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="h-8 w-8 p-0"
|
className="h-7 w-7 p-0"
|
||||||
>
|
>
|
||||||
<ChevronsLeft className="h-4 w-4" />
|
<ChevronsLeft className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="h-8 w-8 p-0"
|
className="h-7 w-7 p-0"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm px-3">
|
<span className="text-xs px-2">
|
||||||
{currentPage} / {totalPages}
|
{currentPage} / {totalPages}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -478,18 +570,18 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
className="h-8 w-8 p-0"
|
className="h-7 w-7 p-0"
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setCurrentPage(totalPages)}
|
onClick={() => setCurrentPage(totalPages)}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
className="h-8 w-8 p-0"
|
className="h-7 w-7 p-0"
|
||||||
>
|
>
|
||||||
<ChevronsRight className="h-4 w-4" />
|
<ChevronsRight className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -499,7 +591,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
})()
|
})()
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
{data.table_rows ? 'No memories match your search' : 'No memories found'}
|
{data.table_rows?.length > 0 ? 'No memories match your filter' : 'No memories found'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -519,7 +611,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewMode === 'timeline' && (
|
{viewMode === 'timeline' && (
|
||||||
<TimelineView data={data} />
|
<TimelineView data={data} filteredRows={filteredTableRows} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -537,17 +629,17 @@ export function DataView({ factType }: DataViewProps) {
|
||||||
// Timeline View Component - Custom compact timeline with zoom and navigation
|
// Timeline View Component - Custom compact timeline with zoom and navigation
|
||||||
type Granularity = 'year' | 'month' | 'week' | 'day';
|
type Granularity = 'year' | 'month' | 'week' | 'day';
|
||||||
|
|
||||||
function TimelineView({ data }: { data: any }) {
|
function TimelineView({ data, filteredRows }: { data: any; filteredRows: any[] }) {
|
||||||
const [selectedItem, setSelectedItem] = useState<any>(null);
|
const [selectedItem, setSelectedItem] = useState<any>(null);
|
||||||
const [granularity, setGranularity] = useState<Granularity>('month');
|
const [granularity, setGranularity] = useState<Granularity>('month');
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Filter and sort items that have occurred_start dates
|
// Filter and sort items that have occurred_start dates (using filtered data)
|
||||||
const { sortedItems, itemsWithoutDates } = useMemo(() => {
|
const { sortedItems, itemsWithoutDates } = useMemo(() => {
|
||||||
if (!data?.table_rows) return { sortedItems: [], itemsWithoutDates: [] };
|
if (!filteredRows || filteredRows.length === 0) return { sortedItems: [], itemsWithoutDates: [] };
|
||||||
|
|
||||||
const withDates = data.table_rows
|
const withDates = filteredRows
|
||||||
.filter((row: any) => row.occurred_start)
|
.filter((row: any) => row.occurred_start)
|
||||||
.sort((a: any, b: any) => {
|
.sort((a: any, b: any) => {
|
||||||
const dateA = new Date(a.occurred_start).getTime();
|
const dateA = new Date(a.occurred_start).getTime();
|
||||||
|
|
@ -555,19 +647,10 @@ function TimelineView({ data }: { data: any }) {
|
||||||
return dateA - dateB;
|
return dateA - dateB;
|
||||||
});
|
});
|
||||||
|
|
||||||
const withoutDates = data.table_rows.filter((row: any) => !row.occurred_start);
|
const withoutDates = filteredRows.filter((row: any) => !row.occurred_start);
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
console.log('Timeline data:', {
|
|
||||||
total: data.table_rows.length,
|
|
||||||
withDates: withDates.length,
|
|
||||||
withoutDates: withoutDates.length,
|
|
||||||
sampleWithDate: withDates[0],
|
|
||||||
sampleWithoutDate: withoutDates[0]
|
|
||||||
});
|
|
||||||
|
|
||||||
return { sortedItems: withDates, itemsWithoutDates: withoutDates };
|
return { sortedItems: withDates, itemsWithoutDates: withoutDates };
|
||||||
}, [data]);
|
}, [filteredRows]);
|
||||||
|
|
||||||
// Group items by granularity
|
// Group items by granularity
|
||||||
const timelineGroups = useMemo(() => {
|
const timelineGroups = useMemo(() => {
|
||||||
|
|
@ -697,9 +780,9 @@ function TimelineView({ data }: { data: any }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 px-4">
|
<div className="px-4">
|
||||||
{/* Timeline */}
|
{/* Timeline */}
|
||||||
<div className={`transition-all ${selectedItem ? 'w-2/3' : 'w-full'}`}>
|
<div>
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="flex items-center justify-between mb-3 gap-4">
|
<div className="flex items-center justify-between mb-3 gap-4">
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
|
|
@ -853,7 +936,7 @@ function TimelineView({ data }: { data: any }) {
|
||||||
{item.entities && (
|
{item.entities && (
|
||||||
<div className="flex gap-1 mt-1 flex-wrap">
|
<div className="flex gap-1 mt-1 flex-wrap">
|
||||||
{item.entities.split(', ').slice(0, 3).map((entity: string, i: number) => (
|
{item.entities.split(', ').slice(0, 3).map((entity: string, i: number) => (
|
||||||
<span key={i} className="text-[9px] px-1 py-0.5 rounded bg-secondary text-secondary-foreground">
|
<span key={i} className="text-[9px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium">
|
||||||
{entity}
|
{entity}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
599
hindsight-control-plane/src/components/graph-2d.tsx
Normal file
599
hindsight-control-plane/src/components/graph-2d.tsx
Normal file
|
|
@ -0,0 +1,599 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRef, useEffect, useState, useMemo } from 'react';
|
||||||
|
import cytoscape, { Core, NodeSingular } from 'cytoscape';
|
||||||
|
|
||||||
|
// Hook to detect dark mode
|
||||||
|
function useIsDarkMode() {
|
||||||
|
const [isDark, setIsDark] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkDark = () => {
|
||||||
|
setIsDark(document.documentElement.classList.contains('dark'));
|
||||||
|
};
|
||||||
|
|
||||||
|
checkDark();
|
||||||
|
|
||||||
|
// Watch for theme changes
|
||||||
|
const observer = new MutationObserver(checkDark);
|
||||||
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Types & Interfaces
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface GraphNode {
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
color?: string;
|
||||||
|
size?: number;
|
||||||
|
group?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphLink {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
color?: string;
|
||||||
|
width?: number;
|
||||||
|
type?: string;
|
||||||
|
entity?: string;
|
||||||
|
weight?: number;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphData {
|
||||||
|
nodes: GraphNode[];
|
||||||
|
links: GraphLink[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Graph2DProps {
|
||||||
|
data: GraphData;
|
||||||
|
height?: number;
|
||||||
|
showLabels?: boolean;
|
||||||
|
onNodeClick?: (node: GraphNode) => void;
|
||||||
|
onNodeHover?: (node: GraphNode | null) => void;
|
||||||
|
nodeColorFn?: (node: GraphNode) => string;
|
||||||
|
nodeSizeFn?: (node: GraphNode) => number;
|
||||||
|
linkColorFn?: (link: GraphLink) => string;
|
||||||
|
linkWidthFn?: (link: GraphLink) => number;
|
||||||
|
maxNodes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Default Values
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Brand colors
|
||||||
|
const BRAND_PRIMARY = '#0074d9';
|
||||||
|
const BRAND_TEAL = '#009296';
|
||||||
|
const LINK_SEMANTIC = '#0074d9'; // Primary blue for semantic
|
||||||
|
const LINK_TEMPORAL = '#009296'; // Teal for temporal
|
||||||
|
const LINK_ENTITY = '#f59e0b'; // Amber for entity
|
||||||
|
|
||||||
|
const DEFAULT_NODE_COLOR = BRAND_PRIMARY;
|
||||||
|
const DEFAULT_LINK_COLOR = LINK_SEMANTIC;
|
||||||
|
const DEFAULT_NODE_SIZE = 20;
|
||||||
|
const DEFAULT_LINK_WIDTH = 1;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Component
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function Graph2D({
|
||||||
|
data,
|
||||||
|
height = 600,
|
||||||
|
showLabels = true,
|
||||||
|
onNodeClick,
|
||||||
|
onNodeHover,
|
||||||
|
nodeColorFn,
|
||||||
|
nodeSizeFn,
|
||||||
|
linkColorFn,
|
||||||
|
linkWidthFn,
|
||||||
|
maxNodes,
|
||||||
|
}: Graph2DProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const cyRef = useRef<Core | null>(null);
|
||||||
|
const [hoveredNode, setHoveredNode] = useState<GraphNode | null>(null);
|
||||||
|
const [hoveredLink, setHoveredLink] = useState<GraphLink | null>(null);
|
||||||
|
const [linkTooltipPos, setLinkTooltipPos] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
// Use refs to store callbacks and data to prevent re-renders from resetting the graph
|
||||||
|
const onNodeClickRef = useRef(onNodeClick);
|
||||||
|
const onNodeHoverRef = useRef(onNodeHover);
|
||||||
|
const fullDataRef = useRef(data);
|
||||||
|
const nodeColorFnRef = useRef(nodeColorFn);
|
||||||
|
const linkColorFnRef = useRef(linkColorFn);
|
||||||
|
onNodeClickRef.current = onNodeClick;
|
||||||
|
onNodeHoverRef.current = onNodeHover;
|
||||||
|
fullDataRef.current = data;
|
||||||
|
nodeColorFnRef.current = nodeColorFn;
|
||||||
|
linkColorFnRef.current = linkColorFn;
|
||||||
|
|
||||||
|
// Transform and limit data - only limit nodes, show ALL links between visible nodes
|
||||||
|
const graphData = useMemo(() => {
|
||||||
|
let nodes = [...data.nodes];
|
||||||
|
|
||||||
|
// Limit nodes if needed
|
||||||
|
if (maxNodes && nodes.length > maxNodes) {
|
||||||
|
nodes = nodes.slice(0, maxNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show ALL links between visible nodes (no random link limiting)
|
||||||
|
const nodeIds = new Set(nodes.map(n => n.id));
|
||||||
|
const links = data.links.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
|
||||||
|
|
||||||
|
return { nodes, links };
|
||||||
|
}, [data, maxNodes]);
|
||||||
|
|
||||||
|
// Convert to Cytoscape format
|
||||||
|
const cyElements = useMemo(() => {
|
||||||
|
const nodes = graphData.nodes.map(node => ({
|
||||||
|
data: {
|
||||||
|
id: node.id,
|
||||||
|
label: node.label || node.id.substring(0, 8),
|
||||||
|
color: nodeColorFn ? nodeColorFn(node) : (node.color || DEFAULT_NODE_COLOR),
|
||||||
|
size: nodeSizeFn ? nodeSizeFn(node) : (node.size || DEFAULT_NODE_SIZE),
|
||||||
|
originalNode: node,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const edges = graphData.links.map((link, idx) => ({
|
||||||
|
data: {
|
||||||
|
id: `edge-${idx}`,
|
||||||
|
source: link.source,
|
||||||
|
target: link.target,
|
||||||
|
color: linkColorFn ? linkColorFn(link) : (link.color || DEFAULT_LINK_COLOR),
|
||||||
|
width: linkWidthFn ? linkWidthFn(link) : (link.width || DEFAULT_LINK_WIDTH),
|
||||||
|
type: link.type,
|
||||||
|
entity: link.entity,
|
||||||
|
weight: link.weight,
|
||||||
|
originalLink: link,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [...nodes, ...edges];
|
||||||
|
}, [graphData, nodeColorFn, nodeSizeFn, linkColorFn, linkWidthFn]);
|
||||||
|
|
||||||
|
// Initialize Cytoscape
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
// Handle empty data case
|
||||||
|
if (cyElements.length === 0) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Theme-aware colors
|
||||||
|
const textColor = isDarkMode ? '#ffffff' : '#1f2937';
|
||||||
|
const textBgColor = isDarkMode ? 'rgba(0,0,0,0.8)' : 'rgba(255,255,255,0.9)';
|
||||||
|
const borderColor = isDarkMode ? '#ffffff' : '#374151';
|
||||||
|
|
||||||
|
const cy = cytoscape({
|
||||||
|
container: containerRef.current,
|
||||||
|
elements: cyElements,
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
selector: 'node',
|
||||||
|
style: {
|
||||||
|
'background-fill': 'radial-gradient',
|
||||||
|
'background-gradient-stop-colors': ['#0074d9', '#005bb5'],
|
||||||
|
'background-gradient-stop-positions': ['0%', '100%'],
|
||||||
|
'width': 'data(size)',
|
||||||
|
'height': 'data(size)',
|
||||||
|
'label': showLabels ? 'data(label)' : '',
|
||||||
|
'color': textColor,
|
||||||
|
'text-valign': 'bottom',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'font-size': '8px',
|
||||||
|
'font-weight': 500,
|
||||||
|
'text-margin-y': 3,
|
||||||
|
'text-wrap': 'wrap',
|
||||||
|
'text-max-width': '80px',
|
||||||
|
'text-background-color': textBgColor,
|
||||||
|
'text-background-opacity': 0.9,
|
||||||
|
'text-background-padding': '2px',
|
||||||
|
'text-background-shape': 'roundrectangle',
|
||||||
|
'border-width': 0,
|
||||||
|
'z-index': 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'node:selected',
|
||||||
|
style: {
|
||||||
|
'border-width': 3,
|
||||||
|
'border-color': '#0074d9',
|
||||||
|
'border-opacity': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'node:active',
|
||||||
|
style: {
|
||||||
|
'overlay-opacity': 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
'width': 'data(width)',
|
||||||
|
'line-color': 'data(color)',
|
||||||
|
'target-arrow-color': 'data(color)',
|
||||||
|
'curve-style': 'bezier',
|
||||||
|
'opacity': isDarkMode ? 0.5 : 0.6,
|
||||||
|
'z-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'edge:selected',
|
||||||
|
style: {
|
||||||
|
'opacity': 1,
|
||||||
|
'width': 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Dimmed state for non-selected elements
|
||||||
|
{
|
||||||
|
selector: '.dimmed',
|
||||||
|
style: {
|
||||||
|
'opacity': 0.15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Highlighted state for selected node and neighbors
|
||||||
|
{
|
||||||
|
selector: 'node.highlighted',
|
||||||
|
style: {
|
||||||
|
'opacity': 1,
|
||||||
|
'border-width': 3,
|
||||||
|
'border-color': '#0074d9',
|
||||||
|
'border-opacity': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: 'edge.highlighted',
|
||||||
|
style: {
|
||||||
|
'opacity': 0.9,
|
||||||
|
'width': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layout: {
|
||||||
|
name: 'cose',
|
||||||
|
animate: false,
|
||||||
|
randomize: true,
|
||||||
|
nodeRepulsion: () => 100000,
|
||||||
|
idealEdgeLength: () => 300,
|
||||||
|
edgeElasticity: () => 20,
|
||||||
|
nestingFactor: 0.1,
|
||||||
|
gravity: 0.01,
|
||||||
|
numIter: 2500,
|
||||||
|
coolingFactor: 0.95,
|
||||||
|
minTemp: 1.0,
|
||||||
|
nodeOverlap: 20,
|
||||||
|
nodeDimensionsIncludeLabels: true,
|
||||||
|
padding: 50,
|
||||||
|
} as any,
|
||||||
|
minZoom: 0.1,
|
||||||
|
maxZoom: 5,
|
||||||
|
wheelSensitivity: 0.3,
|
||||||
|
});
|
||||||
|
|
||||||
|
cyRef.current = cy;
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
cy.on('tap', 'node', (evt) => {
|
||||||
|
const node = evt.target as NodeSingular;
|
||||||
|
const originalNode = node.data('originalNode') as GraphNode;
|
||||||
|
if (onNodeClickRef.current && originalNode) {
|
||||||
|
onNodeClickRef.current(originalNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find ALL connected nodes from full data (not just visible ones)
|
||||||
|
const fullData = fullDataRef.current;
|
||||||
|
const clickedNodeId = originalNode.id;
|
||||||
|
|
||||||
|
// Find all links connected to this node from full data
|
||||||
|
const connectedLinks = fullData.links.filter(
|
||||||
|
l => l.source === clickedNodeId || l.target === clickedNodeId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find all connected node IDs
|
||||||
|
const connectedNodeIds = new Set<string>();
|
||||||
|
connectedLinks.forEach(l => {
|
||||||
|
connectedNodeIds.add(l.source);
|
||||||
|
connectedNodeIds.add(l.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add any missing nodes to the graph
|
||||||
|
const existingNodeIds = new Set(cy.nodes().map(n => n.id()));
|
||||||
|
const nodesToAdd: any[] = [];
|
||||||
|
const edgesToAdd: any[] = [];
|
||||||
|
|
||||||
|
connectedNodeIds.forEach(nodeId => {
|
||||||
|
if (!existingNodeIds.has(nodeId)) {
|
||||||
|
const nodeData = fullData.nodes.find(n => n.id === nodeId);
|
||||||
|
if (nodeData) {
|
||||||
|
nodesToAdd.push({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: nodeData.id,
|
||||||
|
label: nodeData.label || nodeData.id.substring(0, 8),
|
||||||
|
color: nodeColorFnRef.current ? nodeColorFnRef.current(nodeData) : (nodeData.color || DEFAULT_NODE_COLOR),
|
||||||
|
size: nodeData.size || DEFAULT_NODE_SIZE,
|
||||||
|
originalNode: nodeData,
|
||||||
|
isTemporary: true, // Mark as temporarily added
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add missing edges
|
||||||
|
const existingEdgeIds = new Set(cy.edges().map(e => `${e.data('source')}-${e.data('target')}`));
|
||||||
|
connectedLinks.forEach((link, idx) => {
|
||||||
|
const edgeKey = `${link.source}-${link.target}`;
|
||||||
|
const reverseKey = `${link.target}-${link.source}`;
|
||||||
|
if (!existingEdgeIds.has(edgeKey) && !existingEdgeIds.has(reverseKey)) {
|
||||||
|
edgesToAdd.push({
|
||||||
|
group: 'edges',
|
||||||
|
data: {
|
||||||
|
id: `temp-edge-${idx}-${Date.now()}`,
|
||||||
|
source: link.source,
|
||||||
|
target: link.target,
|
||||||
|
color: linkColorFnRef.current ? linkColorFnRef.current(link) : (link.color || DEFAULT_LINK_COLOR),
|
||||||
|
width: link.width || DEFAULT_LINK_WIDTH,
|
||||||
|
type: link.type,
|
||||||
|
isTemporary: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new elements to graph
|
||||||
|
if (nodesToAdd.length > 0 || edgesToAdd.length > 0) {
|
||||||
|
cy.add([...nodesToAdd, ...edgesToAdd]);
|
||||||
|
|
||||||
|
// Position new nodes near the clicked node
|
||||||
|
const clickedPos = node.position();
|
||||||
|
cy.nodes('[?isTemporary]').forEach((n, i) => {
|
||||||
|
const angle = (2 * Math.PI * i) / nodesToAdd.length;
|
||||||
|
const radius = 150;
|
||||||
|
n.position({
|
||||||
|
x: clickedPos.x + radius * Math.cos(angle),
|
||||||
|
y: clickedPos.y + radius * Math.sin(angle),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all connected elements (including newly added)
|
||||||
|
const neighborhood = node.neighborhood().add(node);
|
||||||
|
|
||||||
|
// Dim all elements first
|
||||||
|
cy.elements().addClass('dimmed');
|
||||||
|
|
||||||
|
// Highlight the neighborhood
|
||||||
|
neighborhood.removeClass('dimmed');
|
||||||
|
neighborhood.addClass('highlighted');
|
||||||
|
|
||||||
|
// Center on the neighborhood without changing positions
|
||||||
|
cy.animate({
|
||||||
|
fit: { eles: neighborhood, padding: 50 },
|
||||||
|
}, { duration: 400 });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on background to reset
|
||||||
|
cy.on('tap', (evt) => {
|
||||||
|
if (evt.target === cy) {
|
||||||
|
// Remove temporary nodes and edges
|
||||||
|
cy.elements('[?isTemporary]').remove();
|
||||||
|
|
||||||
|
cy.elements().removeClass('dimmed highlighted');
|
||||||
|
cy.animate({
|
||||||
|
fit: { eles: cy.elements(), padding: 50 },
|
||||||
|
}, { duration: 400 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.on('mouseover', 'node', (evt) => {
|
||||||
|
const node = evt.target as NodeSingular;
|
||||||
|
const originalNode = node.data('originalNode') as GraphNode;
|
||||||
|
setHoveredNode(originalNode);
|
||||||
|
if (onNodeHoverRef.current && originalNode) {
|
||||||
|
onNodeHoverRef.current(originalNode);
|
||||||
|
}
|
||||||
|
containerRef.current!.style.cursor = 'pointer';
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.on('mouseout', 'node', () => {
|
||||||
|
setHoveredNode(null);
|
||||||
|
if (onNodeHoverRef.current) {
|
||||||
|
onNodeHoverRef.current(null);
|
||||||
|
}
|
||||||
|
containerRef.current!.style.cursor = 'default';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edge hover handlers
|
||||||
|
cy.on('mouseover', 'edge', (evt) => {
|
||||||
|
const edge = evt.target;
|
||||||
|
const originalLink = edge.data('originalLink') as GraphLink;
|
||||||
|
if (originalLink) {
|
||||||
|
setHoveredLink(originalLink);
|
||||||
|
// Get position for tooltip
|
||||||
|
const renderedPos = edge.renderedMidpoint();
|
||||||
|
setLinkTooltipPos({ x: renderedPos.x, y: renderedPos.y });
|
||||||
|
}
|
||||||
|
containerRef.current!.style.cursor = 'pointer';
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.on('mouseout', 'edge', () => {
|
||||||
|
setHoveredLink(null);
|
||||||
|
setLinkTooltipPos(null);
|
||||||
|
containerRef.current!.style.cursor = 'default';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run layout
|
||||||
|
cy.layout({
|
||||||
|
name: 'cose',
|
||||||
|
animate: false,
|
||||||
|
randomize: true,
|
||||||
|
nodeRepulsion: () => 100000,
|
||||||
|
idealEdgeLength: () => 300,
|
||||||
|
edgeElasticity: () => 20,
|
||||||
|
nestingFactor: 0.1,
|
||||||
|
gravity: 0.01,
|
||||||
|
numIter: 2500,
|
||||||
|
coolingFactor: 0.95,
|
||||||
|
minTemp: 1.0,
|
||||||
|
nodeOverlap: 20,
|
||||||
|
nodeDimensionsIncludeLabels: true,
|
||||||
|
padding: 50,
|
||||||
|
} as any).run();
|
||||||
|
|
||||||
|
// Fit to viewport
|
||||||
|
cy.fit(undefined, 50);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cy.destroy();
|
||||||
|
};
|
||||||
|
}, [cyElements, showLabels, isDarkMode]);
|
||||||
|
|
||||||
|
// Handle resize
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (cyRef.current) {
|
||||||
|
cyRef.current.resize();
|
||||||
|
cyRef.current.fit(undefined, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full rounded-lg overflow-hidden border border-border" style={{ height }}>
|
||||||
|
{/* Loading state */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-background z-10">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4" />
|
||||||
|
<p className="text-sm text-muted-foreground">Loading graph...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Cytoscape container */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="w-full h-full"
|
||||||
|
style={{
|
||||||
|
background: isDarkMode
|
||||||
|
? 'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.08) 1px, transparent 0)'
|
||||||
|
: 'radial-gradient(circle at 1px 1px, rgba(0,0,0,0.06) 1px, transparent 0)',
|
||||||
|
backgroundSize: '20px 20px',
|
||||||
|
backgroundColor: isDarkMode ? '#0f1419' : '#f8fafc',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
{!isLoading && graphData.nodes.length === 0 && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-muted-foreground">No memories to display</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Link hover tooltip */}
|
||||||
|
{hoveredLink && linkTooltipPos && (
|
||||||
|
<div
|
||||||
|
className="absolute z-30 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
left: linkTooltipPos.x,
|
||||||
|
top: linkTooltipPos.y,
|
||||||
|
transform: 'translate(-50%, -100%) translateY(-8px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={`px-3 py-2 rounded-lg shadow-lg text-sm ${
|
||||||
|
isDarkMode ? 'bg-gray-800 text-white' : 'bg-white text-gray-900 border border-gray-200'
|
||||||
|
}`}>
|
||||||
|
<div className="font-medium capitalize mb-1">
|
||||||
|
{(() => {
|
||||||
|
const type = hoveredLink.type || 'semantic';
|
||||||
|
if (['causes', 'caused_by', 'enables', 'prevents'].includes(type)) {
|
||||||
|
return `Causal (${type.replace('_', ' ')})`;
|
||||||
|
}
|
||||||
|
return `${type} link`;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
{hoveredLink.entity && (
|
||||||
|
<div className="text-xs opacity-80">
|
||||||
|
Entity: <span className="font-medium">{hoveredLink.entity}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hoveredLink.weight !== undefined && (
|
||||||
|
<div className="text-xs opacity-80">
|
||||||
|
Weight: <span className="font-medium">{hoveredLink.weight.toFixed(3)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Controls hint */}
|
||||||
|
<div className="absolute bottom-4 right-4 text-xs text-muted-foreground/60 z-20">
|
||||||
|
Drag to pan • Scroll to zoom • Click node to focus
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Utility Functions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export function convertHindsightGraphData(hindsightData: {
|
||||||
|
nodes?: Array<{ data: { id: string; label?: string; color?: string } }>;
|
||||||
|
edges?: Array<{ data: { source: string; target: string; color?: string; lineStyle?: string; linkType?: string; entityName?: string; weight?: number; similarity?: number } }>;
|
||||||
|
table_rows?: Array<{ id: string; text: string; entities?: string; context?: string }>;
|
||||||
|
}): GraphData {
|
||||||
|
const nodes: GraphNode[] = (hindsightData.nodes || []).map(n => {
|
||||||
|
const tableRow = hindsightData.table_rows?.find(r => r.id === n.data.id);
|
||||||
|
// Use memory text as label, truncated to ~40 chars
|
||||||
|
let label = n.data.label;
|
||||||
|
if (!label && tableRow?.text) {
|
||||||
|
label = tableRow.text.length > 40 ? tableRow.text.substring(0, 40) + '...' : tableRow.text;
|
||||||
|
}
|
||||||
|
if (!label) {
|
||||||
|
label = n.data.id.substring(0, 8);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: n.data.id,
|
||||||
|
label,
|
||||||
|
color: n.data.color,
|
||||||
|
metadata: tableRow,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const links: GraphLink[] = (hindsightData.edges || []).map(e => ({
|
||||||
|
source: e.data.source,
|
||||||
|
target: e.data.target,
|
||||||
|
color: e.data.color,
|
||||||
|
// Use linkType directly from API, fallback to lineStyle check, default to semantic
|
||||||
|
type: e.data.linkType || (e.data.lineStyle === 'dashed' ? 'temporal' : 'semantic'),
|
||||||
|
entity: e.data.entityName, // API returns entityName
|
||||||
|
weight: e.data.weight ?? e.data.similarity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { nodes, links };
|
||||||
|
}
|
||||||
|
|
@ -64,13 +64,12 @@ export function MemoryDetailPanel({
|
||||||
<p className="text-sm text-muted-foreground mt-1">Full memory content and metadata</p>
|
<p className="text-sm text-muted-foreground mt-1">Full memory content and metadata</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="h-9 px-3 gap-2"
|
className="h-8 w-8 p-0"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-5 w-5" />
|
||||||
Close
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,16 +15,16 @@ interface SidebarProps {
|
||||||
|
|
||||||
export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||||
const { currentBank } = useBank();
|
const { currentBank } = useBank();
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
if (!currentBank) {
|
if (!currentBank) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
|
{ id: 'data' as NavItem, label: 'Memories', icon: Database },
|
||||||
{ id: 'recall' as NavItem, label: 'Recall', icon: Search },
|
{ id: 'recall' as NavItem, label: 'Recall', icon: Search },
|
||||||
{ id: 'reflect' as NavItem, label: 'Reflect', icon: Sparkles },
|
{ id: 'reflect' as NavItem, label: 'Reflect', icon: Sparkles },
|
||||||
{ id: 'data' as NavItem, label: 'Memories', icon: Database },
|
|
||||||
{ id: 'documents' as NavItem, label: 'Documents', icon: FileText },
|
{ id: 'documents' as NavItem, label: 'Documents', icon: FileText },
|
||||||
{ id: 'entities' as NavItem, label: 'Entities', icon: Users },
|
{ id: 'entities' as NavItem, label: 'Entities', icon: Users },
|
||||||
{ id: 'profile' as NavItem, label: 'Memory Bank', icon: Box },
|
{ id: 'profile' as NavItem, label: 'Memory Bank', icon: Box },
|
||||||
|
|
@ -35,24 +35,7 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||||
'bg-card border-r border-border flex flex-col transition-all duration-300',
|
'bg-card border-r border-border flex flex-col transition-all duration-300',
|
||||||
isCollapsed ? 'w-16' : 'w-64'
|
isCollapsed ? 'w-16' : 'w-64'
|
||||||
)}>
|
)}>
|
||||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
<nav className="flex-1 p-3 pt-4">
|
||||||
{!isCollapsed && (
|
|
||||||
<h2 className="text-lg font-semibold text-card-foreground">Hindsight</h2>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
||||||
className="p-1 rounded-lg hover:bg-accent transition-colors ml-auto"
|
|
||||||
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
||||||
>
|
|
||||||
{isCollapsed ? (
|
|
||||||
<ChevronRight className="w-5 h-5" />
|
|
||||||
) : (
|
|
||||||
<ChevronLeft className="w-5 h-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="flex-1 p-3">
|
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
|
@ -75,7 +58,7 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-all',
|
'w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-all',
|
||||||
isActive
|
isActive
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary-gradient text-white shadow-sm'
|
||||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||||
isCollapsed && 'justify-center px-0'
|
isCollapsed && 'justify-center px-0'
|
||||||
)}
|
)}
|
||||||
|
|
@ -89,6 +72,27 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Collapse/Expand button at bottom */}
|
||||||
|
<div className="p-3 border-t border-border">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
className={cn(
|
||||||
|
'w-full flex items-center gap-3 px-4 py-2 rounded-lg text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors',
|
||||||
|
isCollapsed && 'justify-center px-0'
|
||||||
|
)}
|
||||||
|
title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||||
|
>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
<span>Collapse</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const buttonVariants = cva(
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary-gradient text-white hover:opacity-90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
|
|
|
||||||
28
hindsight-control-plane/src/components/ui/slider.tsx
Normal file
28
hindsight-control-plane/src/components/ui/slider.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Slider = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
))
|
||||||
|
Slider.displayName = SliderPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Slider }
|
||||||
29
hindsight-control-plane/src/components/ui/switch.tsx
Normal file
29
hindsight-control-plane/src/components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Switch = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
))
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
46
hindsight-control-plane/src/lib/theme-context.tsx
Normal file
46
hindsight-control-plane/src/lib/theme-context.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [theme, setTheme] = useState<Theme>('light');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check for saved preference or system preference
|
||||||
|
const saved = localStorage.getItem('theme') as Theme | null;
|
||||||
|
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const initialTheme = saved || (systemPrefersDark ? 'dark' : 'light');
|
||||||
|
setTheme(initialTheme);
|
||||||
|
document.documentElement.classList.toggle('dark', initialTheme === 'dark');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||||
|
setTheme(newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
document.documentElement.classList.toggle('dark', newTheme === 'dark');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
@ -317,7 +317,7 @@ If it's correct, set correct=true.
|
||||||
response_format=JudgeResponse,
|
response_format=JudgeResponse,
|
||||||
scope="judge",
|
scope="judge",
|
||||||
temperature=0,
|
temperature=0,
|
||||||
max_tokens=4096
|
max_completion_tokens=4096
|
||||||
)
|
)
|
||||||
|
|
||||||
return judgement.correct, judgement.reasoning
|
return judgement.correct, judgement.reasoning
|
||||||
|
|
|
||||||
|
|
@ -398,7 +398,7 @@ Answer:
|
||||||
],
|
],
|
||||||
response_format=QuestionAnswer,
|
response_format=QuestionAnswer,
|
||||||
scope="memory",
|
scope="memory",
|
||||||
max_tokens=32768,
|
max_completion_tokens=32768,
|
||||||
)
|
)
|
||||||
reasoning_text = answer_obj.reasoning or ""
|
reasoning_text = answer_obj.reasoning or ""
|
||||||
if reasoning_text:
|
if reasoning_text:
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,10 @@ API available at http://localhost:8888
|
||||||
|
|
||||||
export OPENAI_API_KEY=sk-xxx
|
export OPENAI_API_KEY=sk-xxx
|
||||||
|
|
||||||
docker run -it -p 8888:8888 -p 9999:9999 \
|
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||||
ghcr.io/vectorize-io/hindsight
|
ghcr.io/vectorize-io/hindsight:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
- **API**: http://localhost:8888
|
- **API**: http://localhost:8888
|
||||||
|
|
@ -100,7 +100,7 @@ await client.reflect('my-bank', 'Tell me about Alice');
|
||||||
<TabItem value="cli" label="CLI">
|
<TabItem value="cli" label="CLI">
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/vectorize-io/hindsight/refs/heads/main/hindsight-cli/install.sh | bash
|
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,12 @@ See [Models](./models) for detailed comparison and configuration.
|
||||||
Run everything in one container with embedded PostgreSQL:
|
Run everything in one container with embedded PostgreSQL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 8888:8888 -p 9999:9999 \
|
export OPENAI_API_KEY=sk-xxx
|
||||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
|
||||||
-e HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx \
|
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||||
ghcr.io/vectorize-io/hindsight
|
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||||
|
ghcr.io/vectorize-io/hindsight:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
- **API Server**: http://localhost:8888
|
- **API Server**: http://localhost:8888
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,17 @@ sidebar_position: 4
|
||||||
|
|
||||||
When you call `reflect()`, Hindsight doesn't just retrieve facts — it **reasons** about them through the lens of the bank's unique disposition, forming new opinions and generating contextual responses.
|
When you call `reflect()`, Hindsight doesn't just retrieve facts — it **reasons** about them through the lens of the bank's unique disposition, forming new opinions and generating contextual responses.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[Query] --> B[Recall Memories]
|
||||||
|
B --> C[Load Disposition]
|
||||||
|
C --> D[Reason]
|
||||||
|
D --> E[Form Opinions]
|
||||||
|
E --> F[Response]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Why Reflect?
|
## Why Reflect?
|
||||||
|
|
||||||
Most AI systems can retrieve facts, but they can't **reason** about them in a consistent way. Every response is generated fresh without a stable perspective or evolving beliefs.
|
Most AI systems can retrieve facts, but they can't **reason** about them in a consistent way. Every response is generated fresh without a stable perspective or evolving beliefs.
|
||||||
|
|
@ -41,16 +52,6 @@ With reflect:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The Reflect Process
|
|
||||||
|
|
||||||
1. **Recall** relevant memories based on the query
|
|
||||||
2. **Load** the bank's disposition traits and background
|
|
||||||
3. **Reason** about the memories through the disposition lens
|
|
||||||
4. **Form** new opinions with confidence scores
|
|
||||||
5. **Return** response, sources, and any new beliefs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Disposition Traits
|
## Disposition Traits
|
||||||
|
|
||||||
When you create a memory bank, you can configure its disposition using three traits. These traits influence how the bank interprets information and forms opinions during `reflect()`:
|
When you create a memory bank, you can configure its disposition using three traits. These traits influence how the bank interprets information and forms opinions during `reflect()`:
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,12 @@ When you call `retain()`, Hindsight transforms conversations and documents into
|
||||||
|
|
||||||
## What Retain Does
|
## What Retain Does
|
||||||
|
|
||||||
```
|
```mermaid
|
||||||
Your Content
|
graph LR
|
||||||
↓
|
A[Your Content] --> B[Extract Facts]
|
||||||
Extract Rich Facts
|
B --> C[Identify Entities]
|
||||||
↓
|
C --> D[Build Connections]
|
||||||
Identify Entities
|
D --> E[Memory Bank]
|
||||||
↓
|
|
||||||
Build Connections
|
|
||||||
↓
|
|
||||||
Searchable Memory Bank
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,24 @@ sidebar_position: 3
|
||||||
|
|
||||||
When you call `recall()`, Hindsight uses multiple search strategies in parallel to find the most relevant memories, regardless of how you phrase your query.
|
When you call `recall()`, Hindsight uses multiple search strategies in parallel to find the most relevant memories, regardless of how you phrase your query.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
Q[Query] --> S[Semantic]
|
||||||
|
Q --> K[Keyword]
|
||||||
|
Q --> G[Graph]
|
||||||
|
Q --> T[Temporal]
|
||||||
|
|
||||||
|
S --> RRF[RRF Fusion]
|
||||||
|
K --> RRF
|
||||||
|
G --> RRF
|
||||||
|
T --> RRF
|
||||||
|
|
||||||
|
RRF --> CE[Cross-Encoder]
|
||||||
|
CE --> R[Results]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## The Challenge of Memory Recall
|
## The Challenge of Memory Recall
|
||||||
|
|
||||||
Different queries need different search approaches:
|
Different queries need different search approaches:
|
||||||
|
|
@ -117,19 +135,6 @@ This gives your agent richer context while maintaining precise control over tota
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How Recall Works
|
|
||||||
|
|
||||||
When you call `recall(query, bank_id)`:
|
|
||||||
|
|
||||||
1. **Parse** → Detect temporal expressions, understand intent
|
|
||||||
2. **Search** → Run 4 strategies in parallel
|
|
||||||
3. **Fuse** → Combine results, prioritizing consensus
|
|
||||||
4. **Rerank** → Neural reranking for final relevance
|
|
||||||
5. **Filter** → Select top memories within token budget
|
|
||||||
6. **Return** → Ranked, relevant memories
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tuning Recall: Quality vs Latency
|
## Tuning Recall: Quality vs Latency
|
||||||
|
|
||||||
Different use cases require different trade-offs between **recall quality** and **response speed**. Two parameters control this:
|
Different use cases require different trade-offs between **recall quality** and **response speed**. Two parameters control this:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ The Hindsight CLI provides command-line access to memory operations and bank man
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://raw.githubusercontent.com/vectorize-io/hindsight/refs/heads/main/hindsight-cli/install.sh | bash
|
curl -fsSL https://hindsight.vectorize.io/get-cli | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
@ -179,12 +179,32 @@ hindsight memory recall <bank_id> "query" -o yaml
|
||||||
|
|
||||||
## Interactive Explorer
|
## Interactive Explorer
|
||||||
|
|
||||||
Launch the TUI explorer for visual navigation:
|
Launch the TUI explorer for visual navigation of your memory banks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hindsight explore
|
hindsight explore
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The explorer provides an interactive terminal interface to:
|
||||||
|
|
||||||
|
- **Browse memory banks** — View all banks and their statistics
|
||||||
|
- **Search memories** — Run recall queries with real-time results
|
||||||
|
- **Inspect entities** — Explore the knowledge graph and entity relationships
|
||||||
|
- **View facts** — Browse world facts, experiences, and opinions
|
||||||
|
- **Navigate documents** — See source documents and their extracted memories
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `↑/↓` | Navigate items |
|
||||||
|
| `Enter` | Select / Expand |
|
||||||
|
| `Tab` | Switch panels |
|
||||||
|
| `/` | Search |
|
||||||
|
| `q` | Quit |
|
||||||
|
|
||||||
|
<!-- Screenshot placeholder: explore command TUI -->
|
||||||
|
|
||||||
## Example Workflow
|
## Example Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
---
|
|
||||||
sidebar_position: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
# LangGraph
|
|
||||||
|
|
||||||
Hindsight provides a `BaseStore` implementation for LangGraph's memory system.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd hindsight-langmem && uv pip install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```python
|
|
||||||
from hindsight_langmem import HindsightStore
|
|
||||||
|
|
||||||
# Create store
|
|
||||||
store = HindsightStore(
|
|
||||||
base_url="http://localhost:8888",
|
|
||||||
default_agent_id="my-agent",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store data
|
|
||||||
store.put(
|
|
||||||
namespace=("user", "preferences"),
|
|
||||||
key="language",
|
|
||||||
value={"language": "Python", "reason": "data science"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Retrieve data
|
|
||||||
item = store.get(namespace=("user", "preferences"), key="language")
|
|
||||||
print(item.value) # {"language": "Python", "reason": "data science"}
|
|
||||||
|
|
||||||
# Search
|
|
||||||
results = store.search(
|
|
||||||
namespace_prefix=("user",),
|
|
||||||
query="programming language",
|
|
||||||
limit=10
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
`HindsightStore` implements LangGraph's `BaseStore` interface:
|
|
||||||
|
|
||||||
- **Namespaces** map to Hindsight agent IDs (joined with `__`)
|
|
||||||
- **Keys** map to document IDs
|
|
||||||
- **Values** are stored as JSON in memory content
|
|
||||||
|
|
||||||
## BaseStore Interface
|
|
||||||
|
|
||||||
### put
|
|
||||||
|
|
||||||
Store an item:
|
|
||||||
|
|
||||||
```python
|
|
||||||
store.put(
|
|
||||||
namespace=("user", "session-123"),
|
|
||||||
key="preferences",
|
|
||||||
value={"theme": "dark", "language": "en"}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### get
|
|
||||||
|
|
||||||
Retrieve an item:
|
|
||||||
|
|
||||||
```python
|
|
||||||
item = store.get(namespace=("user", "session-123"), key="preferences")
|
|
||||||
if item:
|
|
||||||
print(item.value) # {"theme": "dark", "language": "en"}
|
|
||||||
print(item.created_at)
|
|
||||||
print(item.updated_at)
|
|
||||||
```
|
|
||||||
|
|
||||||
### search
|
|
||||||
|
|
||||||
Search within a namespace:
|
|
||||||
|
|
||||||
```python
|
|
||||||
results = store.search(
|
|
||||||
namespace_prefix=("user",),
|
|
||||||
query="theme preferences",
|
|
||||||
limit=10,
|
|
||||||
offset=0
|
|
||||||
)
|
|
||||||
|
|
||||||
for item in results:
|
|
||||||
print(f"{item.key}: {item.value}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### delete
|
|
||||||
|
|
||||||
Delete an item:
|
|
||||||
|
|
||||||
```python
|
|
||||||
store.delete(namespace=("user", "session-123"), key="preferences")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Async Support
|
|
||||||
|
|
||||||
All operations have async variants:
|
|
||||||
|
|
||||||
```python
|
|
||||||
await store.aput(namespace, key, value)
|
|
||||||
item = await store.aget(namespace, key)
|
|
||||||
results = await store.asearch(namespace_prefix, query)
|
|
||||||
await store.adelete(namespace, key)
|
|
||||||
```
|
|
||||||
|
|
||||||
## With LangGraph
|
|
||||||
|
|
||||||
```python
|
|
||||||
from langgraph.graph import StateGraph
|
|
||||||
from hindsight_langmem import HindsightStore
|
|
||||||
|
|
||||||
store = HindsightStore(base_url="http://localhost:8888")
|
|
||||||
|
|
||||||
# Use store in your graph
|
|
||||||
graph = StateGraph()
|
|
||||||
# ... configure graph with store
|
|
||||||
```
|
|
||||||
|
|
||||||
## Namespace Mapping
|
|
||||||
|
|
||||||
Namespaces are converted to Hindsight agent IDs:
|
|
||||||
|
|
||||||
| Namespace | bank ID |
|
|
||||||
|-----------|----------|
|
|
||||||
| `("user",)` | `user` |
|
|
||||||
| `("user", "session")` | `user__session` |
|
|
||||||
| `("app", "v1", "data")` | `app__v1__data` |
|
|
||||||
| `()` | `default_agent_id` |
|
|
||||||
|
|
||||||
Memory banks are created automatically if they don't exist.
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
---
|
|
||||||
sidebar_position: 4
|
|
||||||
---
|
|
||||||
|
|
||||||
# MCP Server
|
|
||||||
|
|
||||||
Model Context Protocol server for AI assistants like Claude Desktop.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
The MCP server is included in the Hindsight API. When running the API with MCP enabled, it exposes MCP tools at `/mcp/{bank_id}/sse`.
|
|
||||||
|
|
||||||
### Claude Desktop Configuration
|
|
||||||
|
|
||||||
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"hindsight": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "mcp-remote", "http://localhost:8888/mcp/my-bank-id/sse"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace `my-bank-id` with your memory bank ID.
|
|
||||||
|
|
||||||
## Available Tools
|
|
||||||
|
|
||||||
### retain
|
|
||||||
|
|
||||||
Store a memory:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "retain",
|
|
||||||
"arguments": {
|
|
||||||
"content": "User prefers Python for data analysis",
|
|
||||||
"context": "preferences"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|------|----------|-------------|
|
|
||||||
| `content` | string | yes | Memory content to store |
|
|
||||||
| `context` | string | no | Category (default: 'general') |
|
|
||||||
|
|
||||||
### recall
|
|
||||||
|
|
||||||
Search memories:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "recall",
|
|
||||||
"arguments": {
|
|
||||||
"query": "What does the user do for work?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Parameters:**
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|------|----------|-------------|
|
|
||||||
| `query` | string | yes | Natural language search query |
|
|
||||||
| `max_results` | integer | no | Max results (default: 10) |
|
|
||||||
|
|
||||||
## Usage Example
|
|
||||||
|
|
||||||
Once configured, Claude can use Hindsight naturally:
|
|
||||||
|
|
||||||
**User**: "Remember that I prefer morning meetings"
|
|
||||||
|
|
||||||
**Claude**: *Uses retain*
|
|
||||||
|
|
||||||
> "I've noted that you prefer morning meetings."
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**User**: "What do you know about my preferences?"
|
|
||||||
|
|
||||||
**Claude**: *Uses recall*
|
|
||||||
|
|
||||||
> "Based on our conversations, you prefer morning meetings and like Python for data analysis."
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
---
|
|
||||||
sidebar_position: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
# OpenAI
|
|
||||||
|
|
||||||
Drop-in replacement for the OpenAI Python client with automatic memory integration.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd hindsight-openai && uv pip install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```python
|
|
||||||
from hindsight_openai import configure, OpenAI
|
|
||||||
|
|
||||||
# Configure once
|
|
||||||
configure(
|
|
||||||
hindsight_api_url="http://localhost:8888",
|
|
||||||
agent_id="my-agent",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Use OpenAI client normally
|
|
||||||
client = OpenAI(api_key="sk-...")
|
|
||||||
|
|
||||||
response = client.chat.completions.create(
|
|
||||||
model="gpt-4",
|
|
||||||
messages=[{"role": "user", "content": "What did we discuss about AI?"}]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
The wrapper intercepts OpenAI calls:
|
|
||||||
|
|
||||||
1. **Before**: Retrieves relevant memories and injects as system message
|
|
||||||
2. **After**: Stores conversation to Hindsight
|
|
||||||
|
|
||||||
Your code works exactly as before, but now has memory.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
```python
|
|
||||||
configure(
|
|
||||||
hindsight_api_url="http://localhost:8888", # Hindsight API
|
|
||||||
agent_id="my-agent", # Required
|
|
||||||
store_conversations=True, # Store conversations
|
|
||||||
inject_memories=True, # Inject memories into prompts
|
|
||||||
document_id="session-123", # Group by document
|
|
||||||
enabled=True, # Master switch
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Memory Injection
|
|
||||||
|
|
||||||
When enabled, memories are automatically injected:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Your code
|
|
||||||
messages = [{"role": "user", "content": "What trails did Alice recommend?"}]
|
|
||||||
|
|
||||||
# What gets sent to OpenAI
|
|
||||||
messages = [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": "Relevant context:\n- Alice loves hiking in Yosemite\n- Alice recommended Half Dome trail"
|
|
||||||
},
|
|
||||||
{"role": "user", "content": "What trails did Alice recommend?"}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Async Support
|
|
||||||
|
|
||||||
```python
|
|
||||||
from hindsight_openai import configure, AsyncOpenAI
|
|
||||||
|
|
||||||
configure(hindsight_api_url="http://localhost:8888", agent_id="my-agent")
|
|
||||||
|
|
||||||
client = AsyncOpenAI(api_key="sk-...")
|
|
||||||
|
|
||||||
response = await client.chat.completions.create(
|
|
||||||
model="gpt-4",
|
|
||||||
messages=[{"role": "user", "content": "Tell me about my preferences"}]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Streaming
|
|
||||||
|
|
||||||
Fully supported:
|
|
||||||
|
|
||||||
```python
|
|
||||||
stream = client.chat.completions.create(
|
|
||||||
model="gpt-4",
|
|
||||||
messages=[{"role": "user", "content": "Tell me a story"}],
|
|
||||||
stream=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for chunk in stream:
|
|
||||||
print(chunk.choices[0].delta.content or "", end="")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Disable Temporarily
|
|
||||||
|
|
||||||
```python
|
|
||||||
from hindsight_openai import configure
|
|
||||||
|
|
||||||
configure(enabled=False) # Disable
|
|
||||||
configure(enabled=True) # Re-enable
|
|
||||||
```
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type * as Preset from '@docusaurus/preset-classic';
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
title: 'Hindsight',
|
title: 'Hindsight',
|
||||||
tagline: 'Entity-Aware Memory System for AI Agents',
|
tagline: 'Entity-Aware Memory System for AI Agents',
|
||||||
favicon: 'img/favicon.ico',
|
favicon: 'img/favicon.png',
|
||||||
|
|
||||||
future: {
|
future: {
|
||||||
v4: true,
|
v4: true,
|
||||||
|
|
@ -49,7 +49,7 @@ const config: Config = {
|
||||||
tagName: 'link',
|
tagName: 'link',
|
||||||
attributes: {
|
attributes: {
|
||||||
rel: 'stylesheet',
|
rel: 'stylesheet',
|
||||||
href: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Nunito+Sans:wght@400;500;600;700;800&display=swap',
|
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@500;600;700&display=swap',
|
||||||
media: 'print',
|
media: 'print',
|
||||||
onload: "this.media='all'",
|
onload: "this.media='all'",
|
||||||
},
|
},
|
||||||
|
|
@ -83,7 +83,7 @@ const config: Config = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
primaryColor: '#0d9488',
|
primaryColor: '#0074d9',
|
||||||
sidebar: {
|
sidebar: {
|
||||||
backgroundColor: '#09090b',
|
backgroundColor: '#09090b',
|
||||||
},
|
},
|
||||||
|
|
@ -92,9 +92,9 @@ const config: Config = {
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
fontSize: '15px',
|
fontSize: '15px',
|
||||||
fontFamily: "'Avenir Book', 'Avenir', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||||
headings: {
|
headings: {
|
||||||
fontFamily: "'Avenir', 'Avenir Book', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
fontFamily: "'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace",
|
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace",
|
||||||
|
|
@ -121,10 +121,10 @@ const config: Config = {
|
||||||
respectPrefersColorScheme: true,
|
respectPrefersColorScheme: true,
|
||||||
},
|
},
|
||||||
navbar: {
|
navbar: {
|
||||||
title: 'Hindsight',
|
|
||||||
logo: {
|
logo: {
|
||||||
alt: 'Hindsight Logo',
|
alt: 'Hindsight Logo',
|
||||||
src: 'img/logo.svg',
|
src: 'img/logo.png',
|
||||||
|
style: { height: '32px' },
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
|
|
@ -164,8 +164,9 @@ const config: Config = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'https://github.com/vectorize-io/hindsight',
|
href: 'https://github.com/vectorize-io/hindsight',
|
||||||
label: 'GitHub',
|
|
||||||
position: 'right',
|
position: 'right',
|
||||||
|
className: 'header-github-link',
|
||||||
|
'aria-label': 'GitHub repository',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -209,24 +210,39 @@ const config: Config = {
|
||||||
mermaid: {
|
mermaid: {
|
||||||
theme: {
|
theme: {
|
||||||
light: 'base',
|
light: 'base',
|
||||||
dark: 'dark',
|
dark: 'base',
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
themeVariables: {
|
themeVariables: {
|
||||||
primaryColor: '#6366f1',
|
// Gradient start (#0074d9 blue) for nodes
|
||||||
|
primaryColor: '#0074d9',
|
||||||
primaryTextColor: '#ffffff',
|
primaryTextColor: '#ffffff',
|
||||||
primaryBorderColor: '#4f46e5',
|
primaryBorderColor: '#005db0',
|
||||||
secondaryColor: '#f1f5f9',
|
// Gradient end (#009296 teal) for edges/clusters
|
||||||
secondaryTextColor: '#1e293b',
|
secondaryColor: '#009296',
|
||||||
secondaryBorderColor: '#cbd5e1',
|
secondaryTextColor: '#ffffff',
|
||||||
tertiaryColor: '#e0e7ff',
|
secondaryBorderColor: '#007a7d',
|
||||||
lineColor: '#94a3b8',
|
// Tertiary
|
||||||
|
tertiaryColor: '#e6f7f8',
|
||||||
|
tertiaryTextColor: '#1e293b',
|
||||||
|
// Lines and edges - gradient end color
|
||||||
|
lineColor: '#009296',
|
||||||
|
// Text
|
||||||
textColor: '#1e293b',
|
textColor: '#1e293b',
|
||||||
mainBkg: '#ffffff',
|
// Node specific - gradient start
|
||||||
nodeBorder: '#4f46e5',
|
nodeBkg: '#0074d9',
|
||||||
clusterBkg: '#f8fafc',
|
nodeTextColor: '#ffffff',
|
||||||
clusterBorder: '#e2e8f0',
|
nodeBorder: '#005db0',
|
||||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
// Main background
|
||||||
|
mainBkg: '#0074d9',
|
||||||
|
// Clusters/subgraphs - gradient end
|
||||||
|
clusterBkg: 'rgba(0, 146, 150, 0.08)',
|
||||||
|
clusterBorder: '#009296',
|
||||||
|
// Labels
|
||||||
|
edgeLabelBackground: 'transparent',
|
||||||
|
labelBackground: 'transparent',
|
||||||
|
// Font - Inter to match body text
|
||||||
|
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1046,6 +1046,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Banks"
|
||||||
|
],
|
||||||
|
"summary": "Delete memory bank",
|
||||||
|
"description": "Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. This is a destructive operation that cannot be undone.",
|
||||||
|
"operationId": "delete_bank",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "bank_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Bank Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeleteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/default/banks/{bank_id}/memories": {
|
"/v1/default/banks/{bank_id}/memories": {
|
||||||
|
|
@ -1494,6 +1535,28 @@
|
||||||
"success": {
|
"success": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Success"
|
"title": "Success"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Message"
|
||||||
|
},
|
||||||
|
"deleted_count": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Deleted Count"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -1503,7 +1566,9 @@
|
||||||
"title": "DeleteResponse",
|
"title": "DeleteResponse",
|
||||||
"description": "Response model for delete operations.",
|
"description": "Response model for delete operations.",
|
||||||
"example": {
|
"example": {
|
||||||
"success": true
|
"success": true,
|
||||||
|
"message": "Deleted successfully",
|
||||||
|
"deleted_count": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DispositionTraits": {
|
"DispositionTraits": {
|
||||||
|
|
|
||||||
|
|
@ -147,28 +147,6 @@ const sidebars: SidebarsConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'category',
|
|
||||||
label: 'Integrations',
|
|
||||||
collapsible: false,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: 'doc',
|
|
||||||
id: 'sdks/openai',
|
|
||||||
label: 'OpenAI',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'doc',
|
|
||||||
id: 'sdks/langgraph',
|
|
||||||
label: 'LangGraph',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'doc',
|
|
||||||
id: 'sdks/mcp',
|
|
||||||
label: 'MCP Server',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
cookbookSidebar: [
|
cookbookSidebar: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,49 @@
|
||||||
* Hindsight custom theme - Modern, clean style
|
* Hindsight custom theme - Modern, clean style
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* Primary colors - Teal/Cyan theme */
|
/* Primary colors - Blue to Teal gradient theme */
|
||||||
:root {
|
:root {
|
||||||
--ifm-color-primary: #0d9488;
|
/* Primary gradient */
|
||||||
--ifm-color-primary-dark: #0f766e;
|
--hindsight-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||||
--ifm-color-primary-darker: #115e59;
|
--hindsight-gradient-start: #0074d9;
|
||||||
--ifm-color-primary-darkest: #134e4a;
|
--hindsight-gradient-end: #009296;
|
||||||
--ifm-color-primary-light: #14b8a6;
|
|
||||||
--ifm-color-primary-lighter: #2dd4bf;
|
/* Fallback solid colors (midpoint of gradient) */
|
||||||
--ifm-color-primary-lightest: #5eead4;
|
--ifm-color-primary: #0074d9;
|
||||||
|
--ifm-color-primary-dark: #0068c3;
|
||||||
|
--ifm-color-primary-darker: #005db0;
|
||||||
|
--ifm-color-primary-darkest: #004d91;
|
||||||
|
--ifm-color-primary-light: #1a85e0;
|
||||||
|
--ifm-color-primary-lighter: #3396e8;
|
||||||
|
--ifm-color-primary-lightest: #e6f3ff;
|
||||||
|
|
||||||
--ifm-code-font-size: 90%;
|
--ifm-code-font-size: 90%;
|
||||||
--docusaurus-highlighted-code-line-bg: rgba(13, 148, 136, 0.1);
|
--docusaurus-highlighted-code-line-bg: rgba(0, 116, 217, 0.1);
|
||||||
|
|
||||||
/* Typography - Avenir Book */
|
/* Typography */
|
||||||
--ifm-font-family-base: 'Avenir Book', 'Avenir', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
--ifm-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
--ifm-heading-font-family: 'Avenir', 'Avenir Book', 'Nunito Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
--ifm-heading-font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
--ifm-font-family-monospace: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', Consolas, monospace;
|
--ifm-font-family-monospace: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', Consolas, monospace;
|
||||||
--ifm-font-weight-semibold: 600;
|
--ifm-font-weight-semibold: 600;
|
||||||
--ifm-font-size-base: 96%;
|
--ifm-font-size-base: 96%;
|
||||||
|
|
||||||
/* Spacing */
|
/* Spacing */
|
||||||
--ifm-spacing-horizontal: 1.5rem;
|
--ifm-spacing-horizontal: 1.5rem;
|
||||||
--ifm-navbar-height: 3.5rem;
|
--ifm-navbar-height: 4.5rem;
|
||||||
|
|
||||||
/* Borders */
|
/* Borders */
|
||||||
--ifm-global-radius: 0.5rem;
|
--ifm-global-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] {
|
[data-theme='dark'] {
|
||||||
--ifm-color-primary: #2dd4bf;
|
--ifm-color-primary: #3396e8;
|
||||||
--ifm-color-primary-dark: #14b8a6;
|
--ifm-color-primary-dark: #1a85e0;
|
||||||
--ifm-color-primary-darker: #0d9488;
|
--ifm-color-primary-darker: #0074d9;
|
||||||
--ifm-color-primary-darkest: #0f766e;
|
--ifm-color-primary-darkest: #0068c3;
|
||||||
--ifm-color-primary-light: #5eead4;
|
--ifm-color-primary-light: #66b3f0;
|
||||||
--ifm-color-primary-lighter: #99f6e4;
|
--ifm-color-primary-lighter: #99cff5;
|
||||||
--ifm-color-primary-lightest: #ccfbf1;
|
--ifm-color-primary-lightest: rgba(51, 150, 232, 0.15);
|
||||||
--docusaurus-highlighted-code-line-bg: rgba(45, 212, 191, 0.15);
|
--docusaurus-highlighted-code-line-bg: rgba(51, 150, 232, 0.15);
|
||||||
|
|
||||||
--ifm-background-color: #09090b;
|
--ifm-background-color: #09090b;
|
||||||
--ifm-background-surface-color: #18181b;
|
--ifm-background-surface-color: #18181b;
|
||||||
|
|
@ -47,6 +53,14 @@
|
||||||
--ifm-toc-border-color: #27272a;
|
--ifm-toc-border-color: #27272a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Gradient text utility */
|
||||||
|
.gradient-text {
|
||||||
|
background: var(--hindsight-gradient);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
/* Navbar styling */
|
/* Navbar styling */
|
||||||
.navbar {
|
.navbar {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
@ -54,9 +68,17 @@
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar__logo {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar__title {
|
.navbar__title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
|
background: var(--hindsight-gradient);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__items {
|
.navbar__items {
|
||||||
|
|
@ -64,19 +86,40 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__link {
|
.navbar__link {
|
||||||
font-weight: 500;
|
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
font-size: 0.8125rem;
|
font-weight: 700;
|
||||||
padding: 0.5rem 0.75rem;
|
font-size: 0.9rem;
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* GitHub icon link */
|
||||||
|
.header-github-link::before {
|
||||||
|
content: '';
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23666' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .header-github-link::before {
|
||||||
|
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-github-link:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar__link:hover {
|
.navbar__link:hover {
|
||||||
background-color: var(--ifm-background-surface-color);
|
background-color: var(--ifm-background-surface-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar__link--active {
|
.navbar__link--active {
|
||||||
color: var(--ifm-color-primary);
|
background: var(--hindsight-gradient);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] .navbar__link:hover {
|
[data-theme='dark'] .navbar__link:hover {
|
||||||
|
|
@ -89,7 +132,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero--primary {
|
.hero--primary {
|
||||||
background: linear-gradient(135deg, var(--ifm-color-primary-darkest) 0%, var(--ifm-color-primary-dark) 100%);
|
background: linear-gradient(135deg, var(--hindsight-gradient-start) 0%, var(--hindsight-gradient-end) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] .hero--primary {
|
[data-theme='dark'] .hero--primary {
|
||||||
|
|
@ -115,18 +158,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--primary {
|
.button--primary {
|
||||||
background: var(--ifm-color-primary);
|
background: var(--hindsight-gradient);
|
||||||
border-color: var(--ifm-color-primary);
|
border: none;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--primary:hover {
|
.button--primary:hover {
|
||||||
background: var(--ifm-color-primary-dark);
|
background: linear-gradient(135deg, #005db0 0%, #007a7d 100%);
|
||||||
border-color: var(--ifm-color-primary-dark);
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--secondary {
|
.button--secondary {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px solid currentColor;
|
border: 2px solid var(--hindsight-gradient-start);
|
||||||
|
color: var(--ifm-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--secondary:hover {
|
||||||
|
background: var(--hindsight-gradient);
|
||||||
|
border-color: transparent;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar */
|
/* Sidebar */
|
||||||
|
|
@ -137,13 +188,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu__link--active {
|
.menu__link--active {
|
||||||
background: var(--ifm-color-primary-lightest);
|
background: linear-gradient(135deg, rgba(0, 116, 217, 0.1) 0%, rgba(0, 146, 150, 0.1) 100%);
|
||||||
color: var(--ifm-color-primary-darkest);
|
}
|
||||||
|
|
||||||
|
.menu__link--active .menu__link {
|
||||||
|
background: var(--hindsight-gradient);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] .menu__link--active {
|
[data-theme='dark'] .menu__link--active {
|
||||||
background: rgba(45, 212, 191, 0.12);
|
background: linear-gradient(135deg, rgba(0, 116, 217, 0.15) 0%, rgba(0, 146, 150, 0.15) 100%);
|
||||||
color: var(--ifm-color-primary-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Non-collapsible category styling */
|
/* Non-collapsible category styling */
|
||||||
|
|
@ -174,11 +230,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code blocks */
|
/* Code blocks */
|
||||||
|
pre,
|
||||||
|
pre code,
|
||||||
|
.prism-code {
|
||||||
|
font-family: 'JetBrains Mono', var(--ifm-font-family-monospace) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.prism-code {
|
.prism-code {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
font-family: var(--ifm-font-family-monospace);
|
|
||||||
padding: 1rem !important;
|
padding: 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,7 +248,7 @@ code {
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
padding: 0.2rem 0.45rem;
|
padding: 0.2rem 0.45rem;
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
font-family: var(--ifm-font-family-monospace);
|
font-family: 'JetBrains Mono', var(--ifm-font-family-monospace) !important;
|
||||||
background-color: #f1f5f9;
|
background-color: #f1f5f9;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
@ -200,7 +261,7 @@ code {
|
||||||
|
|
||||||
/* Don't apply inline styles to code inside pre blocks */
|
/* Don't apply inline styles to code inside pre blocks */
|
||||||
pre code {
|
pre code {
|
||||||
background-color: transparent;
|
background-color: transparent !important;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
@ -208,32 +269,36 @@ pre code {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code block container */
|
/* Code block container - single background source */
|
||||||
div[class*="codeBlockContainer"] {
|
div[class*="codeBlockContainer"] {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: 1px solid var(--ifm-toc-border-color);
|
border: 1px solid var(--ifm-toc-border-color);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #f8fafc;
|
background: #f8fafc !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] div[class*="codeBlockContainer"] {
|
[data-theme='dark'] div[class*="codeBlockContainer"] {
|
||||||
background: #0f172a;
|
background: #0f172a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[class*="codeBlockTitle"] {
|
div[class*="codeBlockTitle"] {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-bottom: 1px solid var(--ifm-toc-border-color);
|
border-bottom: 1px solid var(--ifm-toc-border-color);
|
||||||
font-family: var(--ifm-font-family-monospace);
|
font-family: 'JetBrains Mono', var(--ifm-font-family-monospace);
|
||||||
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code block content area */
|
/* Code block content area - transparent to show container bg */
|
||||||
div[class*="codeBlockContent"] {
|
div[class*="codeBlockContent"],
|
||||||
background: #f8fafc;
|
div[class*="codeBlockContent"] pre,
|
||||||
|
div[class*="codeBlockContent"] .prism-code {
|
||||||
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='dark'] div[class*="codeBlockContent"] {
|
/* Prism token backgrounds */
|
||||||
background: #0f172a;
|
.prism-code span {
|
||||||
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cards/Features */
|
/* Cards/Features */
|
||||||
|
|
@ -246,10 +311,21 @@ div[class*="codeBlockContent"] {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
article h1 {
|
/* Page title with gradient */
|
||||||
|
article h1,
|
||||||
|
.markdown h1,
|
||||||
|
header h1,
|
||||||
|
h1[class*="title"] {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
|
background-image: linear-gradient(90deg, #0074d9, #009296) !important;
|
||||||
|
background-size: 100% !important;
|
||||||
|
-webkit-background-clip: text !important;
|
||||||
|
background-clip: text !important;
|
||||||
|
-webkit-text-fill-color: transparent !important;
|
||||||
|
color: transparent !important;
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
article h2 {
|
article h2 {
|
||||||
|
|
@ -258,7 +334,9 @@ article h2 {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
border-bottom: 1px solid var(--ifm-toc-border-color);
|
border-bottom: 2px solid transparent;
|
||||||
|
border-image: var(--hindsight-gradient);
|
||||||
|
border-image-slice: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
article h3 {
|
article h3 {
|
||||||
|
|
@ -272,10 +350,93 @@ article p {
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Admonitions */
|
/* Links with gradient */
|
||||||
.admonition {
|
article a:not(.button):not([class*="hash-link"]) {
|
||||||
border-radius: 0.5rem;
|
background: var(--hindsight-gradient);
|
||||||
border-left-width: 4px;
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
article a:not(.button):not([class*="hash-link"]):hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: var(--hindsight-gradient-start);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admonitions - gradient themed */
|
||||||
|
.theme-admonition,
|
||||||
|
[class*="admonition_"] {
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border: none !important;
|
||||||
|
background: rgba(0, 116, 217, 0.05) !important;
|
||||||
|
position: relative !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
padding-left: 1.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient left border using pseudo-element */
|
||||||
|
.theme-admonition::before,
|
||||||
|
[class*="admonition_"]::before {
|
||||||
|
content: '' !important;
|
||||||
|
position: absolute !important;
|
||||||
|
left: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
width: 4px !important;
|
||||||
|
background: linear-gradient(180deg, #0074d9, #009296) !important;
|
||||||
|
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admonition heading text - gradient */
|
||||||
|
[class*="admonitionHeading_"] {
|
||||||
|
background-image: linear-gradient(90deg, #0074d9, #009296) !important;
|
||||||
|
-webkit-background-clip: text !important;
|
||||||
|
background-clip: text !important;
|
||||||
|
-webkit-text-fill-color: transparent !important;
|
||||||
|
color: transparent !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admonition icon - gradient start color */
|
||||||
|
[class*="admonitionIcon_"] svg,
|
||||||
|
[class*="admonitionIcon_"] svg path {
|
||||||
|
fill: #0074d9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Different admonition types */
|
||||||
|
.alert--info[class*="admonition_"],
|
||||||
|
.theme-admonition-info {
|
||||||
|
background: rgba(0, 116, 217, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--success[class*="admonition_"],
|
||||||
|
.theme-admonition-tip {
|
||||||
|
background: rgba(0, 146, 150, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--warning[class*="admonition_"],
|
||||||
|
.theme-admonition-warning {
|
||||||
|
background: rgba(0, 116, 217, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--secondary[class*="admonition_"],
|
||||||
|
.theme-admonition-note {
|
||||||
|
background: rgba(0, 131, 154, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode admonitions */
|
||||||
|
[data-theme='dark'] .theme-admonition,
|
||||||
|
[data-theme='dark'] [class*="admonition_"] {
|
||||||
|
background: rgba(0, 131, 154, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .theme-admonition::before,
|
||||||
|
[data-theme='dark'] [class*="admonition_"]::before {
|
||||||
|
background: linear-gradient(180deg, #3396e8, #00b4b8) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tables - Compact styling */
|
/* Tables - Compact styling */
|
||||||
|
|
@ -318,6 +479,14 @@ th {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs__item--active {
|
||||||
|
border-bottom-color: var(--hindsight-gradient-start);
|
||||||
|
background: var(--hindsight-gradient);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Redoc sidebar - expand all tags by default */
|
/* Redoc sidebar - expand all tags by default */
|
||||||
[class*="redoc-wrap"] [class*="menu-content"] ul {
|
[class*="redoc-wrap"] [class*="menu-content"] ul {
|
||||||
|
|
@ -329,6 +498,153 @@ th {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mermaid diagram styling - using high specificity selectors */
|
||||||
|
.mermaid svg[id^="mermaid"] {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node shapes - gradient start color (#0074d9 blue) */
|
||||||
|
.mermaid svg[id^="mermaid"] .node rect,
|
||||||
|
.mermaid svg[id^="mermaid"] .node circle,
|
||||||
|
.mermaid svg[id^="mermaid"] .node ellipse,
|
||||||
|
.mermaid svg[id^="mermaid"] .node polygon,
|
||||||
|
.mermaid svg[id^="mermaid"] .node path,
|
||||||
|
svg[id^="mermaid"] .node rect,
|
||||||
|
svg[id^="mermaid"] .node circle,
|
||||||
|
svg[id^="mermaid"] .node ellipse,
|
||||||
|
svg[id^="mermaid"] .node polygon,
|
||||||
|
svg[id^="mermaid"] .node path {
|
||||||
|
fill: #0074d9 !important;
|
||||||
|
stroke: #005db0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node text - white on colored background, Inter font */
|
||||||
|
.mermaid svg[id^="mermaid"] .node .label,
|
||||||
|
.mermaid svg[id^="mermaid"] .nodeLabel,
|
||||||
|
.mermaid svg[id^="mermaid"] .node text,
|
||||||
|
.mermaid svg[id^="mermaid"] .node foreignObject div,
|
||||||
|
.mermaid svg[id^="mermaid"] .node foreignObject span,
|
||||||
|
.mermaid svg[id^="mermaid"] .node foreignObject p,
|
||||||
|
svg[id^="mermaid"] .node .label,
|
||||||
|
svg[id^="mermaid"] .nodeLabel,
|
||||||
|
svg[id^="mermaid"] .node text,
|
||||||
|
svg[id^="mermaid"] .node foreignObject div,
|
||||||
|
svg[id^="mermaid"] .node foreignObject span,
|
||||||
|
svg[id^="mermaid"] .node foreignObject p {
|
||||||
|
color: #ffffff !important;
|
||||||
|
fill: #ffffff !important;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
font-size: 0.9375rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge/arrow lines - gradient end color (#009296 teal) */
|
||||||
|
.mermaid svg[id^="mermaid"] .edgePath .path,
|
||||||
|
.mermaid svg[id^="mermaid"] .flowchart-link,
|
||||||
|
svg[id^="mermaid"] .edgePath .path,
|
||||||
|
svg[id^="mermaid"] .flowchart-link {
|
||||||
|
stroke: #009296 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Arrow heads - gradient end color */
|
||||||
|
.mermaid svg[id^="mermaid"] .marker path,
|
||||||
|
.mermaid svg[id^="mermaid"] .arrowheadPath,
|
||||||
|
.mermaid svg[id^="mermaid"] marker path,
|
||||||
|
svg[id^="mermaid"] .marker path,
|
||||||
|
svg[id^="mermaid"] .arrowheadPath,
|
||||||
|
svg[id^="mermaid"] marker path {
|
||||||
|
fill: #009296 !important;
|
||||||
|
stroke: #009296 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge labels - transparent background, Inter font */
|
||||||
|
.mermaid svg[id^="mermaid"] .edgeLabel,
|
||||||
|
.mermaid svg[id^="mermaid"] .edgeLabel rect,
|
||||||
|
.mermaid svg[id^="mermaid"] .edgeLabel span,
|
||||||
|
.mermaid svg[id^="mermaid"] .labelBkg,
|
||||||
|
svg[id^="mermaid"] .edgeLabel,
|
||||||
|
svg[id^="mermaid"] .edgeLabel rect,
|
||||||
|
svg[id^="mermaid"] .edgeLabel span,
|
||||||
|
svg[id^="mermaid"] .labelBkg {
|
||||||
|
background: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
fill: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid svg[id^="mermaid"] .edgeLabel text,
|
||||||
|
.mermaid svg[id^="mermaid"] .edgeLabel span,
|
||||||
|
.mermaid svg[id^="mermaid"] .edgeLabel p,
|
||||||
|
svg[id^="mermaid"] .edgeLabel text,
|
||||||
|
svg[id^="mermaid"] .edgeLabel span,
|
||||||
|
svg[id^="mermaid"] .edgeLabel p {
|
||||||
|
color: var(--ifm-font-color-base) !important;
|
||||||
|
fill: var(--ifm-font-color-base) !important;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cluster/subgraph boxes - gradient end color border (#009296) */
|
||||||
|
.mermaid svg[id^="mermaid"] .cluster rect,
|
||||||
|
svg[id^="mermaid"] .cluster rect {
|
||||||
|
fill: rgba(0, 146, 150, 0.08) !important;
|
||||||
|
stroke: #009296 !important;
|
||||||
|
stroke-width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cluster labels - Inter font */
|
||||||
|
.mermaid svg[id^="mermaid"] .cluster text,
|
||||||
|
.mermaid svg[id^="mermaid"] .cluster .nodeLabel,
|
||||||
|
.mermaid svg[id^="mermaid"] .cluster-label text,
|
||||||
|
.mermaid svg[id^="mermaid"] .cluster-label span,
|
||||||
|
.mermaid svg[id^="mermaid"] .cluster-label p,
|
||||||
|
svg[id^="mermaid"] .cluster text,
|
||||||
|
svg[id^="mermaid"] .cluster .nodeLabel,
|
||||||
|
svg[id^="mermaid"] .cluster-label text,
|
||||||
|
svg[id^="mermaid"] .cluster-label span,
|
||||||
|
svg[id^="mermaid"] .cluster-label p {
|
||||||
|
color: var(--ifm-font-color-base) !important;
|
||||||
|
fill: var(--ifm-font-color-base) !important;
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
font-size: 0.9375rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All mermaid text should use Inter */
|
||||||
|
.mermaid svg[id^="mermaid"] text,
|
||||||
|
.mermaid svg[id^="mermaid"] span,
|
||||||
|
.mermaid svg[id^="mermaid"] p,
|
||||||
|
svg[id^="mermaid"] text,
|
||||||
|
svg[id^="mermaid"] span,
|
||||||
|
svg[id^="mermaid"] p {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode adjustments */
|
||||||
|
[data-theme='dark'] .mermaid svg[id^="mermaid"] .edgeLabel text,
|
||||||
|
[data-theme='dark'] .mermaid svg[id^="mermaid"] .edgeLabel span,
|
||||||
|
[data-theme='dark'] .mermaid svg[id^="mermaid"] .edgeLabel p,
|
||||||
|
[data-theme='dark'] svg[id^="mermaid"] .edgeLabel text,
|
||||||
|
[data-theme='dark'] svg[id^="mermaid"] .edgeLabel span,
|
||||||
|
[data-theme='dark'] svg[id^="mermaid"] .edgeLabel p {
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
fill: #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster text,
|
||||||
|
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster-label text,
|
||||||
|
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster-label span,
|
||||||
|
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster-label p,
|
||||||
|
[data-theme='dark'] svg[id^="mermaid"] .cluster text,
|
||||||
|
[data-theme='dark'] svg[id^="mermaid"] .cluster-label text,
|
||||||
|
[data-theme='dark'] svg[id^="mermaid"] .cluster-label span,
|
||||||
|
[data-theme='dark'] svg[id^="mermaid"] .cluster-label p {
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
fill: #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] .mermaid svg[id^="mermaid"] .cluster rect,
|
||||||
|
[data-theme='dark'] svg[id^="mermaid"] .cluster rect {
|
||||||
|
fill: rgba(0, 146, 150, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* List styling */
|
/* List styling */
|
||||||
article ul, article ol {
|
article ul, article ol {
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Hindsight CLI installer
|
# Hindsight CLI installer
|
||||||
# Usage: curl -sSf https://your-domain.com/install.sh | sh
|
# Usage: curl -fsSL https://hindsight.vectorize.io/get-cli | bash
|
||||||
|
|
||||||
REPO_URL="https://github.com/vectorize-io/hindsight"
|
REPO_URL="https://github.com/vectorize-io/hindsight"
|
||||||
INSTALL_DIR="${HINDSIGHT_INSTALL_DIR:-$HOME/.local/bin}"
|
INSTALL_DIR="${HINDSIGHT_INSTALL_DIR:-$HOME/.local/bin}"
|
||||||
BIN
hindsight-docs/static/img/favicon.png
Normal file
BIN
hindsight-docs/static/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
hindsight-docs/static/img/logo.png
Normal file
BIN
hindsight-docs/static/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
|
|
@ -1,17 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#7c3aed;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#a78bfa;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<circle cx="50" cy="50" r="45" fill="url(#grad)"/>
|
|
||||||
<circle cx="50" cy="35" r="12" fill="white" opacity="0.9"/>
|
|
||||||
<circle cx="35" cy="55" r="8" fill="white" opacity="0.7"/>
|
|
||||||
<circle cx="65" cy="55" r="8" fill="white" opacity="0.7"/>
|
|
||||||
<circle cx="50" cy="70" r="6" fill="white" opacity="0.5"/>
|
|
||||||
<line x1="50" y1="47" x2="35" y2="55" stroke="white" stroke-width="2" opacity="0.6"/>
|
|
||||||
<line x1="50" y1="47" x2="65" y2="55" stroke="white" stroke-width="2" opacity="0.6"/>
|
|
||||||
<line x1="35" y1="55" x2="50" y2="70" stroke="white" stroke-width="2" opacity="0.5"/>
|
|
||||||
<line x1="65" y1="55" x2="50" y2="70" stroke="white" stroke-width="2" opacity="0.5"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 968 B |
File diff suppressed because it is too large
Load diff
|
|
@ -1046,6 +1046,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Banks"
|
||||||
|
],
|
||||||
|
"summary": "Delete memory bank",
|
||||||
|
"description": "Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. This is a destructive operation that cannot be undone.",
|
||||||
|
"operationId": "delete_bank",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "bank_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Bank Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeleteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/default/banks/{bank_id}/memories": {
|
"/v1/default/banks/{bank_id}/memories": {
|
||||||
|
|
@ -1494,6 +1535,28 @@
|
||||||
"success": {
|
"success": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Success"
|
"title": "Success"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Message"
|
||||||
|
},
|
||||||
|
"deleted_count": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Deleted Count"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -1503,7 +1566,9 @@
|
||||||
"title": "DeleteResponse",
|
"title": "DeleteResponse",
|
||||||
"description": "Response model for delete operations.",
|
"description": "Response model for delete operations.",
|
||||||
"example": {
|
"example": {
|
||||||
"success": true
|
"success": true,
|
||||||
|
"message": "Deleted successfully",
|
||||||
|
"deleted_count": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DispositionTraits": {
|
"DispositionTraits": {
|
||||||
|
|
|
||||||
BIN
hindsight-favicon.png
Normal file
BIN
hindsight-favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
67
openapi.json
67
openapi.json
|
|
@ -1046,6 +1046,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Banks"
|
||||||
|
],
|
||||||
|
"summary": "Delete memory bank",
|
||||||
|
"description": "Delete an entire memory bank including all memories, entities, documents, and the bank profile itself. This is a destructive operation that cannot be undone.",
|
||||||
|
"operationId": "delete_bank",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "bank_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Bank Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeleteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/default/banks/{bank_id}/memories": {
|
"/v1/default/banks/{bank_id}/memories": {
|
||||||
|
|
@ -1494,6 +1535,28 @@
|
||||||
"success": {
|
"success": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "Success"
|
"title": "Success"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Message"
|
||||||
|
},
|
||||||
|
"deleted_count": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Deleted Count"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
@ -1503,7 +1566,9 @@
|
||||||
"title": "DeleteResponse",
|
"title": "DeleteResponse",
|
||||||
"description": "Response model for delete operations.",
|
"description": "Response model for delete operations.",
|
||||||
"example": {
|
"example": {
|
||||||
"success": true
|
"success": true,
|
||||||
|
"message": "Deleted successfully",
|
||||||
|
"deleted_count": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DispositionTraits": {
|
"DispositionTraits": {
|
||||||
|
|
|
||||||
|
|
@ -3,51 +3,13 @@ set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/../.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
SERVER_ARGS=()
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--help|-h)
|
|
||||||
echo "Usage: $0 [--env local|dev] [uvicorn options...]"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " --env local Use local environment (default)"
|
|
||||||
echo " --env dev Use dev environment"
|
|
||||||
echo ""
|
|
||||||
echo "Uvicorn options (passed to server):"
|
|
||||||
echo " --host HOST Host to bind to (default: 0.0.0.0)"
|
|
||||||
echo " --port PORT Port to bind to (default: 8888)"
|
|
||||||
echo " --reload Enable auto-reload on code changes"
|
|
||||||
echo " --workers WORKERS Number of worker processes (default: 1)"
|
|
||||||
echo " --log-level LEVEL Log level: critical/error/warning/info/debug/trace"
|
|
||||||
echo " --access-log Enable access log"
|
|
||||||
echo " --no-access-log Disable access log"
|
|
||||||
echo " --proxy-headers Enable X-Forwarded-Proto, X-Forwarded-For headers"
|
|
||||||
echo " --forwarded-allow-ips Comma separated list of IPs to trust"
|
|
||||||
echo " --ssl-keyfile FILE SSL key file"
|
|
||||||
echo " --ssl-certfile FILE SSL certificate file"
|
|
||||||
echo ""
|
|
||||||
echo "Example:"
|
|
||||||
echo " $0 --env dev --reload --port 8888 --log-level debug"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# Pass all other arguments to the server
|
|
||||||
SERVER_ARGS+=("$1")
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Source environment file
|
|
||||||
ENV_FILE=".env"
|
ENV_FILE=".env"
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
echo "Error: Environment file $ENV_FILE not found at project root."
|
echo "Error: Environment file $ENV_FILE not found at project root."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "📄 Loading environment from $ENV_FILE"
|
echo "Loading environment from $ENV_FILE"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Export all variables from env file
|
# Export all variables from env file
|
||||||
|
|
@ -55,21 +17,4 @@ set -a
|
||||||
source "$ENV_FILE"
|
source "$ENV_FILE"
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
# Extract port from SERVER_ARGS if provided, otherwise use default
|
|
||||||
PORT=8888
|
|
||||||
for ((i=0; i<${#SERVER_ARGS[@]}; i++)); do
|
|
||||||
if [[ "${SERVER_ARGS[$i]}" == "--port" ]]; then
|
|
||||||
PORT="${SERVER_ARGS[$((i+1))]}"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Server will be available at: http://localhost:${PORT}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Set default arguments if not provided
|
|
||||||
if [[ ${#SERVER_ARGS[@]} -eq 0 ]]; then
|
|
||||||
SERVER_ARGS=(--host 0.0.0.0 --port 8888)
|
|
||||||
fi
|
|
||||||
|
|
||||||
uv run hindsight-api "${SERVER_ARGS[@]}"
|
uv run hindsight-api "${SERVER_ARGS[@]}"
|
||||||
|
|
|
||||||
8
uv.lock
8
uv.lock
|
|
@ -1141,7 +1141,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hindsight-all"
|
name = "hindsight-all"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
source = { editable = "hindsight" }
|
source = { editable = "hindsight" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "hindsight-api" },
|
{ name = "hindsight-api" },
|
||||||
|
|
@ -1165,7 +1165,7 @@ provides-extras = ["test"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hindsight-api"
|
name = "hindsight-api"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
source = { editable = "hindsight-api" }
|
source = { editable = "hindsight-api" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
|
|
@ -1265,7 +1265,7 @@ dev = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hindsight-client"
|
name = "hindsight-client"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
source = { editable = "hindsight-clients/python" }
|
source = { editable = "hindsight-clients/python" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|
@ -1297,7 +1297,7 @@ provides-extras = ["test"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hindsight-dev"
|
name = "hindsight-dev"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
source = { editable = "hindsight-dev" }
|
source = { editable = "hindsight-dev" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "hindsight-api" },
|
{ name = "hindsight-api" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue