> ## Documentation Index
> Fetch the complete documentation index at: https://docs.galileo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Tone

> Analyze and optimize the emotional tone of AI responses using Galileo's Tone Metric to ensure appropriate emotional context and user engagement

export const MultiClassConfusionMatrix = ({matrix, labels, labelDisplayNames = {}, maxWidth = 560, displayFormat = "fraction", fractionDigits = 3, percentageDigits = 1}) => {
  const clampPct = pct => Math.max(0, Math.min(100, Number(pct) || 0));
  const formatValue = pct => {
    const p = clampPct(pct);
    if (displayFormat === "fraction") {
      const d = Number.isFinite(Number(fractionDigits)) ? Number(fractionDigits) : 3;
      return (p / 100).toFixed(d);
    }
    const d = Number.isFinite(Number(percentageDigits)) ? Number(percentageDigits) : 1;
    return `${p.toFixed(d)}%`;
  };
  const palette = ["#f8fafc", "#eff6ff", "#dbeafe", "#bfdbfe", "#93c5fd", "#60a5fa", "#3b82f6", "#2563eb", "#1d4ed8", "#1e40af"];
  const getBg = pct => {
    const p = clampPct(pct);
    const idx = p === 100 ? 9 : Math.floor(p / 10);
    return palette[idx];
  };
  const getColor = pct => clampPct(pct) >= 60 ? "#ffffff" : "#1e3a8a";
  const n = labels.length;
  if (!matrix || !Array.isArray(matrix) || matrix.length !== n) {
    return <div style={{
      color: "red",
      padding: "1rem",
      border: "1px solid red"
    }}>MultiClassConfusionMatrix: matrix must be an {n}x{n} array matching labels.</div>;
  }
  const cellSize = Math.max(44, Math.min(72, Math.floor((maxWidth - 120) / n)));
  return <div style={{
    maxWidth: maxWidth + "px",
    margin: "1rem 0"
  }}>
      <div style={{
    display: "grid",
    gridTemplateColumns: `auto repeat(${n}, ${cellSize}px)`,
    gridTemplateRows: `auto auto auto repeat(${n}, ${cellSize}px) auto`,
    gap: "2px"
  }}>
        {}
        <div></div>
        <div style={{
    gridColumn: `2 / ${n + 2}`,
    textAlign: "center",
    padding: "0.375rem",
    fontWeight: "600",
    fontSize: "0.95rem"
  }}>
          Confusion Matrix (Normalized)
        </div>

        {}
        <div></div>
        <div style={{
    gridColumn: `2 / ${n + 2}`,
    textAlign: "center",
    padding: "0.25rem",
    fontWeight: "600",
    fontSize: "0.8rem"
  }}>
          Predicted Classes
        </div>

        {}
        <div></div>
        {labels.map((label, ci) => <div key={`plabel-${ci}`} style={{
    textAlign: "center",
    padding: "0.125rem",
    fontSize: "0.65rem",
    fontWeight: "500",
    display: "flex",
    alignItems: "flex-end",
    justifyContent: "center",
    wordBreak: "break-word",
    lineHeight: "1.15"
  }}>
            {labelDisplayNames[label] ?? label}
          </div>)}

        {}
        {labels.map((rowLabel, ri) => [<div key={`alabel-${ri}`} style={{
    padding: "0.125rem 0.375rem 0.125rem 0",
    fontSize: "0.65rem",
    fontWeight: "500",
    display: "flex",
    alignItems: "center",
    justifyContent: "flex-end",
    whiteSpace: "nowrap",
    textAlign: "right"
  }}>
              {labelDisplayNames[rowLabel] ?? rowLabel}
            </div>, ...labels.map((colLabel, ci) => {
    const pct = (matrix[ri][ci] ?? 0) * 100;
    return <div key={`cell-${ri}-${ci}`} style={{
      background: getBg(pct),
      color: getColor(pct),
      padding: "0.125rem",
      textAlign: "center",
      borderRadius: "4px",
      width: "100%",
      aspectRatio: "1 / 1",
      display: "flex",
      flexDirection: "column",
      alignItems: "center",
      justifyContent: "center",
      border: "1px solid rgba(148, 163, 184, 0.35)"
    }}>
                  <div style={{
      fontSize: "0.7rem",
      fontWeight: "700"
    }}>{formatValue(pct)}</div>
                </div>;
  })]).flat()}

        {}
        <div></div>
        <div style={{
    gridColumn: `2 / ${n + 2}`,
    marginTop: "0.5rem",
    display: "flex",
    alignItems: "center",
    gap: "0.5rem"
  }}>
          <span style={{
    fontSize: "0.7rem",
    fontWeight: "500"
  }}>{displayFormat === "fraction" ? "0.0" : "0%"}</span>
          <div style={{
    display: "flex",
    flex: 1,
    height: "10px",
    borderRadius: "4px",
    overflow: "hidden",
    border: "1px solid rgba(148, 163, 184, 0.35)"
  }}>
            {palette.map((color, idx) => <div key={idx} style={{
    flex: 1,
    height: "100%",
    background: color
  }} />)}
          </div>
          <span style={{
    fontSize: "0.7rem",
    fontWeight: "500"
  }}>{displayFormat === "fraction" ? "1.0" : "100%"}</span>
        </div>
      </div>
    </div>;
};

export const MultiClassClassificationReport = ({report, labels: labelsProp, labelDisplayNames: labelDisplayNamesProp = {}, decimals = 4, maxWidth = 560, showConfusionMatrix = true}) => {
  const parseReport = reportStr => {
    const lines = reportStr.trim().split('\n').filter(line => line.trim());
    const result = {
      classes: [],
      accuracy: null,
      macroAvg: null,
      weightedAvg: null,
      totalSupport: null
    };
    for (const line of lines) {
      const parts = line.trim().split(/\s+/);
      if (parts[0] === 'precision') continue;
      if (parts[0] === 'accuracy') {
        result.accuracy = parseFloat(parts[1]);
        result.totalSupport = parseInt(parts[2], 10);
        continue;
      }
      if (parts[0] === 'macro' && parts[1] === 'avg') {
        result.macroAvg = {
          precision: parseFloat(parts[2]),
          recall: parseFloat(parts[3]),
          f1: parseFloat(parts[4]),
          support: parseInt(parts[5], 10)
        };
        continue;
      }
      if (parts[0] === 'weighted' && parts[1] === 'avg') {
        result.weightedAvg = {
          precision: parseFloat(parts[2]),
          recall: parseFloat(parts[3]),
          f1: parseFloat(parts[4]),
          support: parseInt(parts[5], 10)
        };
        continue;
      }
      if (parts.length >= 5) {
        const support = parseInt(parts[parts.length - 1], 10);
        const f1 = parseFloat(parts[parts.length - 2]);
        const recall = parseFloat(parts[parts.length - 3]);
        const precision = parseFloat(parts[parts.length - 4]);
        if (!isNaN(support) && !isNaN(f1) && !isNaN(recall) && !isNaN(precision)) {
          const name = parts.slice(0, parts.length - 4).join(' ');
          result.classes.push({
            name,
            precision,
            recall,
            f1,
            support
          });
        }
      }
    }
    return result;
  };
  const buildConfusionMatrix = classes => {
    const n = classes.length;
    const tp = classes.map(c => c.recall * c.support);
    const predTotal = classes.map((c, i) => c.precision > 0 ? tp[i] / c.precision : 0);
    const fpTotal = classes.map((c, i) => Math.max(0, predTotal[i] - tp[i]));
    const fnTotal = classes.map((c, i) => Math.max(0, c.support - tp[i]));
    const cm = Array.from({
      length: n
    }, () => Array(n).fill(0));
    for (let i = 0; i < n; i++) cm[i][i] = tp[i];
    if (n > 1) {
      const rowTargets = fnTotal;
      const colTargets = fpTotal;
      const offDiag = Array.from({
        length: n
      }, () => Array(n).fill(0));
      for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
          if (i !== j) offDiag[i][j] = 1;
        }
      }
      for (let iter = 0; iter < 50; iter++) {
        for (let i = 0; i < n; i++) {
          const rowSum = offDiag[i].reduce((a, v, j) => i !== j ? a + v : a, 0);
          if (rowSum > 0 && rowTargets[i] > 0) {
            const factor = rowTargets[i] / rowSum;
            for (let j = 0; j < n; j++) if (i !== j) offDiag[i][j] *= factor;
          } else {
            for (let j = 0; j < n; j++) if (i !== j) offDiag[i][j] = 0;
          }
        }
        for (let j = 0; j < n; j++) {
          const colSum = offDiag.reduce((a, row, i) => i !== j ? a + row[j] : a, 0);
          if (colSum > 0 && colTargets[j] > 0) {
            const factor = colTargets[j] / colSum;
            for (let i = 0; i < n; i++) if (i !== j) offDiag[i][j] *= factor;
          } else {
            for (let i = 0; i < n; i++) if (i !== j) offDiag[i][j] = 0;
          }
        }
      }
      for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
          if (i !== j) cm[i][j] = Math.max(0, offDiag[i][j]);
        }
      }
    }
    const normalized = cm.map((row, i) => {
      const rowSum = classes[i].support;
      return row.map(v => rowSum > 0 ? v / rowSum : 0);
    });
    return normalized;
  };
  let labelsArr, labelDisplayNames;
  try {
    labelsArr = typeof labelsProp === 'string' ? JSON.parse(labelsProp) : labelsProp;
    labelDisplayNames = typeof labelDisplayNamesProp === 'string' ? JSON.parse(labelDisplayNamesProp) : labelDisplayNamesProp;
  } catch (e) {
    return <div style={{
      color: "red",
      padding: "1rem",
      border: "1px solid red"
    }}>MultiClassClassificationReport: JSON parse error - {e.message}</div>;
  }
  const parsed = parseReport(report);
  if (parsed.classes.length < 2) {
    return <div style={{
      color: "red",
      padding: "1rem",
      border: "1px solid red"
    }}>MultiClassClassificationReport: Could not parse report. Expected at least 2 classes.</div>;
  }
  let orderedClasses = parsed.classes;
  if (Array.isArray(labelsArr) && labelsArr.length > 0) {
    const classMap = {};
    for (const c of parsed.classes) classMap[c.name] = c;
    orderedClasses = labelsArr.map(l => classMap[l]).filter(Boolean);
  }
  const classLabels = orderedClasses.map(c => c.name);
  const confusionMatrix = buildConfusionMatrix(orderedClasses);
  const rowStyle = {
    borderBottom: "1px solid rgba(148, 163, 184, 0.3)"
  };
  const cellStyle = {
    padding: "0.5rem 0.125rem"
  };
  const centerCellStyle = {
    textAlign: "center",
    padding: "0.5rem 0.125rem"
  };
  const fmtMetric = v => {
    const n = Number(v);
    return Number.isFinite(n) ? n.toFixed(decimals) : "—";
  };
  return <div>
      <table style={{
    width: "auto",
    borderCollapse: "collapse",
    marginBottom: "1.25rem",
    fontSize: "0.875rem"
  }}>
        <thead>
          <tr style={{
    borderBottom: "2px solid rgba(148, 163, 184, 0.5)"
  }}>
            <th style={{
    textAlign: "center",
    padding: "0.5rem 0.125rem",
    fontWeight: "600"
  }}></th>
            <th style={{
    textAlign: "center",
    padding: "0.5rem 0.125rem",
    fontWeight: "600"
  }}>Precision</th>
            <th style={{
    textAlign: "center",
    padding: "0.5rem 0.125rem",
    fontWeight: "600"
  }}>Recall</th>
            <th style={{
    textAlign: "center",
    padding: "0.5rem 0.125rem",
    fontWeight: "600"
  }}>F1-Score</th>
          </tr>
        </thead>
        <tbody>
          {orderedClasses.map(cls => <tr key={cls.name} style={rowStyle}>
              <td style={cellStyle}>{labelDisplayNames?.[cls.name] ?? cls.name}</td>
              <td style={centerCellStyle}>{fmtMetric(cls.precision)}</td>
              <td style={centerCellStyle}>{fmtMetric(cls.recall)}</td>
              <td style={centerCellStyle}>{fmtMetric(cls.f1)}</td>
            </tr>)}
        </tbody>
      </table>

      {showConfusionMatrix && <MultiClassConfusionMatrix matrix={confusionMatrix} labels={classLabels} labelDisplayNames={labelDisplayNames || ({})} maxWidth={maxWidth} displayFormat="fraction" fractionDigits={decimals > 3 ? 3 : decimals} />}
    </div>;
};

export const DefinitionCard = ({children}) => {
  return <Card variant="secondary">
    <div style={{
    padding: '0.5rem',
    border: '5px solid var(--primary-light)',
    borderRadius: '0.5rem',
    fontSize: '1.3rem',
    lineHeight: '1.4',
    boxShadow: '0 0 10px 10px var(--primary-light)'
  }}>
        {children}
      </div>

</Card>;
};

export const Scale = ({low, mid, high, lowLabel = "Low", midLabel = "Mid", highLabel = "High", lowDescription, midDescription, highDescription, midColor = "yellow", inverted = false}) => {
  const lowColor = inverted ? "green" : "red";
  const highColor = inverted ? "red" : "green";
  const gradientId = inverted ? "greenToRed" : "redToGreen";
  return <div style={{
    display: 'flex',
    flexDirection: 'column',
    width: '100%'
  }}>
      <svg width="100%" height="30" style={{
    marginBottom: '8px'
  }}>
        <defs>
          <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
            <stop offset="0%" stopColor={lowColor} />
            <stop offset="100%" stopColor={highColor} />
          </linearGradient>
        </defs>
        <rect width="100%" height="100%" fill={`url(#${gradientId})`} rx="4" ry="4" />
      </svg>

      <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    width: '100%',
    marginBottom: '16px'
  }}>
        <p style={{
    margin: 0,
    fontSize: '12px'
  }}>{low}</p>
        {mid && <p style={{
    margin: 0,
    fontSize: '12px'
  }}>{mid}</p>}
        <p style={{
    margin: 0,
    fontSize: '12px'
  }}>{high}</p>
      </div>

      <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    width: '100%'
  }}>
        <div style={{
    maxWidth: '40%'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    marginBottom: '4px'
  }}>
            <div style={{
    width: '12px',
    height: '12px',
    backgroundColor: lowColor,
    borderRadius: '50%',
    marginRight: '8px'
  }}></div>
            <p style={{
    margin: 0,
    fontWeight: 'bold',
    fontSize: '14px'
  }}>{lowLabel}</p>
          </div>
          {lowDescription && <p style={{
    margin: 0,
    fontSize: '14px',
    color: '#666',
    maxWidth: '250px',
    lineHeight: '1.4'
  }}>{lowDescription}</p>}
        </div>
        {mid && <div style={{
    maxWidth: '40%',
    textAlign: 'center'
  }}>
            <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: '4px'
  }}>
              <div style={{
    width: '12px',
    height: '12px',
    backgroundColor: midColor,
    borderRadius: '50%',
    marginRight: '8px'
  }}></div>
              <p style={{
    margin: 0,
    fontWeight: 'bold',
    fontSize: '14px'
  }}>{midLabel}</p>
            </div>
            {midDescription && <p style={{
    margin: 0,
    fontSize: '14px',
    color: '#666',
    maxWidth: '250px',
    textAlign: 'center',
    lineHeight: '1.4'
  }}>{midDescription}</p>}
          </div>}


        <div style={{
    maxWidth: '40%',
    textAlign: 'right'
  }}>
          <div style={{
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'flex-end',
    marginBottom: '4px'
  }}>
            <p style={{
    margin: 0,
    fontWeight: 'bold',
    fontSize: '14px'
  }}>{highLabel}</p>
            <div style={{
    width: '12px',
    height: '12px',
    backgroundColor: highColor,
    borderRadius: '50%',
    marginLeft: '8px'
  }}></div>
          </div>
          {highDescription && <p style={{
    margin: 0,
    fontSize: '14px',
    color: '#666',
    maxWidth: '250px',
    marginLeft: 'auto',
    lineHeight: '1.4'
  }}>{highDescription}</p>}
        </div>
      </div>
    </div>;
};

<DefinitionCard>
  <strong>Tone Analysis</strong> classifies the emotional tone of responses into distinct categories to ensure appropriate emotional context.
</DefinitionCard>

## Emotion categories

<Card>
  <div style={{display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.75rem'}}>
    <div style={{fontSize: '1.25rem', color: 'var(--primary-color)'}}>
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />

        <path d="m9 12 2 2 4-4" />
      </svg>
    </div>

    <h3 style={{margin: 0, fontSize: '1.25rem', fontWeight: '600'}}>Available Emotion Categories</h3>
  </div>

  <CardGroup cols={3}>
    <Card title="Neutral" icon="balance-scale">
      Balanced and objective tone
    </Card>

    <Card title="Joy" icon="face-smile">
      Happiness and delight
    </Card>

    <Card title="Love" icon="heart">
      Affection and warmth
    </Card>

    <Card title="Fear" icon="face-worried">
      Anxiety and concern
    </Card>

    <Card title="Surprise" icon="face-surprise">
      Astonishment and wonder
    </Card>

    <Card title="Sadness" icon="face-sad-tear">
      Melancholy and grief
    </Card>

    <Card title="Anger" icon="face-angry">
      Frustration and rage
    </Card>

    <Card title="Annoyance" icon="face-rolling-eyes">
      Irritation and displeasure
    </Card>

    <Card title="Confusion" icon="face-confused">
      Uncertainty and puzzlement
    </Card>
  </CardGroup>
</Card>

## Calculation method

Tone analysis is computed through a specialized process:

<Steps>
  <Step title="Model Architecture">
    The analysis system utilizes a Small Language Model (SLM) trained on a comprehensive combination of open-source and internal datasets to accurately classify emotional tones across multiple categories.
  </Step>

  <Step title="Performance Validation">
    The classification system demonstrates strong reliability with an 80% accuracy rate when evaluated against the GoEmotions validation dataset, a widely-used benchmark for emotion detection.
  </Step>
</Steps>

## Optimizing your AI system

<Card>
  <div style={{display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.75rem'}}>
    <div style={{fontSize: '1.25rem', color: 'var(--primary-color)'}}>
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
        <path d="M12 20h9" />

        <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
      </svg>
    </div>

    <h3 style={{margin: 0, fontSize: '1.25rem', fontWeight: '600'}}>Managing Tone in Your System</h3>
  </div>

  When optimizing the emotional tone of your system, consider these approaches:

  <div style={{ marginTop: "1rem", paddingTop: "0.75rem", borderTop: "1px solid rgba(209, 213, 219, 0.33)" }}>
    <strong>Define tone preferences:</strong> Set appropriate emotional tones for different contexts and user interactions.
  </div>

  <div style={{ marginTop: "0.75rem", paddingTop: "0.75rem", borderTop: "1px solid rgba(209, 213, 219, 0.33)" }}>
    <strong>Implement tone filters:</strong> Discourage undesirable emotional responses while promoting preferred tones.
  </div>
</Card>

<Note>
  Recognize and categorize the emotional tone of responses to align with user preferences and context, ensuring appropriate emotional engagement in AI interactions.
</Note>

## Performance Benchmarks

We evaluated the Tone classification model against human expert labels on an internal dataset spanning 8 emotion categories.

| Model                  | Macro F1 |
| :--------------------- | :------: |
| GPT-4.1                |   0.97   |
| GPT-4.1 Mini           |   0.94   |
| Gemini 3 Flash Preview |   0.97   |
| Claude Sonnet 4.5      |   0.83   |

### GPT-4.1 Classification Report

<MultiClassClassificationReport
  report={`              precision    recall  f1-score   support

anger     0.9886    1.0000    0.9943        87
confusion     0.9796    0.9600    0.9697        50
fear     0.9870    1.0000    0.9935        76
joy     0.9462    0.9888    0.9670        89
love     0.9623    0.9444    0.9533        54
neutral     0.9873    0.9873    0.9873        79
sadness     1.0000    0.9844    0.9921        64
surprise     0.9841    0.9394    0.9612        66

accuracy                         0.9788       565
macro avg     0.9794    0.9755    0.9773       565
weighted avg     0.9790    0.9788    0.9787       565`}
  maxWidth={720}
/>

<Note>
  Benchmarks based on internal evaluation dataset. Performance may vary by use case.
</Note>

## Related Resources

If you would like to dive deeper or start implementing Tone, check out the following resources:

### Examples

* [Tone Examples](https://app.galileo.ai) - Log in and explore the "Tone" Log Stream in the "Preset Metric Examples" Project to see this metric in action.
