Saltar a contenido

Estado de fase (MASTER-00): MVP — alcance del MVP Shipaton sujeto a resultados de Gate 0 y PoC. Gobernanza: MASTER-00.

Frontend Spec: Animated Node Detail Inspector Drawer Composable (NodeInspector)

1. Overview & Purpose

This specification details the side-drawer component (NodeInspector) in :docugraph-app. Activated when a user taps a graph node on FE-03 GraphCanvas, it slides in smoothly from the screen margin (250ms cubic-bezier transition), displaying YAML frontmatter metadata, rendered Markdown preview, dependency link navigation chips, and a "Copy MCP Context Bundle" button that invokes BE-04 getTaskContext.

2. Component Signature & Props

package io.docugraph.app.ui.inspector

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun NodeInspector(
    nodeId: String?,
    isOpen: Boolean,
    onClose: () -> Unit,
    onNavigateToNode: (nodeId: String) -> Unit,
    viewModel: NodeInspectorViewModel,
    modifier: Modifier = Modifier
)

3. UI State Model & Data Classes

package io.docugraph.app.ui.inspector

import io.docugraph.backend.models.graph.GraphNodeDto

enum class InspectorTab {
    FRONTMATTER,
    MARKDOWN_PREVIEW,
    TRACEABILITY_LINKS
}

data class NodeInspectorUiState(
    val nodeDetail: GraphNodeDto? = null,
    val activeTab: InspectorTab = InspectorTab.MARKDOWN_PREVIEW,
    val upstreamLinks: List<String> = emptyList(),
    val downstreamLinks: List<String> = emptyList(),
    val isExtractingContext: Boolean = false,
    val contextBundleMarkdown: String? = null,
    val copySuccessBanner: Boolean = false
)

4. UI Layout Hierarchy & Composables

AnimatedVisibility (
    visible = isOpen && nodeId != null,
    enter = slideInHorizontally(initialOffsetX = { it }),
    exit = slideOutHorizontally(targetOffsetX = { it })
)
 └── Surface (Modifier.fillMaxHeight().width(380.dp), elevation = 8.dp)
      └── Column (Padding 16.dp)
           ├── HeaderRow
           │    ├── AtomicId Badge (e.g. "US-01") + Status Tag ("APPROVED")
           │    └── IconButton (Close -> onClose())
           ├── Text (Title, style = TitleLarge)
           ├── Text (FilePath, style = BodySmall, color = Gray)
           ├── Spacer (Height 12.dp)
           ├── TabRow (selectedTabIndex = activeTab.ordinal)
           │    ├── Tab ("Preview")
           │    ├── Tab ("Frontmatter")
           │    └── Tab ("Traceability")
           ├── HorizontalDivider()
           ├── Spacer (Height 12.dp)
           ├── Content (Weight 1.0f):
           │    ├── if (Preview) MarkdownRenderer(content)
           │    ├── if (Frontmatter) FrontmatterJsonView(frontmatterJson)
           │    └── if (Traceability) TraceabilityLinksColumn
           │         ├── Upstream Chips (OnClick = onNavigateToNode(id))
           │         └── Downstream Chips (OnClick = onNavigateToNode(id))
           ├── Spacer (Height 16.dp)
           └── Button ("Copy MCP Context Bundle", onClick = extractMcpContext)
                └── Content: if (isExtractingContext) CircularProgressIndicator else Icon(Copy) + Text("Copy Context")

5. MCP Context Bundle Copy Interaction

fun extractMcpContext(taskId: String) {
    viewModelScope.launch {
        _uiState.update { it.copy(isExtractingContext = true) }
        try {
            val response = mcpService.getTaskContext(taskId = taskId, maxDepth = 5)
            clipboardManager.setText(AnnotatedString(response.markdownBundle))
            _uiState.update { it.copy(isExtractingContext = false, copySuccessBanner = true) }
        } catch (e: Exception) {
            _uiState.update { it.copy(isExtractingContext = false) }
        }
    }
}

6. Verification & Test Plan

  • Animation Test: Verify 250ms slide-in/out transition smooth execution.
  • UiTest: Tap upstream dependency chip, verify onNavigateToNode is called. Tap "Copy MCP Context Bundle", verify clipboard receives formatted markdown bundle.