fix: layout fixes
This commit is contained in:
@@ -56,9 +56,9 @@ function createSurfaceMessages(
|
||||
// ---- Tool argument interfaces ----
|
||||
|
||||
export interface RenderChartArgs {
|
||||
chartType: 'bar' | 'line' | 'pie';
|
||||
chartType: 'bar' | 'stacked-bar' | 'line' | 'pie';
|
||||
title?: string;
|
||||
series: Array<{ label: string; value: number }>;
|
||||
series: Array<{ label: string; value: number; segments?: Array<{ label: string; value: number }> }>;
|
||||
}
|
||||
|
||||
export interface RenderTableArgs {
|
||||
|
||||
@@ -323,7 +323,7 @@ Available Data Tools:
|
||||
- get_media_posts: Get posts that use a specific media file.
|
||||
|
||||
Available UI Render Tools (use these to show rich interactive elements):
|
||||
- render_chart: Show data as a bar, line, or pie chart. Use when presenting statistics or comparisons.
|
||||
- render_chart: Show data as a bar, stacked-bar, line, or pie chart. Use when presenting statistics or comparisons. Use stacked-bar when each bar has multiple segments (e.g., published vs draft posts per year).
|
||||
- render_table: Show data in a structured table. Use for tabular comparisons and listings.
|
||||
- render_form: Show an interactive form to collect user input (e.g., metadata edits, settings).
|
||||
- render_card: Show an information card with title, body, and action buttons.
|
||||
|
||||
@@ -924,7 +924,7 @@ export class OpenCodeManager {
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
chartType: { type: 'string', enum: ['bar', 'line', 'pie'], description: 'The type of chart to render' },
|
||||
chartType: { type: 'string', enum: ['bar', 'stacked-bar', 'line', 'pie'], description: 'The type of chart to render. Use stacked-bar when each bar has multiple segments (categories).' },
|
||||
title: { type: 'string', description: 'Optional chart title' },
|
||||
series: {
|
||||
type: 'array',
|
||||
@@ -932,11 +932,23 @@ export class OpenCodeManager {
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: { type: 'string', description: 'Data point label' },
|
||||
value: { type: 'number', description: 'Data point value' },
|
||||
value: { type: 'number', description: 'Data point value (total for stacked bars)' },
|
||||
segments: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: { type: 'string', description: 'Segment category label' },
|
||||
value: { type: 'number', description: 'Segment value' },
|
||||
},
|
||||
required: ['label', 'value'],
|
||||
},
|
||||
description: 'Segments for stacked-bar charts. Each segment is a category within the bar.',
|
||||
},
|
||||
},
|
||||
required: ['label', 'value'],
|
||||
},
|
||||
description: 'Array of data points with label and value',
|
||||
description: 'Array of data points with label and value. For stacked-bar charts, include segments.',
|
||||
},
|
||||
},
|
||||
required: ['chartType', 'series'],
|
||||
|
||||
@@ -9,28 +9,117 @@ interface A2UIComponentProps {
|
||||
renderChildren?: (children: A2UIResolvedComponent[]) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface SegmentEntry {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface SeriesEntry {
|
||||
label: string;
|
||||
value: number;
|
||||
segments?: SegmentEntry[];
|
||||
}
|
||||
|
||||
const SEGMENT_COLORS = [
|
||||
'var(--vscode-charts-blue, #75beff)',
|
||||
'var(--vscode-charts-green, #89d185)',
|
||||
'var(--vscode-charts-orange, #d18616)',
|
||||
'var(--vscode-charts-red, #f14c4c)',
|
||||
'var(--vscode-charts-purple, #b180d7)',
|
||||
'var(--vscode-charts-yellow, #e2e210)',
|
||||
];
|
||||
|
||||
function getSegmentColor(index: number): string {
|
||||
return SEGMENT_COLORS[index % SEGMENT_COLORS.length];
|
||||
}
|
||||
|
||||
/** Collect unique segment labels across all series entries, preserving order. */
|
||||
function collectSegmentLabels(series: SeriesEntry[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const labels: string[] = [];
|
||||
for (const entry of series) {
|
||||
if (entry.segments) {
|
||||
for (const seg of entry.segments) {
|
||||
if (!seen.has(seg.label)) {
|
||||
seen.add(seg.label);
|
||||
labels.push(seg.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
export const A2UIChart: React.FC<A2UIComponentProps> = ({ component }) => {
|
||||
const chartType = String(component.properties.chartType ?? 'bar');
|
||||
const title = component.properties.title as string | undefined;
|
||||
const series = (component.boundValue as SeriesEntry[]) ?? (component.properties.series as SeriesEntry[]) ?? [];
|
||||
const maxValue = Math.max(...series.map((entry) => entry.value), 0);
|
||||
const isStacked = chartType === 'stacked-bar';
|
||||
|
||||
const maxValue = Math.max(
|
||||
...series.map((entry) => {
|
||||
if (isStacked && entry.segments) {
|
||||
return entry.segments.reduce((sum, s) => sum + s.value, 0);
|
||||
}
|
||||
return entry.value;
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
const segmentLabels = isStacked ? collectSegmentLabels(series) : [];
|
||||
|
||||
return (
|
||||
<div className="assistant-panel-chart">
|
||||
{title && <p className="assistant-panel-chart-title">{title}</p>}
|
||||
<div className="assistant-panel-chart-type">{chartType}</div>
|
||||
{series.map((entry, index) => (
|
||||
<div key={`${component.id}-series-${index}`} className="assistant-panel-chart-item">
|
||||
<span>{entry.label}</span>
|
||||
<progress value={entry.value} max={maxValue || 1} />
|
||||
<span>{entry.value}</span>
|
||||
{series.map((entry, index) => {
|
||||
const totalValue = isStacked && entry.segments
|
||||
? entry.segments.reduce((sum, s) => sum + s.value, 0)
|
||||
: entry.value;
|
||||
|
||||
return (
|
||||
<div key={`${component.id}-series-${index}`} className="assistant-panel-chart-item">
|
||||
<span className="assistant-panel-chart-label">{entry.label}</span>
|
||||
<div className="assistant-panel-chart-bar-track">
|
||||
{isStacked && entry.segments ? (
|
||||
entry.segments.map((seg, si) => {
|
||||
const segWidth = maxValue > 0 ? (seg.value / maxValue) * 100 : 0;
|
||||
return (
|
||||
<div
|
||||
key={`${component.id}-seg-${index}-${si}`}
|
||||
className="assistant-panel-chart-bar-segment"
|
||||
style={{
|
||||
width: `${segWidth}%`,
|
||||
backgroundColor: getSegmentColor(segmentLabels.indexOf(seg.label)),
|
||||
}}
|
||||
title={`${seg.label}: ${seg.value}`}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div
|
||||
className="assistant-panel-chart-bar-fill"
|
||||
style={{ width: `${maxValue > 0 ? (entry.value / maxValue) * 100 : 0}%` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="assistant-panel-chart-value">{totalValue}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{isStacked && segmentLabels.length > 0 && (
|
||||
<div className="assistant-panel-chart-legend">
|
||||
{segmentLabels.map((label, i) => (
|
||||
<span key={label} className="assistant-panel-chart-legend-item">
|
||||
<span
|
||||
className="assistant-panel-chart-legend-swatch"
|
||||
style={{ backgroundColor: getSegmentColor(i) }}
|
||||
/>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-metric {
|
||||
.assistant-panel-metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
@@ -83,22 +83,22 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-metric-label {
|
||||
.assistant-panel-metric-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.assistant-sidebar-metric-value {
|
||||
.assistant-panel-metric-value {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-table {
|
||||
.assistant-panel-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.assistant-sidebar-table th,
|
||||
.assistant-sidebar-table td {
|
||||
.assistant-panel-table th,
|
||||
.assistant-panel-table td {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
padding: 6px;
|
||||
font-size: 12px;
|
||||
@@ -112,30 +112,30 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.assistant-sidebar-widget-block {
|
||||
.assistant-panel-widget-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-widget-label {
|
||||
.assistant-panel-widget-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.assistant-sidebar-widget-input {
|
||||
.assistant-panel-widget-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-checkbox {
|
||||
.assistant-panel-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart {
|
||||
.assistant-panel-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@@ -144,30 +144,99 @@
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart-title {
|
||||
.assistant-panel-chart-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart-type {
|
||||
.assistant-panel-chart-type {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart-item {
|
||||
.assistant-panel-chart-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(48px, auto) 1fr auto;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-chart-item progress {
|
||||
width: 100%;
|
||||
.assistant-panel-chart-label {
|
||||
justify-self: end;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 140px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.assistant-sidebar-form {
|
||||
.assistant-panel-chart-bar-track {
|
||||
height: 14px;
|
||||
background: var(--vscode-input-background);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.assistant-panel-chart-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--vscode-charts-blue, #75beff);
|
||||
border-radius: 3px;
|
||||
min-width: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.assistant-panel-chart-bar-segment {
|
||||
height: 100%;
|
||||
min-width: 1px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.assistant-panel-chart-bar-segment:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
.assistant-panel-chart-bar-segment:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.assistant-panel-chart-bar-segment:only-child {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.assistant-panel-chart-value {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.assistant-panel-chart-legend {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 11px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.assistant-panel-chart-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.assistant-panel-chart-legend-swatch {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.assistant-panel-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -176,12 +245,12 @@
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-form-title {
|
||||
.assistant-panel-form-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-sidebar-card {
|
||||
.assistant-panel-card {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
@@ -190,57 +259,57 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-card h4,
|
||||
.assistant-sidebar-card p {
|
||||
.assistant-panel-card h4,
|
||||
.assistant-panel-card p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.assistant-sidebar-card-subtitle {
|
||||
.assistant-panel-card-subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.assistant-sidebar-card-actions {
|
||||
.assistant-panel-card-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assistant-sidebar-image {
|
||||
.assistant-panel-image {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-image img {
|
||||
.assistant-panel-image img {
|
||||
max-width: 100%;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.assistant-sidebar-image figcaption {
|
||||
.assistant-panel-image figcaption {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.assistant-sidebar-tabs {
|
||||
.assistant-panel-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar-tab-strip {
|
||||
.assistant-panel-tab-strip {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assistant-sidebar-tab-button.active {
|
||||
.assistant-panel-tab-button.active {
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.assistant-sidebar-tab-panel {
|
||||
.assistant-panel-tab-panel {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
|
||||
Reference in New Issue
Block a user