brandind and misc fixes
This commit is contained in:
@@ -145,3 +145,7 @@ Note: The maintained wrapper `hindsight_client.py` and `README.md` are preserved
|
||||
- PostgreSQL with pgvector extension
|
||||
- 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`
|
||||
|
||||
# Branding
|
||||
## Colors
|
||||
- Primary: gradient from #0074d9 to #009296
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
[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://pypi.org/project/hindsight-api/)
|
||||
[](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)
|
||||
|
||||
|
||||
@@ -53,12 +54,10 @@ Memories in Hindsight are stored in banks (e.g. memory banks). When memories are
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-key
|
||||
|
||||
docker run -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||
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_MODEL=gpt-4o-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
**Documentation:** [vectorize-io.github.io/hindsight](https://vectorize-io.github.io/hindsight)
|
||||
**Documentation:**
|
||||
- [https://hindsight.vectorize.io](https://hindsight.vectorize.io)
|
||||
|
||||
**Clients:**
|
||||
- [Python](http://hindsight.vectorize.io/sdks/python)
|
||||
- [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:**
|
||||
- [Slack](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting Hindsight..."
|
||||
echo ""
|
||||
|
||||
# Service flags (default to true if not set)
|
||||
ENABLE_API="${HINDSIGHT_ENABLE_API:-true}"
|
||||
ENABLE_CP="${HINDSIGHT_ENABLE_CP:-true}"
|
||||
@@ -31,16 +28,14 @@ if [ "$ENABLE_API" = "true" ]; then
|
||||
PIDS+=($API_PID)
|
||||
|
||||
# Wait for API to be ready
|
||||
echo "⏳ Waiting for API..."
|
||||
for i in {1..60}; do
|
||||
if curl -sf http://localhost:8888/health &>/dev/null; then
|
||||
echo "✅ API is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "⏭️ API disabled (HINDSIGHT_ENABLE_API=false)"
|
||||
echo "API disabled (HINDSIGHT_ENABLE_API=false)"
|
||||
fi
|
||||
|
||||
# Start Control Plane if enabled
|
||||
@@ -51,7 +46,7 @@ if [ "$ENABLE_CP" = "true" ]; then
|
||||
CP_PID=$!
|
||||
PIDS+=($CP_PID)
|
||||
else
|
||||
echo "⏭️ Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
|
||||
echo "Control Plane disabled (HINDSIGHT_ENABLE_CP=false)"
|
||||
fi
|
||||
|
||||
# Print status
|
||||
|
||||
@@ -672,11 +672,15 @@ class DeleteResponse(BaseModel):
|
||||
"""Response model for delete operations."""
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"success": True
|
||||
"success": True,
|
||||
"message": "Deleted successfully",
|
||||
"deleted_count": 10
|
||||
}
|
||||
})
|
||||
|
||||
success: bool
|
||||
message: Optional[str] = None
|
||||
deleted_count: Optional[int] = None
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
@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(
|
||||
"/v1/default/banks/{bank_id}/memories",
|
||||
response_model=RetainResponse,
|
||||
|
||||
@@ -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_DATABASE_URL = "pg0"
|
||||
DEFAULT_LLM_PROVIDER = "groq"
|
||||
DEFAULT_LLM_MODEL = "openai/gpt-oss-20b"
|
||||
DEFAULT_LLM_PROVIDER = "openai"
|
||||
DEFAULT_LLM_MODEL = "gpt-5-mini"
|
||||
|
||||
DEFAULT_EMBEDDINGS_PROVIDER = "local"
|
||||
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._gemini_client = None
|
||||
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
|
||||
|
||||
logger.info(
|
||||
f"Initialized LLM: provider={self.provider}, model={self.model}, base_url={self.base_url}"
|
||||
)
|
||||
async def verify_connection(self) -> None:
|
||||
"""
|
||||
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(
|
||||
self,
|
||||
@@ -149,7 +172,12 @@ class LLMProvider:
|
||||
|
||||
if max_completion_tokens is not None:
|
||||
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
|
||||
|
||||
# Provider-specific parameters
|
||||
@@ -216,7 +244,8 @@ class LLMProvider:
|
||||
except APIConnectionError as e:
|
||||
last_exception = e
|
||||
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)
|
||||
await asyncio.sleep(backoff)
|
||||
continue
|
||||
|
||||
@@ -453,12 +453,17 @@ class MemoryEngine:
|
||||
# Query analyzer load is sync and CPU-bound
|
||||
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
|
||||
await asyncio.gather(
|
||||
start_pg0(),
|
||||
init_embeddings(),
|
||||
init_cross_encoder(),
|
||||
init_query_analyzer(),
|
||||
verify_llm(),
|
||||
)
|
||||
|
||||
# Run database migrations if enabled
|
||||
@@ -1791,10 +1796,14 @@ class MemoryEngine:
|
||||
# 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)
|
||||
|
||||
# Delete the bank profile itself
|
||||
await conn.execute("DELETE FROM banks WHERE bank_id = $1", bank_id)
|
||||
|
||||
return {
|
||||
"memory_units_deleted": units_count,
|
||||
"entities_deleted": entities_count,
|
||||
"documents_deleted": documents_count
|
||||
"documents_deleted": documents_count,
|
||||
"bank_deleted": True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -1839,10 +1848,11 @@ class MemoryEngine:
|
||||
""", *query_params)
|
||||
|
||||
# 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]
|
||||
if unit_ids:
|
||||
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.to_unit_id,
|
||||
ml.link_type,
|
||||
@@ -1851,7 +1861,7 @@ class MemoryEngine:
|
||||
FROM memory_links ml
|
||||
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[])
|
||||
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)
|
||||
else:
|
||||
links = []
|
||||
|
||||
@@ -390,6 +390,27 @@ async def create_temporal_links_batch_per_fact(
|
||||
# Filter and create links in memory (much faster than N queries)
|
||||
link_gen_start = time_mod.time()
|
||||
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")
|
||||
|
||||
if links:
|
||||
@@ -514,9 +535,38 @@ async def create_semantic_links_batch(
|
||||
|
||||
for idx in sorted_indices:
|
||||
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))
|
||||
|
||||
# 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")
|
||||
|
||||
if all_links:
|
||||
|
||||
@@ -21,6 +21,10 @@ from . import MemoryEngine
|
||||
from .api import create_app
|
||||
from .config import get_config, HindsightConfig
|
||||
|
||||
from .banner import print_banner
|
||||
print()
|
||||
print_banner()
|
||||
|
||||
# Filter deprecation warnings from third-party libraries
|
||||
warnings.filterwarnings("ignore", message="websockets.legacy is deprecated")
|
||||
warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProtocol is deprecated")
|
||||
@@ -184,15 +188,19 @@ def main():
|
||||
if 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}")
|
||||
print(f" LLM: {config.llm_provider} / {config.llm_model}")
|
||||
print(f" Embeddings: {config.embeddings_provider}")
|
||||
print(f" Reranker: {config.reranker_provider}")
|
||||
if config.mcp_enabled:
|
||||
print(f" MCP: enabled at /mcp")
|
||||
print()
|
||||
|
||||
|
||||
from .banner import print_startup_info
|
||||
print_startup_info(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
database_url=config.database_url,
|
||||
llm_provider=config.llm_provider,
|
||||
llm_model=config.llm_model,
|
||||
embeddings_provider=config.embeddings_provider,
|
||||
reranker_provider=config.reranker_provider,
|
||||
mcp_enabled=config.mcp_enabled,
|
||||
)
|
||||
|
||||
uvicorn.run(**uvicorn_config)
|
||||
|
||||
|
||||
@@ -257,16 +257,17 @@ class EmbeddedPostgres:
|
||||
last_error = stderr or f"pg0 start returned exit code {returncode}"
|
||||
if attempt < max_retries:
|
||||
delay = retry_delay * (2 ** (attempt - 1))
|
||||
logger.warning(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||
logger.info(f"Retrying in {delay:.1f}s...")
|
||||
logger.debug(f"pg0 start attempt {attempt}/{max_retries} failed: {last_error.strip()}")
|
||||
logger.debug(f"Retrying in {delay:.1f}s...")
|
||||
await asyncio.sleep(delay)
|
||||
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
|
||||
uri = f"postgresql://{self.username}:{self.password}@localhost:{self.port}/{self.database}"
|
||||
logger.warning(f"All pg0 start attempts failed, using constructed URI: {uri}")
|
||||
return uri
|
||||
# All retries exhausted - fail
|
||||
raise RuntimeError(
|
||||
f"Failed to start embedded PostgreSQL after {max_retries} attempts. "
|
||||
f"Last error: {last_error.strip() if last_error else 'unknown'}"
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the PostgreSQL server."""
|
||||
|
||||
@@ -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 logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from hindsight_api.engine.memory_engine import Budget
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1595,3 +1595,133 @@ async def test_all_link_types_together(memory):
|
||||
|
||||
finally:
|
||||
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
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# HTTP client (for timeout configuration)
|
||||
reqwest = "0.12"
|
||||
|
||||
# Serialization (for config and output formatting)
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::collections::HashMap;
|
||||
// Types not defined in OpenAPI spec (TODO: add to openapi.json)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AgentStats {
|
||||
pub agent_id: String,
|
||||
pub bank_id: String,
|
||||
pub total_nodes: i32,
|
||||
pub total_links: i32,
|
||||
pub total_documents: i32,
|
||||
@@ -38,7 +38,7 @@ pub struct Operation {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct OperationsResponse {
|
||||
pub agent_id: String,
|
||||
pub bank_id: String,
|
||||
pub operations: Vec<Operation>,
|
||||
}
|
||||
|
||||
@@ -66,7 +66,13 @@ pub struct ApiClient {
|
||||
impl ApiClient {
|
||||
pub fn new(base_url: String) -> Result<Self> {
|
||||
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 })
|
||||
}
|
||||
|
||||
@@ -231,6 +237,13 @@ impl ApiClient {
|
||||
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
|
||||
|
||||
@@ -12,8 +12,8 @@ pub fn list(client: &ApiClient, verbose: bool, output_format: OutputFormat) -> R
|
||||
|
||||
let response = client.list_agents(verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
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 {
|
||||
Some(ui::create_spinner("Fetching profile..."))
|
||||
Some(ui::create_spinner("Fetching disposition..."))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let response = client.get_profile(bank_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(profile) => {
|
||||
if output_format == OutputFormat::Pretty {
|
||||
ui::print_profile(&profile);
|
||||
ui::print_disposition(&profile);
|
||||
} else {
|
||||
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);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(stats) => {
|
||||
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!(" 📊 Overview");
|
||||
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");
|
||||
println!("{}", ui::gradient_text("─── Memory Units by Type ───"));
|
||||
let mut fact_types: Vec<_> = stats.nodes_by_fact_type.iter().collect();
|
||||
fact_types.sort_by_key(|(k, _)| *k);
|
||||
for (fact_type, count) in fact_types {
|
||||
let icon = match fact_type.as_str() {
|
||||
"world" => "🌍",
|
||||
"agent" => "🤖",
|
||||
"opinion" => "💭",
|
||||
_ => "•"
|
||||
};
|
||||
println!(" {} {:<10} {}", icon, fact_type, count);
|
||||
for (i, (fact_type, count)) in fact_types.iter().enumerate() {
|
||||
let t = i as f32 / fact_types.len().max(1) as f32;
|
||||
println!(" {:<10} {}", fact_type, ui::gradient(&count.to_string(), t));
|
||||
}
|
||||
println!();
|
||||
|
||||
println!(" 🔗 Links by Type");
|
||||
println!("{}", ui::gradient_text("─── Links by Type ───"));
|
||||
let mut link_types: Vec<_> = stats.links_by_link_type.iter().collect();
|
||||
link_types.sort_by_key(|(k, _)| *k);
|
||||
for (link_type, count) in link_types {
|
||||
let icon = match link_type.as_str() {
|
||||
"temporal" => "⏰",
|
||||
"semantic" => "🔤",
|
||||
"entity" => "🏷️",
|
||||
_ => "•"
|
||||
};
|
||||
println!(" {} {:<10} {}", icon, link_type, count);
|
||||
for (i, (link_type, count)) in link_types.iter().enumerate() {
|
||||
let t = i as f32 / link_types.len().max(1) as f32;
|
||||
println!(" {:<10} {}", link_type, ui::gradient(&count.to_string(), t));
|
||||
}
|
||||
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();
|
||||
fact_type_links.sort_by_key(|(k, _)| *k);
|
||||
for (fact_type, count) in fact_type_links {
|
||||
let icon = match fact_type.as_str() {
|
||||
"world" => "🌍",
|
||||
"agent" => "🤖",
|
||||
"opinion" => "💭",
|
||||
_ => "•"
|
||||
};
|
||||
println!(" {} {:<10} {}", icon, fact_type, count);
|
||||
for (i, (fact_type, count)) in fact_type_links.iter().enumerate() {
|
||||
let t = i as f32 / fact_type_links.len().max(1) as f32;
|
||||
println!(" {:<10} {}", fact_type, ui::gradient(&count.to_string(), t));
|
||||
}
|
||||
println!();
|
||||
|
||||
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();
|
||||
fact_types.sort_by_key(|(k, _)| *k);
|
||||
for (fact_type, link_types) in fact_types {
|
||||
let icon = match fact_type.as_str() {
|
||||
"world" => "🌍",
|
||||
"agent" => "🤖",
|
||||
"opinion" => "💭",
|
||||
_ => "•"
|
||||
};
|
||||
println!(" {} {}", icon, fact_type);
|
||||
println!(" {}", fact_type);
|
||||
let mut sorted_links: Vec<_> = link_types.iter().collect();
|
||||
sorted_links.sort_by_key(|(k, _)| *k);
|
||||
for (link_type, count) in sorted_links {
|
||||
println!(" - {:<10} {}", link_type, count);
|
||||
println!(" {:<10} {}", ui::dim(link_type), count);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if stats.pending_operations > 0 || stats.failed_operations > 0 {
|
||||
println!(" ⚙️ Operations");
|
||||
println!("{}", ui::gradient_text("─── Operations ───"));
|
||||
if stats.pending_operations > 0 {
|
||||
println!(" ⏳ Pending: {}", stats.pending_operations);
|
||||
println!(" {} {}", ui::dim("pending:"), stats.pending_operations);
|
||||
}
|
||||
if stats.failed_operations > 0 {
|
||||
println!(" ❌ Failed: {}", stats.failed_operations);
|
||||
println!(" {} {}", ui::dim("failed:"), stats.failed_operations);
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -216,8 +193,8 @@ pub fn update_background(
|
||||
|
||||
let response = client.add_background(bank_id, content, !no_update_disposition, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -244,3 +221,57 @@ pub fn update_background(
|
||||
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);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(docs_response) => {
|
||||
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 {
|
||||
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");
|
||||
@@ -65,8 +65,8 @@ pub fn get(
|
||||
|
||||
let response = client.get_document(agent_id, document_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -102,8 +102,8 @@ pub fn delete(
|
||||
|
||||
let response = client.delete_document(agent_id, document_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
|
||||
@@ -18,8 +18,8 @@ pub fn list(
|
||||
|
||||
let response = client.list_entities(bank_id, Some(limit), verbose)?;
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
if output_format == OutputFormat::Pretty {
|
||||
@@ -66,8 +66,8 @@ pub fn get(
|
||||
|
||||
let response = client.get_entity(bank_id, entity_id, verbose)?;
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
if output_format == OutputFormat::Pretty {
|
||||
@@ -84,17 +84,6 @@ pub fn get(
|
||||
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!();
|
||||
} else {
|
||||
output::print_output(&response, output_format)?;
|
||||
@@ -118,8 +107,8 @@ pub fn regenerate(
|
||||
|
||||
let response = client.regenerate_entity(bank_id, entity_id, verbose)?;
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
if output_format == OutputFormat::Pretty {
|
||||
|
||||
@@ -16,8 +16,15 @@ use ratatui::{
|
||||
Frame, Terminal,
|
||||
};
|
||||
use std::io;
|
||||
use std::sync::mpsc::{self, Receiver, TryRecvError};
|
||||
use std::thread;
|
||||
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)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum View {
|
||||
@@ -61,6 +68,12 @@ enum InputMode {
|
||||
Query,
|
||||
}
|
||||
|
||||
/// Query result from background thread
|
||||
enum QueryResult {
|
||||
Recall(Result<Vec<RecallResult>, String>),
|
||||
Reflect(Result<String, String>),
|
||||
}
|
||||
|
||||
/// Application state
|
||||
struct App {
|
||||
client: ApiClient,
|
||||
@@ -114,6 +127,9 @@ struct App {
|
||||
auto_refresh_enabled: bool,
|
||||
last_refresh: Instant,
|
||||
refresh_interval: Duration,
|
||||
|
||||
// Background query receiver
|
||||
query_receiver: Option<Receiver<QueryResult>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -160,6 +176,8 @@ impl App {
|
||||
auto_refresh_enabled: true,
|
||||
last_refresh: Instant::now(),
|
||||
refresh_interval: Duration::from_secs(5),
|
||||
|
||||
query_receiver: None,
|
||||
};
|
||||
|
||||
// Select first item by default
|
||||
@@ -288,58 +306,106 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn execute_query(&mut self) -> Result<()> {
|
||||
fn execute_query(&mut self) {
|
||||
if let View::Query(bank_id) = &self.view {
|
||||
if self.query_text.is_empty() {
|
||||
self.error_message = "Query cannot be empty".to_string();
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
self.loading = true;
|
||||
self.error_message.clear();
|
||||
self.input_mode = InputMode::Normal;
|
||||
|
||||
match self.query_mode {
|
||||
QueryMode::Recall => {
|
||||
let request = RecallRequest {
|
||||
query: self.query_text.clone(),
|
||||
types: None,
|
||||
budget: Some(self.query_budget.clone()),
|
||||
max_tokens: self.query_max_tokens,
|
||||
trace: false,
|
||||
query_timestamp: None,
|
||||
include: None,
|
||||
};
|
||||
// Create channel for receiving results
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.query_receiver = Some(rx);
|
||||
|
||||
let response = self.client.recall(bank_id, &request, false)?;
|
||||
self.query_results = response.results;
|
||||
// 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 => {
|
||||
let request = RecallRequest {
|
||||
query: query_text,
|
||||
types: None,
|
||||
budget: Some(query_budget),
|
||||
max_tokens: query_max_tokens,
|
||||
trace: false,
|
||||
query_timestamp: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
let result = client.recall(&bank_id, &request, false)
|
||||
.map(|r| r.results)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
let _ = tx.send(QueryResult::Recall(result));
|
||||
}
|
||||
QueryMode::Reflect => {
|
||||
let request = ReflectRequest {
|
||||
query: query_text,
|
||||
budget: Some(query_budget),
|
||||
context: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
let result = client.reflect(&bank_id, &request, false)
|
||||
.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;
|
||||
}
|
||||
QueryMode::Reflect => {
|
||||
let request = ReflectRequest {
|
||||
query: self.query_text.clone(),
|
||||
budget: Some(self.query_budget.clone()),
|
||||
context: None,
|
||||
include: None,
|
||||
};
|
||||
|
||||
let response = self.client.reflect(bank_id, &request, false)?;
|
||||
self.query_response = response.text;
|
||||
|
||||
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.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) {
|
||||
@@ -700,64 +766,64 @@ fn render_control_bar(f: &mut Frame, app: &App, area: Rect) {
|
||||
// Build contextual shortcuts based on view and input mode
|
||||
let shortcuts = match (&app.view, &app.input_mode) {
|
||||
(View::Banks, InputMode::Normal) => vec![
|
||||
("Enter", "Select", Color::Cyan),
|
||||
("R", "Refresh", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("Enter", "Select", BRAND_START),
|
||||
("R", "Refresh", BRAND_MID),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
(View::Memories(_), InputMode::Normal) => vec![
|
||||
("Enter", "View", Color::Cyan),
|
||||
("/", "Query", Color::Green),
|
||||
("←→", "Scroll", Color::Cyan),
|
||||
("n", "Next", Color::Green),
|
||||
("p", "Prev", Color::Green),
|
||||
("Esc", "Back", Color::Yellow),
|
||||
("R", "Refresh", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("Enter", "View", BRAND_START),
|
||||
("/", "Query", BRAND_MID),
|
||||
("←→", "Scroll", BRAND_START),
|
||||
("n", "Next", BRAND_MID),
|
||||
("p", "Prev", BRAND_MID),
|
||||
("Esc", "Back", BRAND_END),
|
||||
("R", "Refresh", BRAND_END),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
(View::Entities(_), InputMode::Normal) => vec![
|
||||
("Enter", "View", Color::Cyan),
|
||||
("/", "Query", Color::Green),
|
||||
("←→", "Scroll", Color::Cyan),
|
||||
("Esc", "Back", Color::Yellow),
|
||||
("R", "Refresh", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("Enter", "View", BRAND_START),
|
||||
("/", "Query", BRAND_MID),
|
||||
("←→", "Scroll", BRAND_START),
|
||||
("Esc", "Back", BRAND_END),
|
||||
("R", "Refresh", BRAND_END),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
(View::Documents(_), InputMode::Normal) => vec![
|
||||
("Enter", "View", Color::Cyan),
|
||||
("/", "Query", Color::Green),
|
||||
("←→", "Scroll", Color::Cyan),
|
||||
("Enter", "View", BRAND_START),
|
||||
("/", "Query", BRAND_MID),
|
||||
("←→", "Scroll", BRAND_START),
|
||||
("Del", "Delete", Color::Red),
|
||||
("Esc", "Back", Color::Yellow),
|
||||
("R", "Refresh", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("Esc", "Back", BRAND_END),
|
||||
("R", "Refresh", BRAND_END),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
],
|
||||
(View::Query(_), InputMode::Normal) => {
|
||||
let mut shortcuts = vec![
|
||||
("/", "Query", Color::Green),
|
||||
("m", "Mode", Color::Cyan),
|
||||
("/", "Query", BRAND_MID),
|
||||
("m", "Mode", BRAND_START),
|
||||
];
|
||||
if app.query_mode == QueryMode::Recall {
|
||||
shortcuts.push(("←→", "Scroll", Color::Cyan));
|
||||
shortcuts.push(("←→", "Scroll", BRAND_START));
|
||||
}
|
||||
shortcuts.extend_from_slice(&[
|
||||
("b", "Budget", Color::Yellow),
|
||||
("+/-", "Tokens", Color::Yellow),
|
||||
("Esc", "Back", Color::Yellow),
|
||||
("?", "Help", Color::Magenta),
|
||||
("b", "Budget", BRAND_END),
|
||||
("+/-", "Tokens", BRAND_END),
|
||||
("Esc", "Back", BRAND_END),
|
||||
("?", "Help", BRAND_END),
|
||||
("q", "Quit", Color::Red),
|
||||
]);
|
||||
shortcuts
|
||||
},
|
||||
(View::Query(_), InputMode::Query) => vec![
|
||||
("Enter", "Execute", Color::Green),
|
||||
("Enter", "Execute", BRAND_MID),
|
||||
("Esc", "Cancel", Color::Red),
|
||||
],
|
||||
_ => vec![
|
||||
("?", "Help", Color::Magenta),
|
||||
("?", "Help", BRAND_END),
|
||||
("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)
|
||||
.block(Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(BRAND_START))
|
||||
.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);
|
||||
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)
|
||||
.block(Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(BRAND_START))
|
||||
.title(" Shortcuts "))
|
||||
.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 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)
|
||||
.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),
|
||||
])
|
||||
} 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() {
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(&app.status_message, Style::default().fg(Color::Green)),
|
||||
Span::styled(&app.status_message, Style::default().fg(BRAND_MID)),
|
||||
])
|
||||
} else {
|
||||
Line::from("")
|
||||
@@ -928,7 +994,7 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let metadata = Paragraph::new(metadata_text)
|
||||
.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]);
|
||||
|
||||
@@ -946,7 +1012,7 @@ fn render_memories(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let mut items = vec![
|
||||
// Header row
|
||||
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
|
||||
@@ -1002,7 +1068,7 @@ fn render_entities(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let metadata = Paragraph::new(metadata_text)
|
||||
.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 });
|
||||
|
||||
f.render_widget(metadata, area);
|
||||
@@ -1011,7 +1077,7 @@ fn render_entities(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let mut items = vec![
|
||||
// Header row
|
||||
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
|
||||
@@ -1071,7 +1137,7 @@ fn render_documents(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let metadata = Paragraph::new(metadata_text)
|
||||
.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]);
|
||||
|
||||
@@ -1091,7 +1157,7 @@ fn render_documents(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let mut items = vec![
|
||||
// Header row
|
||||
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
|
||||
@@ -1137,7 +1203,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
// Query input
|
||||
let query_style = if app.input_mode == InputMode::Query {
|
||||
Style::default().fg(Color::Yellow)
|
||||
Style::default().fg(BRAND_END)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
@@ -1154,6 +1220,38 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
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
|
||||
match app.query_mode {
|
||||
QueryMode::Recall => {
|
||||
@@ -1180,7 +1278,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
let metadata = Paragraph::new(metadata_text)
|
||||
.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]);
|
||||
|
||||
@@ -1196,7 +1294,7 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
let mut items = vec![
|
||||
// Header row
|
||||
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
|
||||
@@ -1248,17 +1346,17 @@ fn render_query(f: &mut Frame, app: &mut App, area: Rect) {
|
||||
|
||||
fn render_help(f: &mut Frame, area: Rect) {
|
||||
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(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(" 2. View memories, entities, or documents for that bank"),
|
||||
Line::from(" 3. Press / from any view to query (recall/reflect)"),
|
||||
Line::from(""),
|
||||
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(" ←/→, 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(""),
|
||||
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(" m - Toggle mode (Recall ↔ Reflect)"),
|
||||
@@ -1275,7 +1373,7 @@ fn render_help(f: &mut Frame, area: Rect) {
|
||||
Line::from(" Enter - Execute query"),
|
||||
Line::from(""),
|
||||
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(" ? - Toggle this help screen"),
|
||||
@@ -1399,7 +1497,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result<()> {
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
if matches!(app.view, View::Query(_)) {
|
||||
app.execute_query()?;
|
||||
app.execute_query();
|
||||
}
|
||||
}
|
||||
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
|
||||
app.do_auto_refresh()?;
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ pub fn recall(
|
||||
|
||||
let response = client.recall(agent_id, &request, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -104,8 +104,8 @@ pub fn reflect(
|
||||
|
||||
let response = client.reflect(agent_id, &request, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -154,8 +154,8 @@ pub fn retain(
|
||||
|
||||
let response = client.retain(agent_id, &request, r#async, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -274,8 +274,8 @@ pub fn retain_files(
|
||||
|
||||
let response = client.retain(agent_id, &request, r#async, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -312,8 +312,8 @@ pub fn delete(
|
||||
|
||||
let response = client.delete_memory(agent_id, unit_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -345,12 +345,12 @@ pub fn clear(
|
||||
if !yes && output_format == OutputFormat::Pretty {
|
||||
let message = if let Some(ft) = &fact_type {
|
||||
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
|
||||
)
|
||||
} else {
|
||||
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
|
||||
)
|
||||
};
|
||||
@@ -377,8 +377,8 @@ pub fn clear(
|
||||
|
||||
let response = client.clear_memories(agent_id, fact_type.as_deref(), verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
|
||||
@@ -17,8 +17,8 @@ pub fn list(
|
||||
|
||||
let response = client.list_operations(agent_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
@@ -62,8 +62,8 @@ pub fn cancel(
|
||||
|
||||
let response = client.cancel_operation(agent_id, operation_id, verbose);
|
||||
|
||||
if let Some(sp) = spinner {
|
||||
sp.finish_and_clear();
|
||||
if let Some(mut sp) = spinner {
|
||||
sp.finish();
|
||||
}
|
||||
|
||||
match response {
|
||||
|
||||
@@ -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(about = "Hindsight CLI - Semantic memory system", long_about = None)]
|
||||
#[command(version)]
|
||||
#[command(before_help = get_before_help())]
|
||||
#[command(after_help = get_after_help())]
|
||||
struct Cli {
|
||||
/// 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)]
|
||||
enum Commands {
|
||||
/// Manage banks (list, profile, stats)
|
||||
@@ -100,8 +105,8 @@ enum BankCommands {
|
||||
/// List all banks
|
||||
List,
|
||||
|
||||
/// Get bank profile (disposition + background)
|
||||
Profile {
|
||||
/// Get bank disposition and background
|
||||
Disposition {
|
||||
/// Bank ID
|
||||
bank_id: String,
|
||||
},
|
||||
@@ -133,6 +138,16 @@ enum BankCommands {
|
||||
#[arg(long)]
|
||||
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)]
|
||||
@@ -378,12 +393,15 @@ fn run() -> Result<()> {
|
||||
Commands::Explore => commands::explore::run(&client),
|
||||
Commands::Bank(bank_cmd) => match bank_cmd {
|
||||
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::Name { bank_id, name } => commands::bank::update_name(&client, &bank_id, &name, verbose, output_format),
|
||||
BankCommands::Background { bank_id, content, no_update_disposition } => {
|
||||
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 {
|
||||
|
||||
+189
-81
@@ -4,80 +4,132 @@ use hindsight_client::types::ChunkData;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
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) {
|
||||
println!();
|
||||
println!("{}", format!("━━━ {} ━━━", title).bright_yellow().bold());
|
||||
println!("{}", gradient_text(&format!("━━━ {} ━━━", title)));
|
||||
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 type_color = match fact_type {
|
||||
"world" => "cyan",
|
||||
"agent" => "magenta",
|
||||
"opinion" => "yellow",
|
||||
_ => "white",
|
||||
// Use gradient positions for different fact types
|
||||
let type_t = match fact_type {
|
||||
"world" => 0.0,
|
||||
"agent" => 0.5,
|
||||
"opinion" => 1.0,
|
||||
_ => 0.5,
|
||||
};
|
||||
|
||||
let prefix = match fact_type {
|
||||
"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!("{}", gradient(&format!("[{}]", fact_type.to_uppercase()), type_t));
|
||||
println!(" {}", fact.text);
|
||||
|
||||
// Show context if available
|
||||
if let Some(context) = &fact.context {
|
||||
println!(" {}: {}", "Context".bright_black(), context.bright_black());
|
||||
println!(" {} {}", dim("context:"), dim(context));
|
||||
}
|
||||
|
||||
// Show temporal information
|
||||
if let Some(occurred_start) = &fact.occurred_start {
|
||||
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 {
|
||||
println!(" {}: {}", "Date".bright_black(), occurred_start.bright_black());
|
||||
println!(" {} {}", dim("date:"), dim(occurred_start));
|
||||
}
|
||||
}
|
||||
|
||||
// Show document ID if available
|
||||
if let Some(document_id) = &fact.document_id {
|
||||
println!(" {}: {}", "Document".bright_black(), document_id.bright_black());
|
||||
println!(" {} {}", dim("document:"), dim(document_id));
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_chunk(chunk: &ChunkData) {
|
||||
println!(" {}", "─── Source Chunk ───".bright_blue());
|
||||
println!(" {}", gradient_mid("─── Source Chunk ───"));
|
||||
|
||||
// Split text into lines and indent each line
|
||||
for line in chunk.text.lines() {
|
||||
println!(" {}", line.bright_white());
|
||||
println!(" {}", line);
|
||||
}
|
||||
|
||||
if chunk.truncated {
|
||||
println!(" {}", "[Truncated due to token limit]".bright_yellow());
|
||||
println!(" {}", gradient_end("[Truncated due to token limit]"));
|
||||
}
|
||||
|
||||
println!(" {}: {} | {}: {}",
|
||||
"Chunk ID".bright_black(),
|
||||
chunk.id.bright_black(),
|
||||
"Index".bright_black(),
|
||||
chunk.chunk_index.to_string().bright_black()
|
||||
println!(" {} {} | {} {}",
|
||||
dim("Chunk ID:"),
|
||||
dim(&chunk.id),
|
||||
dim("Index:"),
|
||||
dim(&chunk.chunk_index.to_string())
|
||||
);
|
||||
|
||||
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()));
|
||||
|
||||
if results.is_empty() {
|
||||
println!("{}", " No results found.".bright_black());
|
||||
println!(" {}", dim("No results found."));
|
||||
} else {
|
||||
for (i, fact) in results.iter().enumerate() {
|
||||
println!("{}", format!(" Result #{}", i + 1).bright_black());
|
||||
println!(" {}", dim(&format!("Result #{}", i + 1)));
|
||||
print_fact(fact, true);
|
||||
|
||||
// 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) {
|
||||
println!();
|
||||
println!("{}", response.text.bright_white());
|
||||
print_section_header("Reflection");
|
||||
|
||||
println!("{}", response.text);
|
||||
println!();
|
||||
|
||||
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>) {
|
||||
print_section_header("Trace Information");
|
||||
print_section_header("Trace");
|
||||
|
||||
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()) {
|
||||
println!(" 📊 Activation count: {}", count.to_string().bright_green());
|
||||
println!(" {} {}", dim("activation count:"), gradient_end(&count.to_string()));
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
pub fn print_success(message: &str) {
|
||||
println!("{} {}", "✓".bright_green().bold(), message.bright_white());
|
||||
println!("{}", gradient_start(message));
|
||||
}
|
||||
|
||||
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) {
|
||||
println!("{} {}", "⚠".bright_yellow().bold(), message.bright_yellow());
|
||||
println!("{} {}", gradient_end("warning:"), message);
|
||||
}
|
||||
|
||||
pub fn print_info(message: &str) {
|
||||
println!("{} {}", "ℹ".bright_blue().bold(), message.bright_white());
|
||||
println!("{}", gradient_start(message));
|
||||
}
|
||||
|
||||
pub fn create_spinner(message: &str) -> ProgressBar {
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.set_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.template("{spinner:.cyan} {msg}")
|
||||
.unwrap()
|
||||
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
|
||||
);
|
||||
pb.set_message(message.to_string());
|
||||
pb.enable_steady_tick(std::time::Duration::from_millis(80));
|
||||
pb
|
||||
/// Animated gradient spinner that shows text with moving gradient colors
|
||||
pub struct GradientSpinner {
|
||||
message: String,
|
||||
running: std::sync::Arc<std::sync::atomic::AtomicBool>,
|
||||
handle: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl GradientSpinner {
|
||||
pub fn new(message: &str) -> Self {
|
||||
let message = message.to_string();
|
||||
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 {
|
||||
@@ -180,7 +296,7 @@ pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar {
|
||||
}
|
||||
|
||||
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()?;
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
pub fn print_profile(profile: &BankProfileResponse) {
|
||||
print_section_header(&format!("Bank Profile: {}", profile.bank_id));
|
||||
pub fn print_disposition(profile: &BankProfileResponse) {
|
||||
print_section_header(&format!("Disposition: {}", profile.bank_id));
|
||||
|
||||
// Print name
|
||||
println!("{} {}", "Name:".bright_cyan().bold(), profile.name.bright_white());
|
||||
println!("{} {}", dim("Name:"), gradient_start(&profile.name));
|
||||
println!();
|
||||
|
||||
// Print background if available
|
||||
if !profile.background.is_empty() {
|
||||
println!("{}", "Background:".bright_yellow());
|
||||
println!("{}", gradient_mid("Background:"));
|
||||
for line in profile.background.lines() {
|
||||
println!("{}", line);
|
||||
}
|
||||
@@ -206,38 +322,30 @@ pub fn print_profile(profile: &BankProfileResponse) {
|
||||
}
|
||||
|
||||
// Print disposition traits
|
||||
println!("{}", "─── Disposition Traits ───".bright_yellow());
|
||||
println!("{}", gradient_text("─── Disposition Traits ───"));
|
||||
println!();
|
||||
|
||||
// New 3-trait disposition system (values 1-5)
|
||||
let traits: [(_, i64, _, _, _); 3] = [
|
||||
("Skepticism", profile.disposition.skepticism.get() as i64, "🔍", "cyan", "1=trusting, 5=skeptical"),
|
||||
("Literalism", profile.disposition.literalism.get() as i64, "📋", "yellow", "1=flexible, 5=literal"),
|
||||
("Empathy", profile.disposition.empathy.get() as i64, "💚", "green", "1=detached, 5=empathetic"),
|
||||
let traits: [(_, i64, f32, _); 3] = [
|
||||
("Skepticism", profile.disposition.skepticism.get() as i64, 0.0, "1=trusting, 5=skeptical"),
|
||||
("Literalism", profile.disposition.literalism.get() as i64, 0.5, "1=flexible, 5=literal"),
|
||||
("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)
|
||||
let bar_length = 40;
|
||||
let filled = ((*value - 1) * 10) as usize; // 1->0, 2->10, 3->20, 4->30, 5->40
|
||||
let empty = bar_length - filled;
|
||||
|
||||
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",
|
||||
emoji,
|
||||
println!(" {:<12} [{}] {}/5",
|
||||
name,
|
||||
colored_bar,
|
||||
gradient(&bar, *t),
|
||||
value
|
||||
);
|
||||
println!(" {}", desc.bright_black());
|
||||
println!(" {}", dim(desc));
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
+83
-107
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hindsight-control-plane",
|
||||
"version": "0.0.21",
|
||||
"version": "0.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hindsight-control-plane",
|
||||
"version": "0.0.21",
|
||||
"version": "0.1.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
@@ -15,8 +15,11 @@
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
@@ -33,19 +36,19 @@
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.0",
|
||||
"react-chrono": "^2.9.1",
|
||||
"react-cytoscape": "^1.0.6",
|
||||
"react-dom": "^19.2.0",
|
||||
"react18-json-view": "^0.2.9",
|
||||
"recharts": "^3.5.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.182.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"../hindsight-clients/typescript": {
|
||||
"name": "@vectorize-io/hindsight-client",
|
||||
"version": "0.0.21",
|
||||
"version": "0.1.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@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": {
|
||||
"version": "1.2.4",
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
@@ -5324,6 +5389,12 @@
|
||||
"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": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
@@ -6219,31 +6290,13 @@
|
||||
},
|
||||
"node_modules/cytoscape": {
|
||||
"version": "3.33.1",
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz",
|
||||
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
@@ -6265,18 +6318,6 @@
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
@@ -6307,10 +6348,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
@@ -6327,17 +6364,6 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
@@ -6362,18 +6388,6 @@
|
||||
"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": {
|
||||
"version": "1.0.8",
|
||||
"license": "BSD-2-Clause"
|
||||
@@ -7215,17 +7229,6 @@
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
@@ -7389,13 +7392,6 @@
|
||||
"version": "1.4.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graphlib": {
|
||||
"version": "2.1.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
@@ -8044,10 +8040,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"license": "MIT"
|
||||
@@ -8550,18 +8542,6 @@
|
||||
"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": {
|
||||
"version": "19.2.0",
|
||||
"license": "MIT",
|
||||
@@ -9315,6 +9295,12 @@
|
||||
"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": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
@@ -9713,16 +9699,6 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
@@ -37,13 +40,13 @@
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.2.0",
|
||||
"react-chrono": "^2.9.1",
|
||||
"react-cytoscape": "^1.0.6",
|
||||
"react-dom": "^19.2.0",
|
||||
"react18-json-view": "^0.2.9",
|
||||
"recharts": "^3.5.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"three": "^0.182.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
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";
|
||||
:root {
|
||||
--background: oklch(0.9911 0 0);
|
||||
@@ -6,8 +7,9 @@
|
||||
--card-foreground: oklch(0.2046 0 0);
|
||||
--popover: oklch(0.9911 0 0);
|
||||
--popover-foreground: oklch(0.4386 0 0);
|
||||
--primary: oklch(0.8348 0.1302 160.9080);
|
||||
--primary-foreground: oklch(0.2626 0.0147 166.4589);
|
||||
--primary: oklch(0.55 0.19 250);
|
||||
--primary-foreground: oklch(0.98 0.01 250);
|
||||
--primary-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||
--secondary: oklch(0.9940 0 0);
|
||||
--secondary-foreground: oklch(0.2046 0 0);
|
||||
--muted: oklch(0.9461 0 0);
|
||||
@@ -18,23 +20,23 @@
|
||||
--destructive-foreground: oklch(0.9934 0.0032 17.2118);
|
||||
--border: oklch(0.9037 0 0);
|
||||
--input: oklch(0.9731 0 0);
|
||||
--ring: oklch(0.8348 0.1302 160.9080);
|
||||
--chart-1: oklch(0.8348 0.1302 160.9080);
|
||||
--ring: oklch(0.55 0.19 250);
|
||||
--chart-1: oklch(0.55 0.19 250);
|
||||
--chart-2: oklch(0.6231 0.1880 259.8145);
|
||||
--chart-3: oklch(0.6056 0.2189 292.7172);
|
||||
--chart-4: oklch(0.7686 0.1647 70.0804);
|
||||
--chart-5: oklch(0.6959 0.1491 162.4796);
|
||||
--sidebar: oklch(0.9911 0 0);
|
||||
--sidebar-foreground: oklch(0.5452 0 0);
|
||||
--sidebar-primary: oklch(0.8348 0.1302 160.9080);
|
||||
--sidebar-primary-foreground: oklch(0.2626 0.0147 166.4589);
|
||||
--sidebar-primary: oklch(0.55 0.19 250);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.01 250);
|
||||
--sidebar-accent: oklch(0.9461 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.2435 0 0);
|
||||
--sidebar-border: oklch(0.9037 0 0);
|
||||
--sidebar-ring: oklch(0.8348 0.1302 160.9080);
|
||||
--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;
|
||||
--sidebar-ring: oklch(0.55 0.19 250);
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-heading: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 1px;
|
||||
@@ -61,8 +63,9 @@
|
||||
--card-foreground: oklch(0.9288 0.0126 255.5078);
|
||||
--popover: oklch(0.2603 0 0);
|
||||
--popover-foreground: oklch(0.7348 0 0);
|
||||
--primary: oklch(0.4365 0.1044 156.7556);
|
||||
--primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||
--primary: oklch(0.60 0.17 250);
|
||||
--primary-foreground: oklch(0.98 0.01 250);
|
||||
--primary-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||
--secondary: oklch(0.2603 0 0);
|
||||
--secondary-foreground: oklch(0.9851 0 0);
|
||||
--muted: oklch(0.2393 0 0);
|
||||
@@ -73,38 +76,20 @@
|
||||
--destructive-foreground: oklch(0.9368 0.0045 34.3092);
|
||||
--border: oklch(0.2809 0 0);
|
||||
--input: oklch(0.2603 0 0);
|
||||
--ring: oklch(0.8003 0.1821 151.7110);
|
||||
--chart-1: oklch(0.8003 0.1821 151.7110);
|
||||
--ring: oklch(0.60 0.17 250);
|
||||
--chart-1: oklch(0.60 0.17 250);
|
||||
--chart-2: oklch(0.7137 0.1434 254.6240);
|
||||
--chart-3: oklch(0.7090 0.1592 293.5412);
|
||||
--chart-4: oklch(0.8369 0.1644 84.4286);
|
||||
--chart-5: oklch(0.7845 0.1325 181.9120);
|
||||
--sidebar: oklch(0.1822 0 0);
|
||||
--sidebar-foreground: oklch(0.6301 0 0);
|
||||
--sidebar-primary: oklch(0.4365 0.1044 156.7556);
|
||||
--sidebar-primary-foreground: oklch(0.9213 0.0135 167.1556);
|
||||
--sidebar-primary: oklch(0.60 0.17 250);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.01 250);
|
||||
--sidebar-accent: oklch(0.3132 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.9851 0 0);
|
||||
--sidebar-border: oklch(0.2809 0 0);
|
||||
--sidebar-ring: oklch(0.8003 0.1821 151.7110);
|
||||
--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);
|
||||
--sidebar-ring: oklch(0.60 0.17 250);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -143,7 +128,7 @@
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
--font-heading: var(--font-heading);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
@@ -170,4 +155,29 @@
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
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 "./globals.css";
|
||||
import { BankProvider } from "@/lib/bank-context";
|
||||
import { ThemeProvider } from "@/lib/theme-context";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Hindsight Control Plane",
|
||||
description: "Control plane for the temporal semantic memory system",
|
||||
icons: {
|
||||
icon: "/favicon.png",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -13,11 +17,13 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body>
|
||||
<BankProvider>
|
||||
{children}
|
||||
</BankProvider>
|
||||
<ThemeProvider>
|
||||
<BankProvider>
|
||||
{children}
|
||||
</BankProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -27,7 +27,9 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
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 { Checkbox } from '@/components/ui/checkbox';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -36,6 +38,7 @@ function BankSelectorInner() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { currentBank, setCurrentBank, banks, loadBanks } = useBank();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
||||
const [newBankId, setNewBankId] = React.useState('');
|
||||
@@ -119,26 +122,34 @@ function BankSelectorInner() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
<span className="font-medium">Memory Bank:</span>
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary-gradient">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
{/* 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}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
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..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<PopoverContent className="w-[250px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search memory banks..." />
|
||||
{sortedBanks.length > 0 && (
|
||||
<CommandInput placeholder="Search memory banks..." />
|
||||
)}
|
||||
<CommandList>
|
||||
<CommandEmpty>No memory bank found.</CommandEmpty>
|
||||
<CommandEmpty>No memory banks yet.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sortedBanks.map((bank) => (
|
||||
<CommandItem
|
||||
@@ -165,34 +176,73 @@ function BankSelectorInner() {
|
||||
))}
|
||||
</CommandGroup>
|
||||
</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>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
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>
|
||||
{/* Separator */}
|
||||
<div className="h-8 w-px bg-border" />
|
||||
|
||||
{/* Add Document Button */}
|
||||
{currentBank && (
|
||||
<Button
|
||||
variant="outline"
|
||||
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)}
|
||||
title="Add document to current bank"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
<span>New Document</span>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Document</span>
|
||||
</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}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
@@ -333,17 +383,32 @@ function BankSelectorInner() {
|
||||
export function BankSelector() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary">
|
||||
<div className="flex items-center gap-2.5 text-sm">
|
||||
<span className="font-medium">Memory Bank:</span>
|
||||
<div className="bg-card text-card-foreground px-5 py-3 border-b-4 border-primary-gradient">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<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
|
||||
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
|
||||
>
|
||||
Loading...
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</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>
|
||||
}>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { client } from '@/lib/api';
|
||||
import { useBank } from '@/lib/bank-context';
|
||||
import cytoscape from 'cytoscape';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 } from 'lucide-react';
|
||||
import { Copy, Check, Calendar, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Settings2, Eye, EyeOff } from 'lucide-react';
|
||||
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 { Graph2D, convertHindsightGraphData, GraphNode } from './graph-2d';
|
||||
|
||||
type FactType = 'world' | 'experience' | 'opinion';
|
||||
type ViewMode = 'graph' | 'table' | 'timeline';
|
||||
@@ -23,16 +25,41 @@ export function DataView({ factType }: DataViewProps) {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('graph');
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nodeLimit, setNodeLimit] = useState(50);
|
||||
const [layout, setLayout] = useState('circle');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedGraphNode, setSelectedGraphNode] = useState<any>(null);
|
||||
const [selectedTableMemory, setSelectedTableMemory] = useState<any>(null);
|
||||
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) => {
|
||||
try {
|
||||
@@ -68,118 +95,98 @@ export function DataView({ factType }: DataViewProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const renderGraph = () => {
|
||||
if (!data || !containerRef.current || !data.nodes || !data.edges) return;
|
||||
// Filter table rows based on search query (text only)
|
||||
const filteredTableRows = useMemo(() => {
|
||||
if (!data?.table_rows) return [];
|
||||
if (!searchQuery) return data.table_rows;
|
||||
|
||||
if (cyRef.current) {
|
||||
cyRef.current.destroy();
|
||||
}
|
||||
|
||||
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)
|
||||
const query = searchQuery.toLowerCase();
|
||||
return data.table_rows.filter((row: any) =>
|
||||
row.text?.toLowerCase().includes(query)
|
||||
);
|
||||
}, [data, searchQuery]);
|
||||
|
||||
const layouts: any = {
|
||||
circle: {
|
||||
name: 'circle',
|
||||
animate: false,
|
||||
radius: 300,
|
||||
spacingFactor: 1.5,
|
||||
},
|
||||
grid: {
|
||||
name: 'grid',
|
||||
animate: false,
|
||||
rows: Math.ceil(Math.sqrt(limitedNodes.length)),
|
||||
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,
|
||||
},
|
||||
};
|
||||
// Get filtered node IDs for graph filtering
|
||||
const filteredNodeIds = useMemo(() => {
|
||||
return new Set(filteredTableRows.map((row: any) => row.id));
|
||||
}, [filteredTableRows]);
|
||||
|
||||
cyRef.current = cytoscape({
|
||||
container: containerRef.current,
|
||||
elements: [
|
||||
...limitedNodes.map((n: any) => ({ data: n.data })),
|
||||
...limitedEdges.map((e: any) => ({ data: e.data })),
|
||||
],
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'background-color': 'data(color)' as any,
|
||||
label: 'data(label)' as any,
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'font-size': '10px',
|
||||
'font-weight': 'bold',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '100px',
|
||||
width: 40,
|
||||
height: 40,
|
||||
'border-width': 2,
|
||||
'border-color': '#333',
|
||||
},
|
||||
},
|
||||
{
|
||||
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
|
||||
cyRef.current.on('tap', 'node', (evt: any) => {
|
||||
const nodeId = evt.target.id();
|
||||
// Find the corresponding table row data
|
||||
const nodeData = data.table_rows?.find((row: any) => row.id === nodeId);
|
||||
if (nodeData) {
|
||||
setSelectedGraphNode(nodeData);
|
||||
}
|
||||
});
|
||||
|
||||
// Click on background to deselect
|
||||
cyRef.current.on('tap', (evt: any) => {
|
||||
if (evt.target === cyRef.current) {
|
||||
setSelectedGraphNode(null);
|
||||
}
|
||||
});
|
||||
// Helper to get normalized link type
|
||||
const getLinkTypeCategory = (type: string | undefined): string => {
|
||||
if (!type) return 'semantic';
|
||||
if (type === 'semantic' || type === 'temporal' || type === 'entity') return type;
|
||||
if (['causes', 'caused_by', 'enables', 'prevents'].includes(type)) return 'causal';
|
||||
return 'semantic';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode === 'graph' && data) {
|
||||
renderGraph();
|
||||
// Convert data for Graph2D with filtering
|
||||
const graph2DData = useMemo(() => {
|
||||
if (!data) return { nodes: [], links: [] };
|
||||
const fullData = convertHindsightGraphData(data);
|
||||
|
||||
let nodes = fullData.nodes;
|
||||
let links = fullData.links;
|
||||
|
||||
// Filter nodes based on search query
|
||||
if (searchQuery) {
|
||||
const filteredNodes = fullData.nodes.filter(node => filteredNodeIds.has(node.id));
|
||||
const filteredNodeIdSet = new Set(filteredNodes.map(n => n.id));
|
||||
nodes = filteredNodes;
|
||||
links = fullData.links.filter(link =>
|
||||
filteredNodeIdSet.has(link.source) && filteredNodeIdSet.has(link.target)
|
||||
);
|
||||
}
|
||||
}, [viewMode, data, nodeLimit, layout]);
|
||||
|
||||
// Filter links based on visible link types
|
||||
links = links.filter(link => {
|
||||
const category = getLinkTypeCategory(link.type);
|
||||
return visibleLinkTypes.has(category);
|
||||
});
|
||||
|
||||
return { nodes, links };
|
||||
}, [data, searchQuery, filteredNodeIds, visibleLinkTypes]);
|
||||
|
||||
// Calculate link stats for display
|
||||
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) {
|
||||
setSelectedGraphNode(nodeData);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
// Memoized color functions to prevent graph re-initialization
|
||||
// Uses brand colors: primary blue (#0074d9), teal (#009296), amber for entity, purple for causal
|
||||
const nodeColorFn = useCallback((node: GraphNode) => node.color || '#0074d9', []);
|
||||
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
|
||||
}, []);
|
||||
|
||||
// Reset to first page when search query changes
|
||||
useEffect(() => {
|
||||
@@ -204,9 +211,20 @@ export function DataView({ factType }: DataViewProps) {
|
||||
</div>
|
||||
) : 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="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 className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
||||
<button
|
||||
@@ -243,134 +261,214 @@ export function DataView({ factType }: DataViewProps) {
|
||||
</div>
|
||||
|
||||
{viewMode === 'graph' && (
|
||||
<div className="flex gap-4">
|
||||
<div className={`relative transition-all ${selectedGraphNode ? 'w-2/3' : 'w-full'}`}>
|
||||
<div className="p-4 bg-card border-b-2 border-primary flex gap-4 items-center flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="font-semibold text-card-foreground">Limit nodes:</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={nodeLimit}
|
||||
onChange={(e) => setNodeLimit(parseInt(e.target.value))}
|
||||
min="10"
|
||||
max="1000"
|
||||
step="10"
|
||||
className="w-20"
|
||||
/>
|
||||
</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 className="flex gap-0">
|
||||
{/* Graph */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Graph2D
|
||||
data={graph2DData}
|
||||
height={700}
|
||||
showLabels={showLabels}
|
||||
onNodeClick={handleGraphNodeClick}
|
||||
maxNodes={maxNodes}
|
||||
nodeColorFn={nodeColorFn}
|
||||
linkColorFn={linkColorFn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Memory Detail Panel for Graph View - Fixed on Right */}
|
||||
{selectedGraphNode && (
|
||||
<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">
|
||||
<MemoryDetailPanel
|
||||
memory={selectedGraphNode}
|
||||
onClose={() => setSelectedGraphNode(null)}
|
||||
inPanel
|
||||
/>
|
||||
{/* Right Toggle Button */}
|
||||
<button
|
||||
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
|
||||
memory={selectedGraphNode}
|
||||
onClose={() => setSelectedGraphNode(null)}
|
||||
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>
|
||||
)}
|
||||
|
||||
{viewMode === 'table' && (
|
||||
<div>
|
||||
<div className="w-full">
|
||||
<div className="px-5 mb-4">
|
||||
<Input
|
||||
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 ? (
|
||||
<div className="pb-4">
|
||||
{filteredTableRows.length > 0 ? (
|
||||
(() => {
|
||||
const filteredRows = data.table_rows.filter((row: any) => {
|
||||
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 totalPages = Math.ceil(filteredTableRows.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const paginatedRows = filteredRows.slice(startIndex, endIndex);
|
||||
const paginatedRows = filteredTableRows.slice(startIndex, endIndex);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<Table className="table-fixed">
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[80px]">ID</TableHead>
|
||||
<TableHead>Text</TableHead>
|
||||
<TableHead className="w-[150px]">Context</TableHead>
|
||||
<TableHead className="w-[100px]">Occurred</TableHead>
|
||||
<TableHead className="w-[100px]">Mentioned</TableHead>
|
||||
<TableHead className="w-[60px]">Actions</TableHead>
|
||||
<TableHead className="w-[45%]">Memory</TableHead>
|
||||
<TableHead className="w-[20%]">Entities</TableHead>
|
||||
<TableHead className="w-[15%]">Occurred</TableHead>
|
||||
<TableHead className="w-[15%]">Mentioned</TableHead>
|
||||
<TableHead className="w-[5%]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedRows.map((row: any, idx: number) => {
|
||||
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;
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -381,46 +479,40 @@ export function DataView({ factType }: DataViewProps) {
|
||||
selectedTableMemory?.id === row.id ? 'bg-primary/10' : ''
|
||||
}`}
|
||||
>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground" title={row.id}>
|
||||
{row.id?.substring(0, 8)}...
|
||||
<TableCell className="py-2">
|
||||
<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>
|
||||
<div className="line-clamp-2 text-sm">{row.text}</div>
|
||||
{row.entities && (
|
||||
<div className="flex gap-1 mt-1 flex-wrap">
|
||||
{row.entities.split(', ').slice(0, 3).map((entity: string, i: number) => (
|
||||
<span key={i} className="text-[10px] px-1.5 py-0.5 rounded bg-secondary text-secondary-foreground">
|
||||
<TableCell className="py-2">
|
||||
{row.entities ? (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{row.entities.split(', ').slice(0, 2).map((entity: string, i: number) => (
|
||||
<span
|
||||
key={i}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium"
|
||||
>
|
||||
{entity}
|
||||
</span>
|
||||
))}
|
||||
{row.entities.split(', ').length > 3 && (
|
||||
{row.entities.split(', ').length > 2 && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
+{row.entities.split(', ').length - 3}
|
||||
+{row.entities.split(', ').length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground truncate max-w-[150px]" title={row.context}>
|
||||
{row.context || '-'}
|
||||
<TableCell className="text-xs py-2">
|
||||
{occurredDisplay || <span className="text-muted-foreground">-</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{occurredDisplay ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{occurredDisplay}
|
||||
</span>
|
||||
) : '-'}
|
||||
<TableCell className="text-xs py-2">
|
||||
{mentionedDisplay || <span className="text-muted-foreground">-</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{mentionedDisplay ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{mentionedDisplay}
|
||||
</span>
|
||||
) : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="py-2">
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -428,7 +520,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0"
|
||||
className="h-6 w-6 p-0"
|
||||
title="Copy ID"
|
||||
>
|
||||
{copiedId === row.id ? (
|
||||
@@ -447,9 +539,9 @@ export function DataView({ factType }: DataViewProps) {
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {startIndex + 1} to {Math.min(endIndex, filteredRows.length)} of {filteredRows.length}
|
||||
<div className="flex items-center justify-between mt-3 pt-3 border-t">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{startIndex + 1}-{Math.min(endIndex, filteredTableRows.length)} of {filteredTableRows.length}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
@@ -457,20 +549,20 @@ export function DataView({ factType }: DataViewProps) {
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 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>
|
||||
<span className="text-sm px-3">
|
||||
<span className="text-xs px-2">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
@@ -478,18 +570,18 @@ export function DataView({ factType }: DataViewProps) {
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
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
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage(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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -499,7 +591,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||
})()
|
||||
) : (
|
||||
<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>
|
||||
@@ -519,7 +611,7 @@ export function DataView({ factType }: DataViewProps) {
|
||||
)}
|
||||
|
||||
{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
|
||||
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 [granularity, setGranularity] = useState<Granularity>('month');
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
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(() => {
|
||||
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)
|
||||
.sort((a: any, b: any) => {
|
||||
const dateA = new Date(a.occurred_start).getTime();
|
||||
@@ -555,19 +647,10 @@ function TimelineView({ data }: { data: any }) {
|
||||
return dateA - dateB;
|
||||
});
|
||||
|
||||
const withoutDates = data.table_rows.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]
|
||||
});
|
||||
const withoutDates = filteredRows.filter((row: any) => !row.occurred_start);
|
||||
|
||||
return { sortedItems: withDates, itemsWithoutDates: withoutDates };
|
||||
}, [data]);
|
||||
}, [filteredRows]);
|
||||
|
||||
// Group items by granularity
|
||||
const timelineGroups = useMemo(() => {
|
||||
@@ -697,9 +780,9 @@ function TimelineView({ data }: { data: any }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 px-4">
|
||||
<div className="px-4">
|
||||
{/* Timeline */}
|
||||
<div className={`transition-all ${selectedItem ? 'w-2/3' : 'w-full'}`}>
|
||||
<div>
|
||||
{/* Controls */}
|
||||
<div className="flex items-center justify-between mb-3 gap-4">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
@@ -853,7 +936,7 @@ function TimelineView({ data }: { data: any }) {
|
||||
{item.entities && (
|
||||
<div className="flex gap-1 mt-1 flex-wrap">
|
||||
{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}
|
||||
</span>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-9 px-3 gap-2"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Close
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,16 +15,16 @@ interface SidebarProps {
|
||||
|
||||
export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
const { currentBank } = useBank();
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
|
||||
if (!currentBank) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ id: 'data' as NavItem, label: 'Memories', icon: Database },
|
||||
{ id: 'recall' as NavItem, label: 'Recall', icon: Search },
|
||||
{ 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: 'entities' as NavItem, label: 'Entities', icon: Users },
|
||||
{ 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',
|
||||
isCollapsed ? 'w-16' : 'w-64'
|
||||
)}>
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
{!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">
|
||||
<nav className="flex-1 p-3 pt-4">
|
||||
<ul className="space-y-1">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
@@ -75,7 +58,7 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-all',
|
||||
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',
|
||||
isCollapsed && 'justify-center px-0'
|
||||
)}
|
||||
@@ -89,6 +72,27 @@ export function Sidebar({ currentTab, onTabChange }: SidebarProps) {
|
||||
})}
|
||||
</ul>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const buttonVariants = cva(
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
default: "bg-primary-gradient text-white hover:opacity-90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
scope="judge",
|
||||
temperature=0,
|
||||
max_tokens=4096
|
||||
max_completion_tokens=4096
|
||||
)
|
||||
|
||||
return judgement.correct, judgement.reasoning
|
||||
|
||||
@@ -398,7 +398,7 @@ Answer:
|
||||
],
|
||||
response_format=QuestionAnswer,
|
||||
scope="memory",
|
||||
max_tokens=32768,
|
||||
max_completion_tokens=32768,
|
||||
)
|
||||
reasoning_text = answer_obj.reasoning or ""
|
||||
if reasoning_text:
|
||||
|
||||
@@ -31,10 +31,10 @@ API available at http://localhost:8888
|
||||
|
||||
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 \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- **API**: http://localhost:8888
|
||||
@@ -100,7 +100,7 @@ await client.reflect('my-bank', 'Tell me about Alice');
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```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
|
||||
|
||||
@@ -37,11 +37,12 @@ See [Models](./models) for detailed comparison and configuration.
|
||||
Run everything in one container with embedded PostgreSQL:
|
||||
|
||||
```bash
|
||||
docker run -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_PROVIDER=openai \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx \
|
||||
-e HINDSIGHT_API_LLM_MODEL=gpt-4o-mini \
|
||||
ghcr.io/vectorize-io/hindsight
|
||||
export OPENAI_API_KEY=sk-xxx
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
- **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.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Query] --> B[Recall Memories]
|
||||
B --> C[Load Disposition]
|
||||
C --> D[Reason]
|
||||
D --> E[Form Opinions]
|
||||
E --> F[Response]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
Your Content
|
||||
↓
|
||||
Extract Rich Facts
|
||||
↓
|
||||
Identify Entities
|
||||
↓
|
||||
Build Connections
|
||||
↓
|
||||
Searchable Memory Bank
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Your Content] --> B[Extract Facts]
|
||||
B --> C[Identify Entities]
|
||||
C --> D[Build Connections]
|
||||
D --> E[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.
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
```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
|
||||
@@ -179,12 +179,32 @@ hindsight memory recall <bank_id> "query" -o yaml
|
||||
|
||||
## Interactive Explorer
|
||||
|
||||
Launch the TUI explorer for visual navigation:
|
||||
Launch the TUI explorer for visual navigation of your memory banks:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```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 = {
|
||||
title: 'Hindsight',
|
||||
tagline: 'Entity-Aware Memory System for AI Agents',
|
||||
favicon: 'img/favicon.ico',
|
||||
favicon: 'img/favicon.png',
|
||||
|
||||
future: {
|
||||
v4: true,
|
||||
@@ -49,7 +49,7 @@ const config: Config = {
|
||||
tagName: 'link',
|
||||
attributes: {
|
||||
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',
|
||||
onload: "this.media='all'",
|
||||
},
|
||||
@@ -83,7 +83,7 @@ const config: Config = {
|
||||
},
|
||||
],
|
||||
theme: {
|
||||
primaryColor: '#0d9488',
|
||||
primaryColor: '#0074d9',
|
||||
sidebar: {
|
||||
backgroundColor: '#09090b',
|
||||
},
|
||||
@@ -92,9 +92,9 @@ const config: Config = {
|
||||
},
|
||||
typography: {
|
||||
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: {
|
||||
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: {
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace",
|
||||
@@ -121,10 +121,10 @@ const config: Config = {
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
navbar: {
|
||||
title: 'Hindsight',
|
||||
logo: {
|
||||
alt: 'Hindsight Logo',
|
||||
src: 'img/logo.svg',
|
||||
src: 'img/logo.png',
|
||||
style: { height: '32px' },
|
||||
},
|
||||
items: [
|
||||
{
|
||||
@@ -164,8 +164,9 @@ const config: Config = {
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/vectorize-io/hindsight',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
className: 'header-github-link',
|
||||
'aria-label': 'GitHub repository',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -209,24 +210,39 @@ const config: Config = {
|
||||
mermaid: {
|
||||
theme: {
|
||||
light: 'base',
|
||||
dark: 'dark',
|
||||
dark: 'base',
|
||||
},
|
||||
options: {
|
||||
themeVariables: {
|
||||
primaryColor: '#6366f1',
|
||||
// Gradient start (#0074d9 blue) for nodes
|
||||
primaryColor: '#0074d9',
|
||||
primaryTextColor: '#ffffff',
|
||||
primaryBorderColor: '#4f46e5',
|
||||
secondaryColor: '#f1f5f9',
|
||||
secondaryTextColor: '#1e293b',
|
||||
secondaryBorderColor: '#cbd5e1',
|
||||
tertiaryColor: '#e0e7ff',
|
||||
lineColor: '#94a3b8',
|
||||
primaryBorderColor: '#005db0',
|
||||
// Gradient end (#009296 teal) for edges/clusters
|
||||
secondaryColor: '#009296',
|
||||
secondaryTextColor: '#ffffff',
|
||||
secondaryBorderColor: '#007a7d',
|
||||
// Tertiary
|
||||
tertiaryColor: '#e6f7f8',
|
||||
tertiaryTextColor: '#1e293b',
|
||||
// Lines and edges - gradient end color
|
||||
lineColor: '#009296',
|
||||
// Text
|
||||
textColor: '#1e293b',
|
||||
mainBkg: '#ffffff',
|
||||
nodeBorder: '#4f46e5',
|
||||
clusterBkg: '#f8fafc',
|
||||
clusterBorder: '#e2e8f0',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
// Node specific - gradient start
|
||||
nodeBkg: '#0074d9',
|
||||
nodeTextColor: '#ffffff',
|
||||
nodeBorder: '#005db0',
|
||||
// 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": {
|
||||
@@ -1494,6 +1535,28 @@
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"title": "Success"
|
||||
},
|
||||
"message": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Message"
|
||||
},
|
||||
"deleted_count": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Deleted Count"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -1503,7 +1566,9 @@
|
||||
"title": "DeleteResponse",
|
||||
"description": "Response model for delete operations.",
|
||||
"example": {
|
||||
"success": true
|
||||
"success": true,
|
||||
"message": "Deleted successfully",
|
||||
"deleted_count": 10
|
||||
}
|
||||
},
|
||||
"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: [
|
||||
{
|
||||
|
||||
@@ -2,43 +2,49 @@
|
||||
* Hindsight custom theme - Modern, clean style
|
||||
*/
|
||||
|
||||
/* Primary colors - Teal/Cyan theme */
|
||||
/* Primary colors - Blue to Teal gradient theme */
|
||||
:root {
|
||||
--ifm-color-primary: #0d9488;
|
||||
--ifm-color-primary-dark: #0f766e;
|
||||
--ifm-color-primary-darker: #115e59;
|
||||
--ifm-color-primary-darkest: #134e4a;
|
||||
--ifm-color-primary-light: #14b8a6;
|
||||
--ifm-color-primary-lighter: #2dd4bf;
|
||||
--ifm-color-primary-lightest: #5eead4;
|
||||
/* Primary gradient */
|
||||
--hindsight-gradient: linear-gradient(135deg, #0074d9 0%, #009296 100%);
|
||||
--hindsight-gradient-start: #0074d9;
|
||||
--hindsight-gradient-end: #009296;
|
||||
|
||||
/* Fallback solid colors (midpoint of gradient) */
|
||||
--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%;
|
||||
--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 */
|
||||
--ifm-font-family-base: 'Avenir Book', 'Avenir', 'Nunito Sans', -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;
|
||||
/* Typography */
|
||||
--ifm-font-family-base: 'Inter', -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-weight-semibold: 600;
|
||||
--ifm-font-size-base: 96%;
|
||||
|
||||
/* Spacing */
|
||||
--ifm-spacing-horizontal: 1.5rem;
|
||||
--ifm-navbar-height: 3.5rem;
|
||||
--ifm-navbar-height: 4.5rem;
|
||||
|
||||
/* Borders */
|
||||
--ifm-global-radius: 0.5rem;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #2dd4bf;
|
||||
--ifm-color-primary-dark: #14b8a6;
|
||||
--ifm-color-primary-darker: #0d9488;
|
||||
--ifm-color-primary-darkest: #0f766e;
|
||||
--ifm-color-primary-light: #5eead4;
|
||||
--ifm-color-primary-lighter: #99f6e4;
|
||||
--ifm-color-primary-lightest: #ccfbf1;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(45, 212, 191, 0.15);
|
||||
--ifm-color-primary: #3396e8;
|
||||
--ifm-color-primary-dark: #1a85e0;
|
||||
--ifm-color-primary-darker: #0074d9;
|
||||
--ifm-color-primary-darkest: #0068c3;
|
||||
--ifm-color-primary-light: #66b3f0;
|
||||
--ifm-color-primary-lighter: #99cff5;
|
||||
--ifm-color-primary-lightest: rgba(51, 150, 232, 0.15);
|
||||
--docusaurus-highlighted-code-line-bg: rgba(51, 150, 232, 0.15);
|
||||
|
||||
--ifm-background-color: #09090b;
|
||||
--ifm-background-surface-color: #18181b;
|
||||
@@ -47,6 +53,14 @@
|
||||
--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 {
|
||||
box-shadow: none;
|
||||
@@ -54,9 +68,17 @@
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.navbar__logo {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.navbar__title {
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
background: var(--hindsight-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.navbar__items {
|
||||
@@ -64,19 +86,40 @@
|
||||
}
|
||||
|
||||
.navbar__link {
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
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 {
|
||||
background-color: var(--ifm-background-surface-color);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -89,7 +132,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -115,18 +158,26 @@
|
||||
}
|
||||
|
||||
.button--primary {
|
||||
background: var(--ifm-color-primary);
|
||||
border-color: var(--ifm-color-primary);
|
||||
background: var(--hindsight-gradient);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button--primary:hover {
|
||||
background: var(--ifm-color-primary-dark);
|
||||
border-color: var(--ifm-color-primary-dark);
|
||||
background: linear-gradient(135deg, #005db0 0%, #007a7d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
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 */
|
||||
@@ -137,13 +188,18 @@
|
||||
}
|
||||
|
||||
.menu__link--active {
|
||||
background: var(--ifm-color-primary-lightest);
|
||||
color: var(--ifm-color-primary-darkest);
|
||||
background: linear-gradient(135deg, rgba(0, 116, 217, 0.1) 0%, rgba(0, 146, 150, 0.1) 100%);
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: rgba(45, 212, 191, 0.12);
|
||||
color: var(--ifm-color-primary-light);
|
||||
background: linear-gradient(135deg, rgba(0, 116, 217, 0.15) 0%, rgba(0, 146, 150, 0.15) 100%);
|
||||
}
|
||||
|
||||
/* Non-collapsible category styling */
|
||||
@@ -174,11 +230,16 @@
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre,
|
||||
pre code,
|
||||
.prism-code {
|
||||
font-family: 'JetBrains Mono', var(--ifm-font-family-monospace) !important;
|
||||
}
|
||||
|
||||
.prism-code {
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
font-family: var(--ifm-font-family-monospace);
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
@@ -187,7 +248,7 @@ code {
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.2rem 0.45rem;
|
||||
font-size: 0.875em;
|
||||
font-family: var(--ifm-font-family-monospace);
|
||||
font-family: 'JetBrains Mono', var(--ifm-font-family-monospace) !important;
|
||||
background-color: #f1f5f9;
|
||||
color: #0f172a;
|
||||
font-weight: 500;
|
||||
@@ -200,7 +261,7 @@ code {
|
||||
|
||||
/* Don't apply inline styles to code inside pre blocks */
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
background-color: transparent !important;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
@@ -208,32 +269,36 @@ pre code {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Code block container */
|
||||
/* Code block container - single background source */
|
||||
div[class*="codeBlockContainer"] {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--ifm-toc-border-color);
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
background: #f8fafc !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] div[class*="codeBlockContainer"] {
|
||||
background: #0f172a;
|
||||
background: #0f172a !important;
|
||||
}
|
||||
|
||||
div[class*="codeBlockTitle"] {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
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 */
|
||||
div[class*="codeBlockContent"] {
|
||||
background: #f8fafc;
|
||||
/* Code block content area - transparent to show container bg */
|
||||
div[class*="codeBlockContent"],
|
||||
div[class*="codeBlockContent"] pre,
|
||||
div[class*="codeBlockContent"] .prism-code {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] div[class*="codeBlockContent"] {
|
||||
background: #0f172a;
|
||||
/* Prism token backgrounds */
|
||||
.prism-code span {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Cards/Features */
|
||||
@@ -246,10 +311,21 @@ div[class*="codeBlockContent"] {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
/* Page title with gradient */
|
||||
article h1,
|
||||
.markdown h1,
|
||||
header h1,
|
||||
h1[class*="title"] {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 800;
|
||||
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 {
|
||||
@@ -258,7 +334,9 @@ article h2 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
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 {
|
||||
@@ -272,10 +350,93 @@ article p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Admonitions */
|
||||
.admonition {
|
||||
border-radius: 0.5rem;
|
||||
border-left-width: 4px;
|
||||
/* Links with gradient */
|
||||
article a:not(.button):not([class*="hash-link"]) {
|
||||
background: var(--hindsight-gradient);
|
||||
-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 */
|
||||
@@ -318,6 +479,14 @@ th {
|
||||
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 */
|
||||
[class*="redoc-wrap"] [class*="menu-content"] ul {
|
||||
@@ -329,6 +498,153 @@ th {
|
||||
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 */
|
||||
article ul, article ol {
|
||||
font-size: 0.9375rem;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -e
|
||||
|
||||
# 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"
|
||||
INSTALL_DIR="${HINDSIGHT_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
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 |
+334
-1114
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": {
|
||||
@@ -1494,6 +1535,28 @@
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"title": "Success"
|
||||
},
|
||||
"message": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Message"
|
||||
},
|
||||
"deleted_count": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Deleted Count"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -1503,7 +1566,9 @@
|
||||
"title": "DeleteResponse",
|
||||
"description": "Response model for delete operations.",
|
||||
"example": {
|
||||
"success": true
|
||||
"success": true,
|
||||
"message": "Deleted successfully",
|
||||
"deleted_count": 10
|
||||
}
|
||||
},
|
||||
"DispositionTraits": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
+66
-1
@@ -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": {
|
||||
@@ -1494,6 +1535,28 @@
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"title": "Success"
|
||||
},
|
||||
"message": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Message"
|
||||
},
|
||||
"deleted_count": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Deleted Count"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -1503,7 +1566,9 @@
|
||||
"title": "DeleteResponse",
|
||||
"description": "Response model for delete operations.",
|
||||
"example": {
|
||||
"success": true
|
||||
"success": true,
|
||||
"message": "Deleted successfully",
|
||||
"deleted_count": 10
|
||||
}
|
||||
},
|
||||
"DispositionTraits": {
|
||||
|
||||
@@ -3,51 +3,13 @@ set -e
|
||||
|
||||
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"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Error: Environment file $ENV_FILE not found at project root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📄 Loading environment from $ENV_FILE"
|
||||
echo "Loading environment from $ENV_FILE"
|
||||
echo ""
|
||||
|
||||
# Export all variables from env file
|
||||
@@ -55,21 +17,4 @@ set -a
|
||||
source "$ENV_FILE"
|
||||
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[@]}"
|
||||
|
||||
@@ -1141,7 +1141,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-all"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = { editable = "hindsight" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
@@ -1165,7 +1165,7 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-api"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = { editable = "hindsight-api" }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
@@ -1265,7 +1265,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-client"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = { editable = "hindsight-clients/python" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1297,7 +1297,7 @@ provides-extras = ["test"]
|
||||
|
||||
[[package]]
|
||||
name = "hindsight-dev"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = { editable = "hindsight-dev" }
|
||||
dependencies = [
|
||||
{ name = "hindsight-api" },
|
||||
|
||||
Reference in New Issue
Block a user