wip: more rework and docs

This commit is contained in:
2026-02-26 11:01:17 +01:00
parent affd62ca79
commit 00a9d22a36
18 changed files with 149 additions and 226 deletions

33
API.md
View File

@@ -1,6 +1,6 @@
# API Documentation
Contract version: 1.5.0
Contract version: 1.6.0
This reference documents all Python runtime API calls available through `bds_api` in embedded Pyodide.
@@ -4050,37 +4050,6 @@ Stored API key state for chat provider.
[↑ Back to Table of contents](#table-of-contents)
### ProtocolNeedsInputField
A required clarification input field used for needsInput prompts.
**Fields**
- key (`string`, required): Stable field key used in submitted values.
- label (`string`, required): User-facing field label.
- inputType (`'text' | 'textarea' | 'select' | 'checkbox' | 'date' | 'number'`, required): Rendered input control type.
- required (`boolean`, optional): Whether user input is required.
- options (`Array<{ label: string; value: string }>`, optional): Selectable options for select controls.
- placeholder (`string`, optional): Optional placeholder text for text-like controls.
- defaultValue (`string | number | boolean`, optional): Default field value shown in UI.
[↑ Back to Table of contents](#table-of-contents)
### ProtocolAction
A declarative assistant action exposed to the UI runtime.
**Fields**
- id (`string`, required): Stable action id within a response envelope.
- action (`string`, required): Action name to dispatch in renderer.
- label (`string`, optional): Optional user-facing action label.
- payload (`Record<string, unknown>`, optional): Optional action payload arguments.
- policy (`'silent' | 'confirm' | 'danger'`, required): Action confirmation policy level.
- requiresConfirmation (`boolean`, required): Whether confirmation is required before dispatch.
[↑ Back to Table of contents](#table-of-contents)
---
Generated from contract at 2026-02-25T00:00:00.000Z.

View File

@@ -11,7 +11,7 @@
- [Working with media](#working-with-media)
- [Using macros](#using-macros)
- [Using scripting (early access)](#using-scripting-early-access)
- [Using assistant panel widgets](#using-assistant-panel-widgets)
- [Using the AI assistant](#using-the-ai-assistant)
- [Organizing with tags](#organizing-with-tags)
- [Importing from WordPress (WXR)](#importing-from-wordpress-wxr)
- [Using Git (Source Control)](#using-git-source-control)
@@ -256,160 +256,60 @@ Notes:
---
## Using assistant panel widgets
## Using the AI assistant
The assistant sidebar can render structured panel widgets when the AI response includes a valid JSON panel spec. This is useful when you want the assistant to return actionable UI instead of plain text only.
The AI assistant is built into bDS to help you manage your blog through natural conversation. You can ask it to search posts, analyze your content, update metadata, and visualize data. Instead of returning only plain text, the assistant can present results as rich interactive elements such as charts, tables, forms, and more.
Use this envelope:
The assistant works entirely with your local blog content. It does not have access to the internet or external services. When you ask a question, it uses your posts, media, tags, and categories to find answers and present them in the most useful format. In most cases the assistant automatically picks the right visualization for your request, but you can also ask for a specific format explicitly.
```json
{
"specVersion": "1",
"elements": []
}
```
### Charts
### Supported widget types
The assistant can display bar, line, and pie charts to help you spot patterns and trends in your blog data. Charts include a title, labeled data points, and a visual representation that makes it easy to compare values at a glance.
- `text`
- `metric`
- `list`
- `table`
- `action`
- `chart`
- `input`
- `form`
- `datePicker`
- `card`
- `image`
- `tabs`
**Try asking:** "Show me a chart of posts published per month this year"
### Example snippets
### Tables
```json
{ "type": "text", "text": "Review complete." }
```
When you need to compare posts side by side or see structured information, the assistant can render a table with columns and rows. Tables are useful for listings, comparisons, and any data that benefits from a grid layout.
```json
{ "type": "metric", "label": "Draft posts", "value": "12" }
```
**Try asking:** "Compare my last 10 posts showing title, status, and word count"
```json
{ "type": "list", "title": "Next steps", "items": ["Refine title", "Add tags"] }
```
### Cards
```json
{
"type": "table",
"columns": ["Post", "Status"],
"rows": [["Roadmap", "Draft"], ["Release", "Published"]]
}
```
Cards present a focused summary with a title, body text, and optional action buttons. The assistant uses cards when highlighting a specific item, making a recommendation, or presenting a result that you might want to act on.
```json
{
"type": "action",
"label": "Open tags",
"action": "switchView",
"payload": { "view": "tags" }
}
```
**Try asking:** "Give me a summary card for my most recent draft post"
```json
{
"type": "chart",
"chartType": "bar",
"title": "Posts by month",
"series": [
{ "label": "Jan", "value": 10 },
{ "label": "Feb", "value": 14 }
]
}
```
### Metrics
```json
{
"type": "input",
"key": "query",
"label": "Search",
"inputType": "text",
"placeholder": "Find post...",
"submitLabel": "Run",
"action": "openChat"
}
```
A metric is a single prominent number or value with a label. The assistant uses metrics when the answer to your question is one key figure, such as a count, a status, or a statistic.
```json
{
"type": "form",
"formId": "meta-form",
"title": "Update metadata",
"submitLabel": "Apply",
"action": "openSettings",
"fields": [
{ "key": "title", "label": "Title", "inputType": "text" },
{ "key": "isDraft", "label": "Draft", "inputType": "checkbox" }
]
}
```
**Try asking:** "How many draft posts do I have?"
```json
{
"type": "datePicker",
"key": "publishDate",
"label": "Publish date",
"submitLabel": "Set",
"action": "openSettings"
}
```
### Lists
```json
{
"type": "card",
"title": "Suggestion",
"subtitle": "Editorial",
"body": "Add one category and two tags.",
"actions": [
{ "label": "Open tags", "action": "switchView", "payload": { "view": "tags" } }
]
}
```
Lists display items as a simple bulleted enumeration. They work well for tag listings, next steps, checklists, and any result that is naturally a sequence of items.
```json
{
"type": "image",
"src": "https://example.com/preview.png",
"alt": "Generated preview",
"caption": "Current preview snapshot"
}
```
**Try asking:** "List all tags that are used by fewer than 3 posts"
```json
{
"type": "tabs",
"defaultTabId": "summary",
"tabs": [
{
"id": "summary",
"label": "Summary",
"elements": [{ "type": "text", "text": "Short summary" }]
},
{
"id": "actions",
"label": "Actions",
"elements": [
{ "type": "action", "label": "Open settings", "action": "openSettings" }
]
}
]
}
```
### Forms
### Notes
When the assistant needs structured input from you, it can display an interactive form with text fields, checkboxes, dropdowns, and date pickers. Forms are typically used for metadata updates, multi-field edits, and configuration tasks where typing everything into a single message would be awkward.
- `tabs` are panel-local UI tabs inside one assistant response; they are not editor tabs.
- Unknown or invalid widget payloads are ignored by the parser.
- Actions are restricted to supported safe action names in the app.
**Try asking:** "Help me update the metadata for my post about React"
### Tabs
Tabs let the assistant organize multiple views into a single switchable interface. Each tab can contain any combination of text, charts, tables, metrics, and lists. Tabs are especially useful for multi-dimensional comparisons where you want to explore different slices of data without scrolling through a long response.
**Try asking:** "Show post statistics by year, with each year as a tab containing a chart of monthly post counts"
### Key takeaways
- The assistant picks the right visualization automatically based on your question.
- You can ask for a specific format by mentioning it in your prompt ("show as a chart", "put it in a table").
- Tabs can contain charts, tables, and other elements for rich multi-view displays.
- The assistant can only access your local blog content.
[↑ Back to In this article](#in-this-article)

View File

@@ -328,7 +328,7 @@ Available UI Render Tools (use these to show rich interactive elements):
- render_card: Show an information card with title, body, and action buttons.
- render_metric: Show a single KPI or statistic prominently.
- render_list: Show a bulleted list of items.
- render_tabs: Organize information into switchable tabs.
- render_tabs: Organize information into switchable tabs. Tab content supports all content types: text, metrics, lists, charts, and tables.
When answering questions:
1. USE THE TOOLS to find information. Never make up data about posts or media.
@@ -338,7 +338,8 @@ When answering questions:
5. When asked to describe or analyze an image, use the view_image tool to see the actual image content.
6. When presenting data, statistics, or comparisons, prefer using render tools (render_chart, render_table, render_metric) to show rich interactive UI instead of plain text.
7. When you need user input for a multi-field operation, use render_form to present a structured form.
8. Use render_card with action buttons when presenting items the user might want to navigate to (e.g., posts, media).`;
8. Use render_card with action buttons when presenting items the user might want to navigate to (e.g., posts, media).
9. When comparing data across multiple dimensions (e.g., statistics per year), use render_tabs with embedded charts or tables in each tab.`;
}
/**

View File

@@ -1018,7 +1018,7 @@ export class OpenCodeManager {
},
{
name: 'render_tabs',
description: 'Render a tabbed interface in the chat UI. Use this when you want to organize information into multiple tabs that the user can switch between.',
description: 'Render a tabbed interface in the chat UI. Use this when you want to organize information into multiple tabs that the user can switch between. Each tab can contain any combination of text, metrics, lists, charts, and tables.',
input_schema: {
type: 'object',
properties: {
@@ -1033,7 +1033,28 @@ export class OpenCodeManager {
items: {
type: 'object',
properties: {
type: { type: 'string', enum: ['text', 'metric', 'list'], description: 'Content type' },
type: { type: 'string', enum: ['text', 'metric', 'list', 'chart', 'table'], description: 'Content type' },
text: { type: 'string', description: 'Text content (for type text)' },
label: { type: 'string', description: 'Label (for type metric)' },
value: { type: 'string', description: 'Display value (for type metric)' },
title: { type: 'string', description: 'Title (for type list, chart, or table)' },
items: { type: 'array', items: { type: 'string' }, description: 'Items (for type list)' },
chartType: { type: 'string', enum: ['bar', 'line', 'pie'], description: 'Chart type (for type chart)' },
series: {
type: 'array',
items: {
type: 'object',
properties: { label: { type: 'string' }, value: { type: 'number' } },
required: ['label', 'value'],
},
description: 'Data series (for type chart)',
},
columns: { type: 'array', items: { type: 'string' }, description: 'Column headers (for type table)' },
rows: {
type: 'array',
items: { type: 'array', items: { type: 'string' } },
description: 'Table rows (for type table)',
},
},
required: ['type'],
},

View File

@@ -17,7 +17,7 @@ interface SeriesEntry {
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[]) ?? [];
const series = (component.boundValue as SeriesEntry[]) ?? (component.properties.series as SeriesEntry[]) ?? [];
const maxValue = Math.max(...series.map((entry) => entry.value), 0);
return (

View File

@@ -11,7 +11,7 @@ interface A2UIComponentProps {
export const A2UIList: React.FC<A2UIComponentProps> = ({ component }) => {
const title = component.properties.title as string | undefined;
const items = (component.boundValue as string[]) ?? [];
const items = (component.boundValue as string[]) ?? (component.properties.items as string[]) ?? [];
return (
<div>

View File

@@ -11,7 +11,7 @@ interface A2UIComponentProps {
export const A2UITable: React.FC<A2UIComponentProps> = ({ component }) => {
const columns = (component.properties.columns as string[]) ?? [];
const rows = (component.boundValue as string[][]) ?? [];
const rows = (component.boundValue as string[][]) ?? (component.properties.rows as string[][]) ?? [];
const title = component.properties.title as string | undefined;
return (

View File

@@ -250,7 +250,7 @@ export const AssistantSidebar: React.FC = () => {
{actionError && <p className="assistant-sidebar-error chat-surface-error">{actionError}</p>}
{surfaceMode.showWelcomeTips && messages.length === 0 && !isStreaming && (
{surfaceMode.showWelcomeTips && messages.filter(m => m.role !== 'system' && m.role !== 'tool').length === 0 && !isStreaming && (
<div className="assistant-sidebar-raw-message chat-surface-section">
{tr('chat.welcomeDescription')}
</div>

View File

@@ -98,6 +98,8 @@
justify-content: center;
text-align: center;
padding: 32px;
min-height: 100%;
box-sizing: border-box;
color: var(--vscode-descriptionForeground);
}

View File

@@ -344,17 +344,17 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
</div>
<div className="chat-messages chat-surface-scroll">
{surfaceMode.showWelcomeTips && messages.length === 0 && !isStreaming && (
{surfaceMode.showWelcomeTips && messages.filter(m => m.role !== 'system' && m.role !== 'tool').length === 0 && !isStreaming && (
<div className="chat-welcome">
<div className="chat-welcome-icon">{'\u{1F916}'}</div>
<h2>{tr('chat.welcomeTitle')}</h2>
<p>{tr('chat.welcomeDescription')}</p>
<ul>
<li>{tr('chat.welcomeTipSearch')}</li>
<li>{tr('chat.welcomeTipDetails')}</li>
<li>{tr('chat.welcomeTipTags')}</li>
<li>{tr('chat.welcomeTipChart')}</li>
<li>{tr('chat.welcomeTipTable')}</li>
<li>{tr('chat.welcomeTipMetadata')}</li>
<li>{tr('chat.welcomeTipImages')}</li>
<li>{tr('chat.welcomeTipTabs')}</li>
</ul>
</div>
)}

View File

@@ -196,12 +196,12 @@
"chat.apiKeyValidationFailed": "API-Schlüssel konnte nicht validiert werden.",
"chat.newChat": "Neuer Chat",
"chat.welcomeTitle": "Willkommen beim KI-Assistenten",
"chat.welcomeDescription": "Ich kann dir helfen, deine Beiträge und Medien zu verwalten. Frag mich zum Beispiel:",
"chat.welcomeDescription": "Ich kann dir helfen, deinen Blog mit anschaulichen Darstellungen zu verwalten. Frag mich zum Beispiel:",
"chat.welcomeTipSearch": "Nach Beiträgen zu einem bestimmten Thema suchen",
"chat.welcomeTipDetails": "Details zu einem bestimmten Beitrag anzeigen",
"chat.welcomeTipTags": "Alle Tags oder Kategorien in deinem Blog auflisten",
"chat.welcomeTipChart": "Ein Diagramm der pro Monat veröffentlichten Beiträge anzeigen",
"chat.welcomeTipTable": "Meine letzten Beiträge in einer Tabelle vergleichen",
"chat.welcomeTipMetadata": "Metadaten für Beiträge oder Medien aktualisieren",
"chat.welcomeTipImages": "Alle Bilder in deiner Mediathek auflisten",
"chat.welcomeTipTabs": "Beitragsstatistiken nach Jahr in Tabs mit Diagrammen anzeigen",
"chat.role.you": "Du",
"chat.role.assistant": "Assistent",
"chat.stop": "Stopp",

View File

@@ -196,12 +196,12 @@
"chat.apiKeyValidationFailed": "Failed to validate API key.",
"chat.newChat": "New Chat",
"chat.welcomeTitle": "Welcome to the AI Assistant",
"chat.welcomeDescription": "I can help you manage your posts and media. Try asking me to:",
"chat.welcomeDescription": "I can help you manage your blog with rich visualizations. Try asking me to:",
"chat.welcomeTipSearch": "Search for posts about a specific topic",
"chat.welcomeTipDetails": "Get details about a specific post",
"chat.welcomeTipTags": "List all tags or categories in your blog",
"chat.welcomeTipChart": "Show a chart of posts published per month",
"chat.welcomeTipTable": "Compare my recent posts in a table",
"chat.welcomeTipMetadata": "Update metadata for posts or media",
"chat.welcomeTipImages": "List all images in your media library",
"chat.welcomeTipTabs": "Show post statistics by year in tabs with charts",
"chat.role.you": "You",
"chat.role.assistant": "Assistant",
"chat.stop": "Stop",

View File

@@ -196,12 +196,12 @@
"chat.apiKeyValidationFailed": "No se pudo validar la clave API.",
"chat.newChat": "Nuevo chat",
"chat.welcomeTitle": "Bienvenido al asistente de IA",
"chat.welcomeDescription": "Puedo ayudarte a gestionar tus entradas y medios. Prueba a pedirme que:",
"chat.welcomeDescription": "Puedo ayudarte a gestionar tu blog con visualizaciones interactivas. Prueba a pedirme que:",
"chat.welcomeTipSearch": "Busque entradas sobre un tema específico",
"chat.welcomeTipDetails": "Muestre detalles de una entrada específica",
"chat.welcomeTipTags": "Lista todas las etiquetas o categorías de tu blog",
"chat.welcomeTipChart": "Muestre un gráfico de entradas publicadas por mes",
"chat.welcomeTipTable": "Compare mis entradas recientes en una tabla",
"chat.welcomeTipMetadata": "Actualice metadatos de entradas o medios",
"chat.welcomeTipImages": "Liste todas las imágenes de tu biblioteca de medios",
"chat.welcomeTipTabs": "Muestre estadísticas por año en pestañas con gráficos",
"chat.role.you": "Tú",
"chat.role.assistant": "Asistente",
"chat.stop": "Detener",

View File

@@ -196,12 +196,12 @@
"chat.apiKeyValidationFailed": "Impossible de valider la clé API.",
"chat.newChat": "Nouveau chat",
"chat.welcomeTitle": "Bienvenue dans lassistant IA",
"chat.welcomeDescription": "Je peux vous aider à gérer vos articles et médias. Essayez par exemple :",
"chat.welcomeDescription": "Je peux vous aider à gérer votre blog avec des visualisations riches. Essayez par exemple :",
"chat.welcomeTipSearch": "Rechercher des articles sur un sujet précis",
"chat.welcomeTipDetails": "Afficher les détails dun article précis",
"chat.welcomeTipTags": "Lister toutes les étiquettes ou catégories de votre blog",
"chat.welcomeTipChart": "Afficher un graphique des articles publiés par mois",
"chat.welcomeTipTable": "Comparer mes derniers articles dans un tableau",
"chat.welcomeTipMetadata": "Mettre à jour les métadonnées des articles ou médias",
"chat.welcomeTipImages": "Lister toutes les images de votre bibliothèque média",
"chat.welcomeTipTabs": "Afficher les statistiques par année dans des onglets avec graphiques",
"chat.role.you": "Vous",
"chat.role.assistant": "Assistant IA",
"chat.stop": "Arrêter",

View File

@@ -196,12 +196,12 @@
"chat.apiKeyValidationFailed": "Impossibile convalidare la chiave API.",
"chat.newChat": "Nuova chat",
"chat.welcomeTitle": "Benvenuto nellassistente IA",
"chat.welcomeDescription": "Posso aiutarti a gestire post e media. Prova a chiedermi di:",
"chat.welcomeDescription": "Posso aiutarti a gestire il tuo blog con visualizzazioni interattive. Prova a chiedermi di:",
"chat.welcomeTipSearch": "Cercare post su un argomento specifico",
"chat.welcomeTipDetails": "Ottieni dettagli su un post specifico",
"chat.welcomeTipTags": "Elenca tutti i tag o le categorie del tuo blog",
"chat.welcomeTipChart": "Mostrare un grafico dei post pubblicati per mese",
"chat.welcomeTipTable": "Confrontare i miei post recenti in una tabella",
"chat.welcomeTipMetadata": "Aggiornare i metadati di post o media",
"chat.welcomeTipImages": "Elenca tutte le immagini nella tua libreria media",
"chat.welcomeTipTabs": "Mostrare statistiche per anno in schede con grafici",
"chat.role.you": "Tu",
"chat.role.assistant": "Assistente",
"chat.stop": "Ferma",

View File

@@ -359,35 +359,10 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
{ name: 'maskedKey', type: 'string', required: true, description: 'Masked key representation for UI display.' },
],
},
{
name: 'ProtocolNeedsInputField',
description: 'A required clarification input field used for needsInput prompts.',
fields: [
{ name: 'key', type: 'string', required: true, description: 'Stable field key used in submitted values.' },
{ name: 'label', type: 'string', required: true, description: 'User-facing field label.' },
{ name: 'inputType', type: "'text' | 'textarea' | 'select' | 'checkbox' | 'date' | 'number'", required: true, description: 'Rendered input control type.' },
{ name: 'required', type: 'boolean', required: false, description: 'Whether user input is required.' },
{ name: 'options', type: 'Array<{ label: string; value: string }>', required: false, description: 'Selectable options for select controls.' },
{ name: 'placeholder', type: 'string', required: false, description: 'Optional placeholder text for text-like controls.' },
{ name: 'defaultValue', type: 'string | number | boolean', required: false, description: 'Default field value shown in UI.' },
],
},
{
name: 'ProtocolAction',
description: 'A declarative assistant action exposed to the UI runtime.',
fields: [
{ name: 'id', type: 'string', required: true, description: 'Stable action id within a response envelope.' },
{ name: 'action', type: 'string', required: true, description: 'Action name to dispatch in renderer.' },
{ name: 'label', type: 'string', required: false, description: 'Optional user-facing action label.' },
{ name: 'payload', type: 'Record<string, unknown>', required: false, description: 'Optional action payload arguments.' },
{ name: 'policy', type: "'silent' | 'confirm' | 'danger'", required: true, description: 'Action confirmation policy level.' },
{ name: 'requiresConfirmation', type: 'boolean', required: true, description: 'Whether confirmation is required before dispatch.' },
],
},
];
export const BDS_PYTHON_API_CONTRACT_V1: PythonApiContractV1 = {
version: '1.5.0',
version: '1.6.0',
generatedAt: '2026-02-25T00:00:00.000Z',
methods: METHODS_V1,
dataStructures: DATA_STRUCTURES_V1,

View File

@@ -259,5 +259,60 @@ describe('A2UI generator', () => {
expect(tabsComponent!.children).toHaveLength(2);
expect(tabsComponent!.properties.tabLabels).toEqual(['Overview', 'Details']);
});
it('creates chart components inside tabs with series in properties', () => {
const messages = generateTabs('conv-1', {
tabs: [
{
label: 'Stats',
content: [{
type: 'chart',
chartType: 'bar',
title: 'Monthly Posts',
series: [{ label: 'Jan', value: 5 }, { label: 'Feb', value: 8 }],
}],
},
],
});
expect(messages).toHaveLength(2);
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
const chartComponent = updateMsg.components.find((c) => c.type === 'chart');
expect(chartComponent).toBeDefined();
expect(chartComponent!.properties.chartType).toBe('bar');
expect(chartComponent!.properties.title).toBe('Monthly Posts');
expect(chartComponent!.properties.series).toEqual([
{ label: 'Jan', value: 5 },
{ label: 'Feb', value: 8 },
]);
});
it('creates table components inside tabs with rows in properties', () => {
const messages = generateTabs('conv-1', {
tabs: [
{
label: 'Data',
content: [{
type: 'table',
title: 'Recent Posts',
columns: ['Title', 'Status'],
rows: [['Hello', 'published'], ['Draft', 'draft']],
}],
},
],
});
expect(messages).toHaveLength(2);
const updateMsg = messages[1] as Extract<A2UIServerMessage, { type: 'updateComponents' }>;
const tableComponent = updateMsg.components.find((c) => c.type === 'table');
expect(tableComponent).toBeDefined();
expect(tableComponent!.properties.columns).toEqual(['Title', 'Status']);
expect(tableComponent!.properties.rows).toEqual([
['Hello', 'published'],
['Draft', 'draft'],
]);
});
});
});

View File

@@ -70,7 +70,7 @@ describe('pythonApiContractV1', () => {
it('contains semantic version metadata for compatibility checks', () => {
expect(BDS_PYTHON_API_CONTRACT_V1).toMatchObject({
version: '1.5.0',
version: '1.6.0',
generatedAt: expect.any(String),
});
});