Skip to main content
Glama

render_dashboard

Generate a standalone HTML dashboard from a VOC report, showing sentiment bar, pain-point panels, executive summary, and listing optimization card.

Instructions

Render a VOC report as a standalone black-gold HTML dashboard.

The output is single-file HTML — no external dependencies, opens directly in any browser. Includes sentiment bar, pain-point / selling-point panels, executive summary, and (if improvements provided) a copy-ready listing optimization card.

Args: report: Output from analyze_reviews / voc_full / analyze_csv. improvements: Optional output from extract_listing_improvements. product_name: Friendly product name for the headline. output_path: Optional file path to write the HTML to.

Returns: {html, bytes, output_path}

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
reportYes
improvementsNo
product_nameNo
output_pathNo

Implementation Reference

  • Core handler that renders a VOC report dict into a standalone black-gold HTML dashboard string. Reads a template file, computes sentiment percentages, formats pain/selling points, listing improvements, and substitutions into the template.
    def render_dashboard(
        report: dict,
        *,
        improvements: dict | None = None,
        product_name: str | None = None,
    ) -> str:
        """Render `report` as a standalone HTML dashboard.
    
        Args:
            report: The VOC analysis result. Expected shape:
                {
                  "asin", "market", "report_markdown",
                  "sentiment": {positive, neutral, negative},
                  "pain_points": [{zh, en, count}, ...],
                  "selling_points": [{zh, en, count}, ...],
                  "tips": [{zh, en}, ...],
                  "summary_zh", "summary_en"
                }
            improvements: Optional output of `extract_listing_improvements`.
            product_name: Optional friendly product name for the headline.
    
        Returns:
            Standalone HTML string. Save to .html and open in any browser.
        """
        template = TEMPLATE_PATH.read_text(encoding="utf-8")
        sentiment = report.get("sentiment") or {}
        pos = float(sentiment.get("positive", 0))
        neu = float(sentiment.get("neutral", 0))
        neg = float(sentiment.get("negative", 0))
        total = pos + neu + neg
        if total > 0:
            pos_pct = round(pos * 100 / total, 1)
            neu_pct = round(neu * 100 / total, 1)
            neg_pct = round(100 - pos_pct - neu_pct, 1)
        else:
            pos_pct = neu_pct = neg_pct = 0
    
        pains = report.get("pain_points") or []
        sellings = report.get("selling_points") or []
        asin = _esc(report.get("asin") or "")
        market = _esc(report.get("market") or "US")
        headline = product_name or asin or "Review Analysis"
    
        total_reviews = report.get("total_reviews") or report.get("meta", {}).get("rows_used") or "—"
    
        top_pain = pains[0]["zh"] if pains else "—"
        top_pain_count = pains[0].get("count") if pains and pains[0].get("count") is not None else ""
    
        meta_parts = [
            f'<span><b>ASIN</b>{asin or "—"}</span>',
            f'<span><b>Market</b>{market}</span>',
            f'<span><b>Reviews</b>{total_reviews}</span>',
            f'<span><b>Pain points</b>{len(pains)}</span>',
            f'<span><b>Selling points</b>{len(sellings)}</span>',
        ]
    
        substitutions = {
            "{{TITLE}}": _esc(headline),
            "{{HEADLINE_HTML}}": (
                f'<em>{_esc(headline)}</em> — VOC Report'
                if headline != "Review Analysis"
                else "<em>Voice-of-Customer</em> Report"
            ),
            "{{SUB_HTML}}": _esc(
                report.get("summary_en")
                or "Sentiment, pain points, selling points and copy-ready listing improvements — extracted from real customer language."
            ),
            "{{META_ROW_HTML}}": "\n".join(meta_parts),
            "{{KPI_TOTAL}}": str(total_reviews),
            "{{KPI_TOTAL_SUB}}": f"market={market}",
            "{{KPI_POS}}": str(pos_pct),
            "{{KPI_NEG}}": str(neg_pct),
            "{{KPI_PAIN}}": _esc(top_pain),
            "{{KPI_PAIN_SUB}}": f"flagged {top_pain_count}× across reviews" if top_pain_count else "Top signal to fix",
            "{{POS_PCT}}": str(pos_pct),
            "{{NEU_PCT}}": str(neu_pct),
            "{{NEG_PCT}}": str(neg_pct),
            "{{TOTAL_REVIEWS}}": str(total_reviews),
            "{{PAIN_COUNT}}": str(len(pains)),
            "{{SELLING_COUNT}}": str(len(sellings)),
            "{{PAIN_POINTS_HTML}}": "\n".join(_render_point(p) for p in pains[:8]) or '<li style="color:var(--text-muted);">No pain points detected</li>',
            "{{SELLING_POINTS_HTML}}": "\n".join(_render_point(p) for p in sellings[:8]) or '<li style="color:var(--text-muted);">No selling points detected</li>',
            "{{LISTING_HTML}}": _render_listing(report, improvements),
            "{{SUMMARY_EN}}": _esc(report.get("summary_en", "")),
            "{{SUMMARY_ZH}}": _esc(report.get("summary_zh", "")),
            "{{GENERATED_AT}}": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
        }
        # Decode escaped quotes in the embedded en-text spans:
        substitutions["{{PAIN_POINTS_HTML}}"] = substitutions["{{PAIN_POINTS_HTML}}"].replace(""", '"')
        substitutions["{{SELLING_POINTS_HTML}}"] = substitutions["{{SELLING_POINTS_HTML}}"].replace(""", '"')
    
        out = template
        for k, v in substitutions.items():
            out = out.replace(k, v)
        return out
  • Helper to render a single pain/selling point as an HTML list item with zh/en labels and frequency count.
    def _render_point(p: dict) -> str:
        zh = _esc(p.get("zh", ""))
        en = _esc(p.get("en", ""))
        count = p.get("count")
        freq = f"×{count}" if count is not None else ""
        return (
            f'<li>'
            f'<span class="label"><b>{zh}</b>'
            f'{(f"<div class="en">{en}</div>") if en else ""}</span>'
            f'<span class="freq">{freq}</span>'
            f'</li>'
        )
  • Helper to render the listing optimization card HTML, showing title suggestion, bullets, description, keywords, and warnings (or a placeholder if no improvements provided).
    def _render_listing(report: dict, improvements: dict | None) -> str:
        if not improvements:
            return (
                '<div class="listing-card">'
                '<h3>Listing Optimization</h3>'
                '<p style="color:var(--text-muted);">Call '
                '<code>extract_listing_improvements</code> to populate this section '
                'with copy-ready title / bullets / description rewrites grounded '
                'in actual customer language.</p>'
                '</div>'
            )
    
        title = _esc(improvements.get("title_suggestion", ""))
        title_reason = _esc(improvements.get("title_reasoning", ""))
        bullets = improvements.get("bullet_suggestions") or []
        description = _esc(improvements.get("description_paragraph", ""))
        keywords = improvements.get("keyword_opportunities") or []
        warnings = improvements.get("warnings") or []
    
        bullets_html = "".join(
            f'<li>{_esc(b.get("text",""))}'
            + (f'<span class="addresses">addresses: {_esc(b.get("addresses",""))}</span>' if b.get("addresses") else "")
            + '</li>'
            for b in bullets
        )
    
        kw_html = ""
        if keywords:
            items = "".join(f'<li><span class="label">{_esc(k)}</span></li>' for k in keywords)
            kw_html = (
                '<div class="listing-field">'
                '<div class="label">Missing Keyword Opportunities</div>'
                f'<ul class="bullet-list">{items}</ul>'
                '</div>'
            )
    
        warn_html = ""
        if warnings:
            items = "".join(f'<li>{_esc(w)}</li>' for w in warnings)
            warn_html = (
                '<div class="listing-field">'
                '<div class="label">Signals NOT fixable in copy (product/QA)</div>'
                f'<ul class="bullet-list" style="border-left:3px solid var(--accent-coral);">{items}</ul>'
                '</div>'
            )
    
        return f"""
    <div class="listing-card">
      <h3>Listing Optimization · grounded in customer language</h3>
    
      <div class="listing-field">
        <div class="label">Proposed Title</div>
        <div class="value">{title}</div>
        <div class="reasoning">{title_reason}</div>
      </div>
    
      <div class="listing-field">
        <div class="label">Bullet Suggestions</div>
        <ul class="bullet-list">{bullets_html}</ul>
      </div>
    
      <div class="listing-field">
        <div class="label">Description Paragraph</div>
        <div class="value" style="color:var(--text-primary);">{description}</div>
      </div>
    
      {kw_html}
      {warn_html}
    </div>
    """
  • Business-logic tool implementation that delegates HTML rendering to dashboard.render_dashboard(), optionally writes to a file path, and returns {html, bytes, output_path}.
    def render_dashboard(
        report: dict[str, Any],
        *,
        improvements: dict[str, Any] | None = None,
        product_name: str | None = None,
        output_path: str | None = None,
    ) -> dict[str, Any]:
        """Render a VOC report as a standalone black-gold HTML dashboard.
    
        Writes the file (if `output_path` provided) and also returns the HTML
        string so the caller can save it elsewhere or pipe it into a web preview.
        """
        from . import dashboard
    
        html_str = dashboard.render_dashboard(
            report, improvements=improvements, product_name=product_name
        )
        written_path = None
        if output_path:
            written_path = str(Path(output_path).expanduser().resolve())
            Path(written_path).parent.mkdir(parents=True, exist_ok=True)
            Path(written_path).write_text(html_str, encoding="utf-8")
        return {
            "html": html_str,
            "bytes": len(html_str.encode("utf-8")),
            "output_path": written_path,
        }
  • MCP tool registration via @mcp.tool() decorator. Defines the user-facing API signature, docstring, and delegates to tools.render_dashboard().
    @mcp.tool()
    def render_dashboard(
        report: dict,
        improvements: dict | None = None,
        product_name: str | None = None,
        output_path: str | None = None,
    ) -> dict:
        """Render a VOC report as a standalone black-gold HTML dashboard.
    
        The output is single-file HTML — no external dependencies, opens directly
        in any browser. Includes sentiment bar, pain-point / selling-point
        panels, executive summary, and (if `improvements` provided) a
        copy-ready listing optimization card.
    
        Args:
            report: Output from `analyze_reviews` / `voc_full` / `analyze_csv`.
            improvements: Optional output from `extract_listing_improvements`.
            product_name: Friendly product name for the headline.
            output_path: Optional file path to write the HTML to.
    
        Returns: {html, bytes, output_path}
        """
        return tools.render_dashboard(
            report=report, improvements=improvements,
            product_name=product_name, output_path=output_path,
        )
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description carries full burden. Describes output format and optional features (improvements card). However, lacks details on error handling, side effects (file writing), and default behavior when output_path is omitted. Acceptable but not comprehensive.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Description is concise (~150 words) with a clear front-loaded purpose statement and a structured Args section. Every sentence contributes valuable information without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no annotations, no output schema, and 1 required param, the description covers the tool's function, inputs, and outputs adequately. It mentions return type {html, bytes, output_path}. Minor gap: does not clarify behavior when output_path is absent (presumably returns HTML string only).

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, but the description's Args section explains each parameter's purpose and source (e.g., report from `analyze_reviews`, improvements from `extract_listing_improvements`). Adds significant meaning beyond the bare schema types.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states it renders a VOC report as a standalone HTML dashboard. Specifies output format (single-file HTML) and lists included sections (sentiment bar, panels, executive summary, optimization card). Distinguishes from sibling analysis tools by being a rendering step.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Implicitly indicates usage after obtaining a report from sibling tools like `analyze_reviews`. Does not explicitly state when not to use, but the context is clear. Could be improved by stating prerequisites (e.g., 'Use after analysis tools').

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mguozhen/voc-amazon-reviews'

If you have feedback or need assistance with the MCP directory API, please join our Discord server