feat: expose document_metadata in API and control plane (#798)
* feat: expose document_metadata in API and control plane
Add document_metadata (sourced from retain_params.metadata) to both
list and get document endpoints. Display it in the control plane
documents table and detail panel. Drop the unused metadata column
from the documents table (was always stored as empty {}).
* fix: code review fixes for document_metadata feature
- Remove unnecessary `import json as _json` (json already imported at module level)
- Simplify redundant truthiness checks in retain_params parsing
- Regenerate OpenAPI spec and client SDKs (Python, TypeScript, Go)
- Add tests for document_metadata in get_document and list_documents
* feat(ui): improve documents table and detail panel
- Relative timestamps with full date on hover
- Remove context column from table
- Metadata shown as k=v badges (blue, like tags)
- Size in bytes instead of chars
- Document IDs wrap instead of truncating
- Detail panel wider (560px)
- Retain params: context, event_date, metadata badges
This commit is contained in:
parent
bdb33c58d1
commit
627ec5d524
11 changed files with 361 additions and 39 deletions
|
|
@ -0,0 +1,34 @@
|
|||
"""Drop unused metadata column from documents table
|
||||
|
||||
Revision ID: d6e7f8a9b0c1
|
||||
Revises: c2d3e4f5g6h7, c5d6e7f8a9b0
|
||||
Create Date: 2026-03-30
|
||||
|
||||
The metadata column on documents was always stored as an empty dict {}.
|
||||
Actual document metadata is stored inside retain_params.metadata.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import context, op
|
||||
|
||||
revision: str = "d6e7f8a9b0c1"
|
||||
down_revision: str | Sequence[str] | None = ("c2d3e4f5g6h7", "c5d6e7f8a9b0")
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def _get_schema_prefix() -> str:
|
||||
"""Get schema prefix for table names (required for multi-tenant support)."""
|
||||
schema = context.config.get_main_option("target_schema")
|
||||
return f'"{schema}".' if schema else ""
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
schema = _get_schema_prefix()
|
||||
op.execute(f"ALTER TABLE {schema}documents DROP COLUMN IF EXISTS metadata")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
schema = _get_schema_prefix()
|
||||
op.execute(f"ALTER TABLE {schema}documents ADD COLUMN IF NOT EXISTS metadata jsonb DEFAULT '{{}}'")
|
||||
|
|
@ -1277,6 +1277,8 @@ class DocumentResponse(BaseModel):
|
|||
"updated_at": "2024-01-15T10:30:00Z",
|
||||
"memory_unit_count": 15,
|
||||
"tags": ["user_a", "session_123"],
|
||||
"document_metadata": {"source": "slack", "channel": "#general"},
|
||||
"retain_params": {"context": "Team meeting notes", "event_date": "2024-01-15"},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -1289,6 +1291,8 @@ class DocumentResponse(BaseModel):
|
|||
updated_at: str
|
||||
memory_unit_count: int
|
||||
tags: list[str] = FieldWithDefault(list, description="Tags associated with this document")
|
||||
document_metadata: dict[str, Any] | None = Field(default=None, description="Document metadata")
|
||||
retain_params: dict[str, Any] | None = Field(default=None, description="Parameters used during retain")
|
||||
|
||||
|
||||
class UpdateDocumentRequest(BaseModel):
|
||||
|
|
|
|||
|
|
@ -3509,11 +3509,13 @@ class MemoryEngine(MemoryEngineInterface):
|
|||
doc = await conn.fetchrow(
|
||||
f"""
|
||||
SELECT d.id, d.bank_id, d.original_text, d.content_hash,
|
||||
d.created_at, d.updated_at, d.tags, COUNT(mu.id) as unit_count
|
||||
d.created_at, d.updated_at, d.tags, d.retain_params,
|
||||
COUNT(mu.id) as unit_count
|
||||
FROM {fq_table("documents")} d
|
||||
LEFT JOIN {fq_table("memory_units")} mu ON mu.document_id = d.id
|
||||
WHERE d.id = $1 AND d.bank_id = $2
|
||||
GROUP BY d.id, d.bank_id, d.original_text, d.content_hash, d.created_at, d.updated_at, d.tags
|
||||
GROUP BY d.id, d.bank_id, d.original_text, d.content_hash,
|
||||
d.created_at, d.updated_at, d.tags, d.retain_params
|
||||
""",
|
||||
document_id,
|
||||
bank_id,
|
||||
|
|
@ -3522,6 +3524,14 @@ class MemoryEngine(MemoryEngineInterface):
|
|||
if not doc:
|
||||
return None
|
||||
|
||||
retain_params_raw = doc["retain_params"]
|
||||
retain_params_parsed = (
|
||||
json.loads(retain_params_raw) if isinstance(retain_params_raw, str) else retain_params_raw
|
||||
)
|
||||
|
||||
# document_metadata is sourced from retain_params.metadata
|
||||
document_metadata = retain_params_parsed.get("metadata") if retain_params_parsed else None
|
||||
|
||||
return {
|
||||
"id": doc["id"],
|
||||
"bank_id": doc["bank_id"],
|
||||
|
|
@ -3531,6 +3541,8 @@ class MemoryEngine(MemoryEngineInterface):
|
|||
"created_at": doc["created_at"].isoformat() if doc["created_at"] else None,
|
||||
"updated_at": doc["updated_at"].isoformat() if doc["updated_at"] else None,
|
||||
"tags": list(doc["tags"]) if doc["tags"] else [],
|
||||
"document_metadata": document_metadata or None,
|
||||
"retain_params": retain_params_parsed or None,
|
||||
}
|
||||
|
||||
async def delete_document(
|
||||
|
|
@ -4950,6 +4962,14 @@ class MemoryEngine(MemoryEngineInterface):
|
|||
bank_id_val = row["bank_id"]
|
||||
unit_count = count_map.get((doc_id, bank_id_val), 0)
|
||||
|
||||
retain_params_val = row["retain_params"]
|
||||
retain_params_val = (
|
||||
json.loads(retain_params_val) if isinstance(retain_params_val, str) else retain_params_val
|
||||
)
|
||||
|
||||
# document_metadata is sourced from retain_params.metadata
|
||||
document_metadata = retain_params_val.get("metadata") if retain_params_val else None
|
||||
|
||||
items.append(
|
||||
{
|
||||
"id": doc_id,
|
||||
|
|
@ -4959,7 +4979,8 @@ class MemoryEngine(MemoryEngineInterface):
|
|||
"updated_at": row["updated_at"].isoformat() if row["updated_at"] else "",
|
||||
"text_length": row["text_length"] or 0,
|
||||
"memory_unit_count": unit_count,
|
||||
"retain_params": row["retain_params"] if row["retain_params"] else None,
|
||||
"retain_params": retain_params_val or None,
|
||||
"document_metadata": document_metadata or None,
|
||||
"tags": row["tags"] if row["tags"] else [],
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -288,12 +288,11 @@ async def _upsert_document_row(
|
|||
"""Insert or update a document row."""
|
||||
await conn.execute(
|
||||
f"""
|
||||
INSERT INTO {fq_table("documents")} (id, bank_id, original_text, content_hash, metadata, retain_params, tags)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO {fq_table("documents")} (id, bank_id, original_text, content_hash, retain_params, tags)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (id, bank_id) DO UPDATE
|
||||
SET original_text = EXCLUDED.original_text,
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
metadata = EXCLUDED.metadata,
|
||||
retain_params = EXCLUDED.retain_params,
|
||||
tags = EXCLUDED.tags,
|
||||
updated_at = NOW()
|
||||
|
|
@ -302,7 +301,6 @@ async def _upsert_document_row(
|
|||
bank_id,
|
||||
combined_content,
|
||||
content_hash,
|
||||
json.dumps({}), # Empty metadata dict
|
||||
json.dumps(retain_params) if retain_params else None,
|
||||
document_tags or [],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -141,6 +141,70 @@ async def test_memory_without_document(memory, request_context):
|
|||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_metadata_from_retain_params(memory, request_context):
|
||||
"""Test that document_metadata is returned from retain_params.metadata in both get and list."""
|
||||
bank_id = f"test_doc_meta_{datetime.now(timezone.utc).timestamp()}"
|
||||
|
||||
try:
|
||||
document_id = "doc-with-metadata"
|
||||
metadata = {"source": "slack", "channel": "#general"}
|
||||
|
||||
await memory.retain_batch_async(
|
||||
bank_id=bank_id,
|
||||
contents=[{"content": "Alice works at Google.", "context": "Team meeting", "metadata": metadata}],
|
||||
document_id=document_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
# get_document should include document_metadata
|
||||
doc = await memory.get_document(document_id, bank_id, request_context=request_context)
|
||||
assert doc is not None
|
||||
assert doc["document_metadata"] == metadata
|
||||
assert doc["retain_params"] is not None
|
||||
assert doc["retain_params"]["metadata"] == metadata
|
||||
|
||||
# list_documents should also include document_metadata
|
||||
docs_list = await memory.list_documents(
|
||||
bank_id=bank_id, search_query=None, limit=100, offset=0, request_context=request_context
|
||||
)
|
||||
listed_doc = next(d for d in docs_list["items"] if d["id"] == document_id)
|
||||
assert listed_doc["document_metadata"] == metadata
|
||||
|
||||
finally:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_without_metadata(memory, request_context):
|
||||
"""Test that document_metadata is None when no metadata was provided during retain."""
|
||||
bank_id = f"test_doc_no_meta_{datetime.now(timezone.utc).timestamp()}"
|
||||
|
||||
try:
|
||||
document_id = "doc-no-metadata"
|
||||
|
||||
await memory.retain_async(
|
||||
bank_id=bank_id,
|
||||
content="Bob works at Microsoft.",
|
||||
context="Meeting",
|
||||
document_id=document_id,
|
||||
request_context=request_context,
|
||||
)
|
||||
|
||||
doc = await memory.get_document(document_id, bank_id, request_context=request_context)
|
||||
assert doc is not None
|
||||
assert doc["document_metadata"] is None
|
||||
|
||||
docs_list = await memory.list_documents(
|
||||
bank_id=bank_id, search_query=None, limit=100, offset=0, request_context=request_context
|
||||
)
|
||||
listed_doc = next(d for d in docs_list["items"] if d["id"] == document_id)
|
||||
assert listed_doc["document_metadata"] is None
|
||||
|
||||
finally:
|
||||
await memory.delete_bank(bank_id, request_context=request_context)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_persisted_with_zero_facts(memory, request_context):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3910,9 +3910,15 @@ components:
|
|||
bank_id: user123
|
||||
content_hash: abc123
|
||||
created_at: 2024-01-15T10:30:00Z
|
||||
document_metadata:
|
||||
channel: '#general'
|
||||
source: slack
|
||||
id: session_1
|
||||
memory_unit_count: 15
|
||||
original_text: Full document text here...
|
||||
retain_params:
|
||||
context: Team meeting notes
|
||||
event_date: 2024-01-15
|
||||
tags:
|
||||
- user_a
|
||||
- session_123
|
||||
|
|
@ -3945,6 +3951,12 @@ components:
|
|||
items:
|
||||
type: string
|
||||
type: array
|
||||
document_metadata:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
retain_params:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
required:
|
||||
- bank_id
|
||||
- content_hash
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ type DocumentResponse struct {
|
|||
MemoryUnitCount int32 `json:"memory_unit_count"`
|
||||
// Tags associated with this document
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
DocumentMetadata map[string]interface{} `json:"document_metadata,omitempty"`
|
||||
RetainParams map[string]interface{} `json:"retain_params,omitempty"`
|
||||
}
|
||||
|
||||
type _DocumentResponse DocumentResponse
|
||||
|
|
@ -260,6 +262,72 @@ func (o *DocumentResponse) SetTags(v []string) {
|
|||
o.Tags = v
|
||||
}
|
||||
|
||||
// GetDocumentMetadata returns the DocumentMetadata field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *DocumentResponse) GetDocumentMetadata() map[string]interface{} {
|
||||
if o == nil {
|
||||
var ret map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
return o.DocumentMetadata
|
||||
}
|
||||
|
||||
// GetDocumentMetadataOk returns a tuple with the DocumentMetadata field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *DocumentResponse) GetDocumentMetadataOk() (map[string]interface{}, bool) {
|
||||
if o == nil || IsNil(o.DocumentMetadata) {
|
||||
return map[string]interface{}{}, false
|
||||
}
|
||||
return o.DocumentMetadata, true
|
||||
}
|
||||
|
||||
// HasDocumentMetadata returns a boolean if a field has been set.
|
||||
func (o *DocumentResponse) HasDocumentMetadata() bool {
|
||||
if o != nil && !IsNil(o.DocumentMetadata) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDocumentMetadata gets a reference to the given map[string]interface{} and assigns it to the DocumentMetadata field.
|
||||
func (o *DocumentResponse) SetDocumentMetadata(v map[string]interface{}) {
|
||||
o.DocumentMetadata = v
|
||||
}
|
||||
|
||||
// GetRetainParams returns the RetainParams field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *DocumentResponse) GetRetainParams() map[string]interface{} {
|
||||
if o == nil {
|
||||
var ret map[string]interface{}
|
||||
return ret
|
||||
}
|
||||
return o.RetainParams
|
||||
}
|
||||
|
||||
// GetRetainParamsOk returns a tuple with the RetainParams field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *DocumentResponse) GetRetainParamsOk() (map[string]interface{}, bool) {
|
||||
if o == nil || IsNil(o.RetainParams) {
|
||||
return map[string]interface{}{}, false
|
||||
}
|
||||
return o.RetainParams, true
|
||||
}
|
||||
|
||||
// HasRetainParams returns a boolean if a field has been set.
|
||||
func (o *DocumentResponse) HasRetainParams() bool {
|
||||
if o != nil && !IsNil(o.RetainParams) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetRetainParams gets a reference to the given map[string]interface{} and assigns it to the RetainParams field.
|
||||
func (o *DocumentResponse) SetRetainParams(v map[string]interface{}) {
|
||||
o.RetainParams = v
|
||||
}
|
||||
|
||||
func (o DocumentResponse) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
|
|
@ -280,6 +348,12 @@ func (o DocumentResponse) ToMap() (map[string]interface{}, error) {
|
|||
if !IsNil(o.Tags) {
|
||||
toSerialize["tags"] = o.Tags
|
||||
}
|
||||
if o.DocumentMetadata != nil {
|
||||
toSerialize["document_metadata"] = o.DocumentMetadata
|
||||
}
|
||||
if o.RetainParams != nil {
|
||||
toSerialize["retain_params"] = o.RetainParams
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ class DocumentResponse(BaseModel):
|
|||
updated_at: StrictStr
|
||||
memory_unit_count: StrictInt
|
||||
tags: Optional[List[StrictStr]] = Field(default=None, description="Tags associated with this document")
|
||||
__properties: ClassVar[List[str]] = ["id", "bank_id", "original_text", "content_hash", "created_at", "updated_at", "memory_unit_count", "tags"]
|
||||
document_metadata: Optional[Dict[str, Any]] = None
|
||||
retain_params: Optional[Dict[str, Any]] = None
|
||||
__properties: ClassVar[List[str]] = ["id", "bank_id", "original_text", "content_hash", "created_at", "updated_at", "memory_unit_count", "tags", "document_metadata", "retain_params"]
|
||||
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
|
|
@ -80,6 +82,16 @@ class DocumentResponse(BaseModel):
|
|||
if self.content_hash is None and "content_hash" in self.model_fields_set:
|
||||
_dict['content_hash'] = None
|
||||
|
||||
# set to None if document_metadata (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.document_metadata is None and "document_metadata" in self.model_fields_set:
|
||||
_dict['document_metadata'] = None
|
||||
|
||||
# set to None if retain_params (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.retain_params is None and "retain_params" in self.model_fields_set:
|
||||
_dict['retain_params'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
|
|
@ -99,7 +111,9 @@ class DocumentResponse(BaseModel):
|
|||
"created_at": obj.get("created_at"),
|
||||
"updated_at": obj.get("updated_at"),
|
||||
"memory_unit_count": obj.get("memory_unit_count"),
|
||||
"tags": obj.get("tags")
|
||||
"tags": obj.get("tags"),
|
||||
"document_metadata": obj.get("document_metadata"),
|
||||
"retain_params": obj.get("retain_params")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
|
|
|||
|
|
@ -954,6 +954,22 @@ export type DocumentResponse = {
|
|||
* Tags associated with this document
|
||||
*/
|
||||
tags?: Array<string>;
|
||||
/**
|
||||
* Document Metadata
|
||||
*
|
||||
* Document metadata
|
||||
*/
|
||||
document_metadata?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
/**
|
||||
* Retain Params
|
||||
*
|
||||
* Parameters used during retain
|
||||
*/
|
||||
retain_params?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -36,6 +36,48 @@ import {
|
|||
|
||||
const ITEMS_PER_PAGE = 50;
|
||||
|
||||
function formatRelativeTime(dateStr: string): string {
|
||||
const now = Date.now();
|
||||
const then = new Date(dateStr).getTime();
|
||||
const seconds = Math.floor((now - then) / 1000);
|
||||
if (seconds < 60) return "just now";
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days < 30) return `${days}d ago`;
|
||||
const months = Math.floor(days / 30);
|
||||
if (months < 12) return `${months}mo ago`;
|
||||
return `${Math.floor(months / 12)}y ago`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function MetadataBadges({ metadata }: { metadata: Record<string, any> }) {
|
||||
const entries = Object.entries(metadata);
|
||||
if (entries.length === 0) return <span>-</span>;
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{entries.slice(0, 3).map(([k, v]) => (
|
||||
<span
|
||||
key={k}
|
||||
className="text-xs px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400 font-medium"
|
||||
>
|
||||
{k}={String(v)}
|
||||
</span>
|
||||
))}
|
||||
{entries.length > 3 && (
|
||||
<span className="text-xs px-2 py-0.5 text-muted-foreground">+{entries.length - 3}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocumentsView() {
|
||||
const { currentBank } = useBank();
|
||||
const [documents, setDocuments] = useState<any[]>([]);
|
||||
|
|
@ -238,8 +280,8 @@ export function DocumentsView() {
|
|||
<TableHead>Document ID</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Tags</TableHead>
|
||||
<TableHead>Context</TableHead>
|
||||
<TableHead>Text Length</TableHead>
|
||||
<TableHead>Metadata</TableHead>
|
||||
<TableHead>Size</TableHead>
|
||||
<TableHead>Memory Units</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
|
@ -251,11 +293,14 @@ export function DocumentsView() {
|
|||
className={`cursor-pointer hover:bg-muted/50 ${selectedDocument?.id === doc.id ? "bg-primary/10" : ""}`}
|
||||
onClick={() => viewDocumentText(doc.id)}
|
||||
>
|
||||
<TableCell title={doc.id} className="text-card-foreground">
|
||||
{doc.id.length > 30 ? doc.id.substring(0, 30) + "..." : doc.id}
|
||||
<TableCell className="text-card-foreground font-mono text-xs break-all">
|
||||
{doc.id}
|
||||
</TableCell>
|
||||
<TableCell className="text-card-foreground">
|
||||
{doc.created_at ? new Date(doc.created_at).toLocaleString() : "N/A"}
|
||||
<TableCell
|
||||
className="text-card-foreground"
|
||||
title={doc.created_at ? new Date(doc.created_at).toLocaleString() : ""}
|
||||
>
|
||||
{doc.created_at ? formatRelativeTime(doc.created_at) : "N/A"}
|
||||
</TableCell>
|
||||
<TableCell className="text-card-foreground">
|
||||
{doc.tags && doc.tags.length > 0 ? (
|
||||
|
|
@ -279,10 +324,15 @@ export function DocumentsView() {
|
|||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-card-foreground">
|
||||
{doc.retain_params?.context || "-"}
|
||||
{doc.document_metadata &&
|
||||
Object.keys(doc.document_metadata).length > 0 ? (
|
||||
<MetadataBadges metadata={doc.document_metadata} />
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-card-foreground">
|
||||
{doc.text_length?.toLocaleString()} chars
|
||||
{formatBytes(doc.text_length || 0)}
|
||||
</TableCell>
|
||||
<TableCell className="text-card-foreground">
|
||||
{doc.memory_unit_count}
|
||||
|
|
@ -362,7 +412,7 @@ export function DocumentsView() {
|
|||
|
||||
{/* Document Detail Panel - Fixed on Right */}
|
||||
{documents.length > 0 && selectedDocument && (
|
||||
<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">
|
||||
<div className="fixed right-0 top-0 h-screen w-[560px] bg-card border-l-2 border-primary shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
|
||||
<div className="p-5">
|
||||
{/* Header with close button */}
|
||||
<div className="flex justify-between items-center mb-6 pb-4 border-b border-border">
|
||||
|
|
@ -424,46 +474,47 @@ export function DocumentsView() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Text Length */}
|
||||
{/* Text Size */}
|
||||
{selectedDocument.original_text && (
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
|
||||
Text Length
|
||||
Size
|
||||
</div>
|
||||
<div className="text-sm font-medium text-card-foreground">
|
||||
{selectedDocument.original_text.length.toLocaleString()} characters
|
||||
{formatBytes(new Blob([selectedDocument.original_text]).size)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Retain Parameters */}
|
||||
{selectedDocument.retain_params && (
|
||||
<div className="p-4 bg-muted/50 rounded-lg">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
|
||||
<div className="p-4 bg-muted/50 rounded-lg space-y-3">
|
||||
<div className="text-xs font-bold text-muted-foreground uppercase">
|
||||
Retain Parameters
|
||||
</div>
|
||||
<div className="text-sm space-y-2 text-card-foreground">
|
||||
{selectedDocument.retain_params.context && (
|
||||
<div>
|
||||
<span className="font-semibold">Context:</span>{" "}
|
||||
<div className="text-xs text-muted-foreground mb-1">Context</div>
|
||||
<div className="text-sm text-card-foreground">
|
||||
{selectedDocument.retain_params.context}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedDocument.retain_params.event_date && (
|
||||
<div>
|
||||
<span className="font-semibold">Event Date:</span>{" "}
|
||||
<div className="text-xs text-muted-foreground mb-1">Event Date</div>
|
||||
<div className="text-sm text-card-foreground">
|
||||
{new Date(selectedDocument.retain_params.event_date).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
{selectedDocument.retain_params.metadata && (
|
||||
<div className="mt-2">
|
||||
<span className="font-semibold">Metadata:</span>
|
||||
<pre className="mt-1 text-xs bg-background p-2 rounded text-card-foreground">
|
||||
{JSON.stringify(selectedDocument.retain_params.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{selectedDocument.retain_params.metadata &&
|
||||
Object.keys(selectedDocument.retain_params.metadata).length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Metadata</div>
|
||||
<MetadataBadges metadata={selectedDocument.retain_params.metadata} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -5815,6 +5815,32 @@
|
|||
"title": "Tags",
|
||||
"description": "Tags associated with this document",
|
||||
"default": []
|
||||
},
|
||||
"document_metadata": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Document Metadata",
|
||||
"description": "Document metadata"
|
||||
},
|
||||
"retain_params": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Retain Params",
|
||||
"description": "Parameters used during retain"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
|
@ -5833,9 +5859,17 @@
|
|||
"bank_id": "user123",
|
||||
"content_hash": "abc123",
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"document_metadata": {
|
||||
"channel": "#general",
|
||||
"source": "slack"
|
||||
},
|
||||
"id": "session_1",
|
||||
"memory_unit_count": 15,
|
||||
"original_text": "Full document text here...",
|
||||
"retain_params": {
|
||||
"context": "Team meeting notes",
|
||||
"event_date": "2024-01-15"
|
||||
},
|
||||
"tags": [
|
||||
"user_a",
|
||||
"session_123"
|
||||
|
|
|
|||
Loading…
Reference in a new issue