@meistrari/chat-ui-lang 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,18 @@
1
- # Temporary Holding Version
1
+ # @meistrari/chat-ui-lang
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The catalogue of widgets the chat agent may emit as OpenUI Lang, shared by
4
+ chat-nuxt (which renders them) and agent-api (which puts `prompt()` in the
5
+ system prompt). No Vue here: only names, descriptions, Zod props and the
6
+ prompt generator.
7
+
8
+ Adding a widget is a two-part change in this monorepo: define it here and add
9
+ its Vue renderer to `chat-nuxt/src/runtime/rendering/openui/registry.ts`. The
10
+ registry test fails until both sides match. The full walkthrough is in
11
+ `packages/chat-nuxt/docs/guides/widgets.md`.
12
+
13
+ Bump `LIBRARY_VERSION` on every catalogue change: agent-api stamps it on each
14
+ turn and chat-nuxt logs it with widget errors, which is how drift between the
15
+ prompt and the renderer shows up in production.
16
+
17
+ `@openuidev/lang-core` ships a postinstall that sends installation telemetry;
18
+ the root `pnpm.neverBuiltDependencies` entry disables it.
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
- "name": "@meistrari/chat-ui-lang",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@meistrari/chat-ui-lang",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "test": "bun test",
16
+ "typecheck": "tsc --noEmit",
17
+ "measure": "bun scripts/measure.ts"
18
+ },
19
+ "dependencies": {
20
+ "@openuidev/lang-core": "1.0.0",
21
+ "zod": "^4.1.13"
22
+ },
23
+ "devDependencies": {
24
+ "@anthropic-ai/sdk": "0.133.0",
25
+ "typescript": "5.9.3"
26
+ },
27
+ "description": "Catalogue of chat widgets the Tela agent may emit as OpenUI Lang, shared by chat-nuxt and agent-api",
28
+ "license": "UNLICENSED",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/meistrari/chat-app.git",
32
+ "directory": "packages/ui-lang"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
package/src/actions.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineAction } from '@openuidev/lang-core'
2
+ import { z } from 'zod'
3
+
4
+ export const CopyText = defineAction({
5
+ name: 'CopyText',
6
+ description: 'Copia um texto para a área de transferência do usuário.',
7
+ params: z.object({
8
+ text: z.string(),
9
+ }),
10
+ })
11
+
12
+ export const actions = { CopyText }
@@ -0,0 +1,21 @@
1
+ import { action } from '@openuidev/lang-core'
2
+ import { z } from 'zod'
3
+ import { defineUiComponent } from '../define'
4
+
5
+ export const Button = defineUiComponent({
6
+ name: 'Button',
7
+ description: 'Botão de ação. Com @ToAssistant envia uma mensagem como se o usuário tivesse digitado.',
8
+ props: z.object({
9
+ label: z.string(),
10
+ action: action().optional(),
11
+ variant: z.enum(['primary', 'secondary']).optional().describe('Padrão secondary; no máximo um primary por grupo'),
12
+ }),
13
+ })
14
+
15
+ export const Buttons = defineUiComponent({
16
+ name: 'Buttons',
17
+ description: 'Grupo horizontal de botões.',
18
+ props: z.object({
19
+ buttons: z.array(Button.ref).min(1).max(4),
20
+ }),
21
+ })
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod'
2
+ import { defineUiComponent } from '../define'
3
+
4
+ export const Bar = defineUiComponent({
5
+ name: 'Bar',
6
+ description: 'Uma barra do gráfico.',
7
+ props: z.object({
8
+ label: z.string(),
9
+ value: z.number().describe('Valor numérico usado para o tamanho da barra'),
10
+ display: z.string().optional().describe('Valor formatado para exibir, ex: "R$ 1,2 mi"'),
11
+ }),
12
+ })
13
+
14
+ export const BarChart = defineUiComponent({
15
+ name: 'BarChart',
16
+ description: 'Gráfico de barras horizontais para comparar até 8 valores da mesma unidade.',
17
+ props: z.object({
18
+ title: z.string(),
19
+ bars: z.array(Bar.ref).min(2).max(8),
20
+ tone: z.enum(['neutral', 'positive', 'negative']).optional(),
21
+ }),
22
+ })
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod'
2
+ import { defineUiComponent } from '../define'
3
+
4
+ export const Option = defineUiComponent({
5
+ name: 'Option',
6
+ description: 'Uma alternativa de resposta.',
7
+ props: z.object({
8
+ id: z.string().describe('Identificador curto e único dentro da pergunta'),
9
+ label: z.string(),
10
+ description: z.string().optional().describe('Uma linha explicando a alternativa'),
11
+ }),
12
+ })
13
+
14
+ export const Question = defineUiComponent({
15
+ name: 'Question',
16
+ description: 'Pergunta de múltipla escolha para o usuário. Entre 2 e 4 alternativas.',
17
+ props: z.object({
18
+ id: z.string(),
19
+ title: z.string(),
20
+ options: z.array(Option.ref).min(2).max(4),
21
+ multiple: z.boolean().optional().describe('true permite marcar mais de uma alternativa'),
22
+ }),
23
+ })
24
+
25
+ export const Questions = defineUiComponent({
26
+ name: 'Questions',
27
+ description: 'Bloco de perguntas de esclarecimento. As respostas voltam como uma mensagem do usuário. Use no máximo 3 perguntas e só quando a resposta depender delas.',
28
+ props: z.object({
29
+ requestId: z.string().describe('Identificador curto desta rodada de perguntas, ex: "q-prazos"'),
30
+ questions: z.array(Question.ref).min(1).max(3),
31
+ }),
32
+ })
33
+
34
+ export const FileCard = defineUiComponent({
35
+ name: 'FileCard',
36
+ description: 'Cartão de um documento do vault, com preview e download.',
37
+ props: z.object({
38
+ vaultRef: z.string().describe('Referência vault:// do documento, exatamente como recebida'),
39
+ name: z.string().describe('Nome exibido do arquivo'),
40
+ }),
41
+ })
42
+
43
+ export const Citation = defineUiComponent({
44
+ name: 'Citation',
45
+ description: 'Chip de citação para um trecho de documento. Prefira citações inline no markdown; use este componente dentro de Card ou Stack quando a citação for o item em si.',
46
+ props: z.object({
47
+ ref: z.string().describe('Referência cite:// exatamente como recebida'),
48
+ label: z.string().optional().describe('Texto curto exibido no chip'),
49
+ }),
50
+ })
51
+
52
+ export const Image = defineUiComponent({
53
+ name: 'Image',
54
+ description: 'Imagem com legenda. Aceita referência vault:// de uma imagem gerada ou recebida, ou uma URL https pública.',
55
+ props: z.object({
56
+ src: z.string().describe('vault://... ou https://..., exatamente como recebida'),
57
+ alt: z.string().describe('Descrição curta da imagem'),
58
+ caption: z.string().optional(),
59
+ }),
60
+ })
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod'
2
+ import { defineUiComponent } from '../define'
3
+
4
+ export const Metric = defineUiComponent({
5
+ name: 'Metric',
6
+ description: 'Um número de destaque com rótulo. Use para valores isolados, nunca para listas.',
7
+ props: z.object({
8
+ label: z.string().describe('O que o número representa'),
9
+ value: z.string().describe('Valor já formatado, ex: "R$ 12.400,00"'),
10
+ delta: z.string().optional().describe('Variação, ex: "+12%"'),
11
+ tone: z.enum(['neutral', 'positive', 'negative']).optional(),
12
+ }),
13
+ })
14
+
15
+ export const Col = defineUiComponent({
16
+ name: 'Col',
17
+ description: 'Uma coluna de tabela: rótulo e a lista de valores, um por linha.',
18
+ props: z.object({
19
+ label: z.string(),
20
+ values: z.array(z.union([z.string(), z.number()])).describe('Um valor por linha, na mesma ordem em todas as colunas'),
21
+ align: z.enum(['left', 'right']).optional().describe('right para números'),
22
+ }),
23
+ })
24
+
25
+ export const Table = defineUiComponent({
26
+ name: 'Table',
27
+ description: 'Tabela orientada a colunas. Use para 3 ou mais linhas comparáveis.',
28
+ props: z.object({
29
+ columns: z.array(Col.ref).min(1),
30
+ caption: z.string().optional(),
31
+ }),
32
+ })
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod'
2
+ import { defineUiComponent } from '../define'
3
+
4
+ export const Input = defineUiComponent({
5
+ name: 'Input',
6
+ description: 'Campo de texto livre de uma linha.',
7
+ props: z.object({
8
+ name: z.string().describe('Identificador do campo dentro do formulário'),
9
+ label: z.string(),
10
+ placeholder: z.string().optional(),
11
+ }),
12
+ })
13
+
14
+ export const Select = defineUiComponent({
15
+ name: 'Select',
16
+ description: 'Escolha única entre alternativas curtas.',
17
+ props: z.object({
18
+ name: z.string(),
19
+ label: z.string(),
20
+ options: z.array(z.string()).min(2).max(8),
21
+ }),
22
+ })
23
+
24
+ export const Checkbox = defineUiComponent({
25
+ name: 'Checkbox',
26
+ description: 'Caixa de marcação sim/não.',
27
+ props: z.object({
28
+ name: z.string(),
29
+ label: z.string(),
30
+ }),
31
+ })
32
+
33
+ export const Form = defineUiComponent({
34
+ name: 'Form',
35
+ description: 'Formulário curto para coletar dados que faltam. Ao enviar, os valores voltam como uma mensagem do usuário.',
36
+ props: z.object({
37
+ name: z.string().describe('Identificador do formulário, ex: "dados-prazo"'),
38
+ fields: z.array(z.union([Input.ref, Select.ref, Checkbox.ref])).min(1).max(6),
39
+ submitLabel: z.string().optional().describe('Padrão "Enviar"'),
40
+ }),
41
+ })
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod'
2
+ import { defineUiComponent } from '../define'
3
+ import { Button, Buttons } from './actions'
4
+ import { Bar, BarChart } from './chart'
5
+ import { Citation, FileCard, Image, Option, Question, Questions } from './chat'
6
+ import { Col, Metric, Table } from './data'
7
+ import { Checkbox, Form, Input, Select } from './form'
8
+ import { Callout, Markdown } from './text'
9
+
10
+ const blockRef = z.union([Markdown.ref, Callout.ref, Metric.ref, Table.ref, BarChart.ref, Buttons.ref, Form.ref, Questions.ref, FileCard.ref, Image.ref, Citation.ref])
11
+
12
+ export const Card = defineUiComponent({
13
+ name: 'Card',
14
+ description: 'Cartão com borda que agrupa blocos relacionados. Não aninhe Card em Card.',
15
+ props: z.object({
16
+ children: z.array(blockRef).min(1),
17
+ title: z.string().optional(),
18
+ }),
19
+ })
20
+
21
+ export const Stack = defineUiComponent({
22
+ name: 'Stack',
23
+ description: 'Raiz do widget. Empilha blocos em coluna ou os distribui em linha.',
24
+ props: z.object({
25
+ children: z.array(z.union([Card.ref, ...blockRef.options])).min(1),
26
+ direction: z.enum(['column', 'row']).optional().describe('row só para Metric ou Card lado a lado'),
27
+ }),
28
+ })
29
+
30
+ export const components = { Stack, Card, Markdown, Callout, Metric, Col, Table, Bar, BarChart, Button, Buttons, Input, Select, Checkbox, Form, Option, Question, Questions, FileCard, Image, Citation }
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod'
2
+ import { defineUiComponent } from '../define'
3
+
4
+ export const Markdown = defineUiComponent({
5
+ name: 'Markdown',
6
+ description: 'Texto em markdown. Use para qualquer prose dentro de um widget; suporta citações cite:// e links vault://.',
7
+ props: z.object({
8
+ text: z.string().describe('Conteúdo em markdown'),
9
+ }),
10
+ })
11
+
12
+ export const Callout = defineUiComponent({
13
+ name: 'Callout',
14
+ description: 'Aviso destacado com tom. Use para alertas, prazos, ressalvas ou conclusões importantes.',
15
+ props: z.object({
16
+ text: z.string().describe('Conteúdo em markdown'),
17
+ tone: z.enum(['info', 'success', 'warning', 'error']).optional().describe('Tom visual, padrão info'),
18
+ title: z.string().optional(),
19
+ }),
20
+ })
package/src/define.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineComponent as defineLangComponent } from '@openuidev/lang-core'
2
+ import type { DefinedComponent } from '@openuidev/lang-core'
3
+ import type { z } from 'zod'
4
+
5
+ export type UiComponent<T extends z.ZodObject = z.ZodObject> = DefinedComponent<T, null>
6
+
7
+ export function defineUiComponent<T extends z.ZodObject>(config: { name: string, description: string, props: T }): UiComponent<T> {
8
+ return defineLangComponent<T, null>({ ...config, component: null })
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { actions, CopyText } from './actions'
2
+ export { Button, Buttons } from './components/actions'
3
+ export { Bar, BarChart } from './components/chart'
4
+ export { Citation, FileCard, Image, Option, Question, Questions } from './components/chat'
5
+ export { Col, Metric, Table } from './components/data'
6
+ export { Checkbox, Form, Input, Select } from './components/form'
7
+ export { Card, components, Stack } from './components/layout'
8
+ export { Callout, Markdown } from './components/text'
9
+ export { defineUiComponent } from './define'
10
+ export type { UiComponent } from './define'
11
+ export { componentNames, library } from './library'
12
+ export type { UiLibrary } from './library'
13
+ export { prompt, PROMPT_EXAMPLES } from './prompt'
14
+ export { LIBRARY_VERSION, UI_LANG_FENCE } from './version'
package/src/library.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { createLibrary } from '@openuidev/lang-core'
2
+ import { CopyText } from './actions'
3
+ import { components } from './components/layout'
4
+
5
+ export const library = createLibrary<null, typeof CopyText>({
6
+ root: 'Stack',
7
+ components: Object.values(components),
8
+ actions: [CopyText],
9
+ componentGroups: [
10
+ {
11
+ name: 'Layout',
12
+ components: ['Stack', 'Card'],
13
+ notes: [
14
+ '- root é sempre Stack. Use Card só quando houver mais de um bloco relacionado.',
15
+ '- direction "row" apenas para Metric ou Card lado a lado, no máximo 3.',
16
+ ],
17
+ },
18
+ {
19
+ name: 'Texto',
20
+ components: ['Markdown', 'Callout'],
21
+ notes: [
22
+ '- Citações cite:// e links vault:// aparecem no markdown, em Markdown/Callout, ou nos props ref de Citation e vaultRef de FileCard. Nunca em labels, títulos ou valores de tabela.',
23
+ ],
24
+ },
25
+ {
26
+ name: 'Dados',
27
+ components: ['Metric', 'Table', 'Col', 'BarChart', 'Bar'],
28
+ notes: [
29
+ '- Table para 3 ou mais linhas comparáveis. Menos que isso, use markdown fora do widget.',
30
+ '- Cada Col tem exatamente o mesmo número de values. Declare cada Col como statement próprio.',
31
+ '- Metric só para valores isolados e já formatados. Nunca repita um valor da tabela como Metric.',
32
+ '- BarChart só para valores numéricos da mesma unidade; Bar.value é número puro e Bar.display é o texto formatado.',
33
+ ],
34
+ },
35
+ {
36
+ name: 'Documentos',
37
+ components: ['FileCard', 'Image', 'Citation'],
38
+ notes: [
39
+ '- Use FileCard para um arquivo gerado ou recebido que o usuário deve abrir, e Image para uma imagem que deve aparecer na resposta. Nunca invente referências vault://, cite:// ou URLs.',
40
+ ],
41
+ },
42
+ {
43
+ name: 'Perguntas e formulários',
44
+ components: ['Questions', 'Question', 'Option', 'Form', 'Input', 'Select', 'Checkbox'],
45
+ notes: [
46
+ '- Questions quando a resposta depender de uma escolha do usuário. Pergunte o mínimo; não pergunte o que já foi dito.',
47
+ '- Form quando faltarem dados concretos (datas, valores, nomes). Nunca combine Form e Questions no mesmo widget.',
48
+ '- Depois de um Questions ou Form, encerre a resposta: não responda o que ainda depende da resposta do usuário.',
49
+ ],
50
+ },
51
+ {
52
+ name: 'Ações',
53
+ components: ['Buttons', 'Button'],
54
+ notes: [
55
+ '- Use Buttons para oferecer próximos passos concretos. Cada Button com @ToAssistant("mensagem") escrita na primeira pessoa, como o usuário diria.',
56
+ '- @CopyText("texto") para copiar um valor pronto, como um número de processo ou um trecho de petição.',
57
+ '- No máximo um Button com variant "primary".',
58
+ ],
59
+ },
60
+ ],
61
+ })
62
+
63
+ export type UiLibrary = typeof library
64
+ export const componentNames = Object.keys(library.components) as Array<keyof typeof components>
package/src/prompt.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { library } from './library'
2
+ import { UI_LANG_FENCE } from './version'
3
+
4
+ const PREAMBLE = `## Widgets na resposta
5
+
6
+ Você responde em markdown normal. Quando parte da resposta ficar melhor como interface (tabela comparativa, números de destaque, aviso, próximos passos clicáveis), insira um bloco de código com a linguagem \`${UI_LANG_FENCE}\` contendo um programa OpenUI Lang. O bloco é renderizado como interface nativa no lugar do código; o texto fora do bloco continua markdown.
7
+
8
+ Regras do envelope:
9
+ - Zero ou mais blocos \`${UI_LANG_FENCE}\` por resposta, cada um com seu próprio \`root = Stack(...)\` na primeira linha.
10
+ - Prose, citações cite:// e links vault:// ficam no markdown fora do bloco, ou dentro de Markdown/Callout.
11
+ - Resposta curta ou puramente textual: nenhum bloco. A maioria das respostas não precisa de widget.
12
+ - Nunca repita no markdown o que já está no widget, nem o contrário.
13
+ - Dentro do bloco só OpenUI Lang: sem comentários, sem markdown solto, sem fences aninhados.`
14
+
15
+ export const PROMPT_EXAMPLES = [
16
+ [
17
+ 'Pergunta: "compare os prazos de recurso". Resposta com widget:',
18
+ '',
19
+ 'Os prazos variam por tipo de recurso:',
20
+ '',
21
+ `\`\`\`${UI_LANG_FENCE}`,
22
+ 'root = Stack([tbl, next])',
23
+ 'tbl = Table([tipo, prazo, base], "Prazos recursais")',
24
+ 'tipo = Col("Recurso", ["Apelação", "Agravo", "Embargos"])',
25
+ 'prazo = Col("Prazo", ["15 dias", "15 dias", "5 dias"], "right")',
26
+ 'base = Col("Base legal", ["CPC art. 1.003", "CPC art. 1.003", "CPC art. 1.023"])',
27
+ 'next = Buttons([b1, b2])',
28
+ 'b1 = Button("Detalhar a apelação", @ToAssistant("Detalhe os requisitos da apelação"), "primary")',
29
+ 'b2 = Button("Ver contagem em dias úteis", @ToAssistant("Como conto esses prazos em dias úteis?"))',
30
+ '```',
31
+ '',
32
+ 'Todos contados em dias úteis [cite://doc/123#p=4].',
33
+ ].join('\n'),
34
+ ]
35
+
36
+ let defaultPrompt: string | undefined
37
+
38
+ export function prompt(options: { additionalRules?: string[] } = {}): string {
39
+ if (!options.additionalRules?.length)
40
+ return defaultPrompt ??= buildPrompt()
41
+ return buildPrompt(options.additionalRules)
42
+ }
43
+
44
+ function buildPrompt(additionalRules?: string[]): string {
45
+ return library.prompt({
46
+ preamble: PREAMBLE,
47
+ examples: PROMPT_EXAMPLES,
48
+ inlineMode: false,
49
+ toolCalls: false,
50
+ bindings: false,
51
+ builtinFunctions: false,
52
+ additionalRules,
53
+ })
54
+ }
package/src/version.ts ADDED
@@ -0,0 +1,2 @@
1
+ export const LIBRARY_VERSION = '0.3.0'
2
+ export const UI_LANG_FENCE = 'openui-lang'