@softize/opus 12.8.0 → 12.9.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/CHANGELOG.md +17 -0
- package/package.json +1 -1
- package/src/core/logical-type.ts +26 -2
- package/src/ui/components/primitives/chat.tsx +24 -4
- package/src/ui/docs/content/chat.md +24 -0
- package/src/ui/meta.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 12.9.0 — 2026-09-03
|
|
11
|
+
|
|
12
|
+
`Chat` aceita `empty`, um nó React para o estado sem mensagens: o app compõe o vazio com os
|
|
13
|
+
slots de `Empty` (marca, nome do agente, saudação administrável) e entrega no mesmo lugar do
|
|
14
|
+
`greeting`, que continua valendo como atalho de uma frase. `empty` vence quando os dois existem
|
|
15
|
+
e some quando a conversa começa; o slot ganha `data-variant="composed"`. Mudança aditiva.
|
|
16
|
+
|
|
17
|
+
## 12.8.1 — 2026-09-02
|
|
18
|
+
|
|
19
|
+
Num monorepo pnpm em que o workspace do contrato e o da SPA resolvem cópias distintas do Opus
|
|
20
|
+
(peers diferentes), as colunas de dicionário zero-config do `ActionList` e o fallback de opções
|
|
21
|
+
do `ActionForm` não encontravam o dicionário: a meta que `t.dict` grava no schema vivia num
|
|
22
|
+
`WeakMap` por instância, invisível para a outra cópia. A meta anexada por `attachLogicalType`
|
|
23
|
+
agora viaja também como propriedade não enumerável do próprio schema, chaveada por
|
|
24
|
+
`Symbol.for('@softize/opus/logical-type')`, e `getLogicalType` a lê quando o `WeakMap` local não
|
|
25
|
+
conhece o objeto. Sem mudança de API.
|
|
26
|
+
|
|
10
27
|
## 12.8.0 — 2026-09-02
|
|
11
28
|
|
|
12
29
|
A apresentação de um dicionário passa a ser declarada no próprio `t.dict`, e não inferida por
|
package/package.json
CHANGED
package/src/core/logical-type.ts
CHANGED
|
@@ -5,12 +5,21 @@
|
|
|
5
5
|
* labels de dict lendo a meta que viaja no schema do contrato, e a fronteira SPA
|
|
6
6
|
* (gate de arquitetura) só deixa a UI tocar ui/lib/core. O `@softize/opus/schema`
|
|
7
7
|
* re-exporta — a API pública não muda; as factories `t.*` seguem no driver.
|
|
8
|
+
*
|
|
9
|
+
* A meta é guardada de duas formas: num `WeakMap` desta instância e numa propriedade
|
|
10
|
+
* não enumerável do próprio schema, chaveada por `Symbol.for`. A segunda é o que
|
|
11
|
+
* atravessa instâncias do pacote: num monorepo pnpm, o workspace do contrato e o da
|
|
12
|
+
* SPA podem resolver cópias distintas do Opus (peers diferentes), e um `WeakMap` de
|
|
13
|
+
* uma cópia é invisível para a outra — o schema, não; ele é o mesmo objeto.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
import type { LogicalTypeMeta } from './types.ts'
|
|
11
17
|
|
|
12
18
|
const META = new WeakMap<object, LogicalTypeMeta>()
|
|
13
19
|
|
|
20
|
+
/** Chave global da propriedade que carrega a meta no próprio schema. */
|
|
21
|
+
export const LOGICAL_TYPE_PROPERTY = Symbol.for('@softize/opus/logical-type')
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Anexa metadata logical type a qualquer schema (objeto). Drivers usam
|
|
16
25
|
* internamente; consumers usam pra registrar tipos lógicos customizados.
|
|
@@ -20,12 +29,27 @@ export function attachLogicalType<S extends object>(
|
|
|
20
29
|
meta: LogicalTypeMeta,
|
|
21
30
|
): S {
|
|
22
31
|
META.set(schema, meta)
|
|
32
|
+
// Objeto congelado ou selado fica só no WeakMap — a assinatura aceita "qualquer objeto".
|
|
33
|
+
if (Object.isExtensible(schema)) {
|
|
34
|
+
Object.defineProperty(schema, LOGICAL_TYPE_PROPERTY, {
|
|
35
|
+
value: meta,
|
|
36
|
+
enumerable: false,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
23
41
|
return schema
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
/**
|
|
27
|
-
* Lê a metadata logical type anexada
|
|
45
|
+
* Lê a metadata logical type anexada, por esta instância ou por outra cópia do
|
|
46
|
+
* pacote que tenha anexado no mesmo objeto. Retorna `undefined` se ausente.
|
|
28
47
|
*/
|
|
29
48
|
export function getLogicalType(schema: object): LogicalTypeMeta | undefined {
|
|
30
|
-
|
|
49
|
+
const own = META.get(schema)
|
|
50
|
+
if (own !== undefined) return own
|
|
51
|
+
const carried = (schema as { [LOGICAL_TYPE_PROPERTY]?: unknown })[LOGICAL_TYPE_PROPERTY]
|
|
52
|
+
return carried !== null && typeof carried === 'object' && 'logicalType' in carried
|
|
53
|
+
? (carried as LogicalTypeMeta)
|
|
54
|
+
: undefined
|
|
31
55
|
}
|
|
@@ -71,6 +71,11 @@ export interface ChatProps {
|
|
|
71
71
|
/** Texto do estado vazio (centrado, some quando a conversa começa). Não entra no
|
|
72
72
|
* transcript — é apresentação, não fala do assistente. */
|
|
73
73
|
greeting?: string
|
|
74
|
+
/** Estado vazio composto pelo app (ex.: `EmptyHeader` com a marca, o nome e a saudação do
|
|
75
|
+
* agente). Ocupa o mesmo lugar do `greeting` e vence quando os dois existem; `null` ou
|
|
76
|
+
* `false` deixam o `greeting` valer; some quando a conversa começa. Prefira `greeting`
|
|
77
|
+
* quando bastar uma frase. */
|
|
78
|
+
empty?: React.ReactNode
|
|
74
79
|
/** Histórico inicial do modo autogerenciado (reidratação). Troque a `key` do componente
|
|
75
80
|
* ao trocar de conversa — o estado interno reinicia com estas mensagens. */
|
|
76
81
|
initialMessages?: ChatMessage[]
|
|
@@ -107,6 +112,7 @@ interface ChatTranscriptProps {
|
|
|
107
112
|
items: ChatTranscriptItem[]
|
|
108
113
|
indicator: string | null | undefined
|
|
109
114
|
greeting: string | undefined
|
|
115
|
+
empty: React.ReactNode | undefined
|
|
110
116
|
renderArtifact: ChatProps['renderArtifact']
|
|
111
117
|
}
|
|
112
118
|
|
|
@@ -120,11 +126,15 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
120
126
|
items,
|
|
121
127
|
indicator,
|
|
122
128
|
greeting,
|
|
129
|
+
empty,
|
|
123
130
|
renderArtifact,
|
|
124
131
|
}: ChatTranscriptProps): React.ReactElement {
|
|
125
132
|
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
126
133
|
const turns = React.useMemo(() => groupTurns(items), [items])
|
|
127
134
|
const indicatorVisible = indicator !== undefined
|
|
135
|
+
// `null`/`false` em `empty` significam "sem nó" (o dado ainda não chegou): o greeting
|
|
136
|
+
// continua como fallback em vez de um wrapper composto vazio.
|
|
137
|
+
const composedEmpty = empty !== undefined && empty !== null && empty !== false
|
|
128
138
|
React.useEffect(() => {
|
|
129
139
|
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight })
|
|
130
140
|
}, [items, indicator])
|
|
@@ -135,11 +145,19 @@ const ChatTranscript = React.memo(function ChatTranscript({
|
|
|
135
145
|
data-slot="chat-scroll"
|
|
136
146
|
className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4"
|
|
137
147
|
>
|
|
138
|
-
{items.length === 0 && !indicatorVisible &&
|
|
139
|
-
<div data-slot="chat-empty" className="m-auto
|
|
140
|
-
|
|
141
|
-
<p className="text-sm leading-relaxed">{greeting}</p>
|
|
148
|
+
{items.length === 0 && !indicatorVisible && composedEmpty ? (
|
|
149
|
+
<div data-slot="chat-empty" data-variant="composed" className="m-auto flex justify-center text-center">
|
|
150
|
+
{empty}
|
|
142
151
|
</div>
|
|
152
|
+
) : (
|
|
153
|
+
items.length === 0 &&
|
|
154
|
+
!indicatorVisible &&
|
|
155
|
+
greeting !== undefined && (
|
|
156
|
+
<div data-slot="chat-empty" className="m-auto max-w-[17.5rem] text-center text-muted-foreground">
|
|
157
|
+
<div className="mb-2 text-4xl">✦</div>
|
|
158
|
+
<p className="text-sm leading-relaxed">{greeting}</p>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
143
161
|
)}
|
|
144
162
|
{turns.map((turn, ti) => (
|
|
145
163
|
// Hierarquia do espaço: as falas de um mesmo turno são um raciocínio contínuo
|
|
@@ -235,6 +253,7 @@ export function Chat({
|
|
|
235
253
|
composerActions,
|
|
236
254
|
composerClassName,
|
|
237
255
|
greeting,
|
|
256
|
+
empty,
|
|
238
257
|
initialMessages,
|
|
239
258
|
kickoff,
|
|
240
259
|
renderArtifact,
|
|
@@ -374,6 +393,7 @@ export function Chat({
|
|
|
374
393
|
items={items}
|
|
375
394
|
indicator={indicator}
|
|
376
395
|
greeting={greeting}
|
|
396
|
+
empty={empty}
|
|
377
397
|
renderArtifact={renderArtifact}
|
|
378
398
|
/>
|
|
379
399
|
{/* Composer da casa (pílula elevada, enviar dentro). Extraído no <Composer> — o Chat
|
|
@@ -15,6 +15,30 @@ Um chat mínimo: lista de mensagens + composer. A conversa é gerenciada por den
|
|
|
15
15
|
</div>
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Estado vazio composto
|
|
19
|
+
|
|
20
|
+
Quando uma frase não basta — marca, nome do agente e saudação administrável, por exemplo — o
|
|
21
|
+
app compõe o vazio com os slots de `Empty` e entrega em `empty`. O nó ocupa o mesmo lugar do
|
|
22
|
+
`greeting`, vence quando os dois existem e some quando a conversa começa. Sem a moldura `Empty`
|
|
23
|
+
em volta: o chat já é a estrutura, e a moldura tracejada é para região de criação.
|
|
24
|
+
|
|
25
|
+
```tsx preview
|
|
26
|
+
<div className="h-72 rounded-lg border">
|
|
27
|
+
<Chat
|
|
28
|
+
empty={
|
|
29
|
+
<EmptyHeader>
|
|
30
|
+
<EmptyMedia variant="icon">
|
|
31
|
+
<Sparkles />
|
|
32
|
+
</EmptyMedia>
|
|
33
|
+
<EmptyTitle>Copilot</EmptyTitle>
|
|
34
|
+
<EmptyDescription>Pergunte sobre vendas, estoque, oficina ou pessoas.</EmptyDescription>
|
|
35
|
+
</EmptyHeader>
|
|
36
|
+
}
|
|
37
|
+
send={async () => 'Pronto.'}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
```
|
|
41
|
+
|
|
18
42
|
## O backend (o agente)
|
|
19
43
|
|
|
20
44
|
O `send` posta a conversa no seu endpoint, que resolve o usuário e roda o agente sobre as actions `ai:enabled` — **como o usuário logado**:
|
package/src/ui/meta.ts
CHANGED
|
@@ -75,7 +75,7 @@ export const componentMeta = {
|
|
|
75
75
|
name: 'chat',
|
|
76
76
|
ancestry: 'opus',
|
|
77
77
|
whenToUse:
|
|
78
|
-
'Chat da casa (lista de mensagens + composer) que gerencia a conversa por dentro (estado/loading/auto-scroll; Enter envia, Shift+Enter quebra linha). A inteligência vem da prop `send` — resposta inteira (Promise<string>) OU streaming (AsyncIterable<ChatEvent>: texto incremental, indicador vivo do tool, artefato via renderArtifact). No Opus, o backend liga em `runtime.aiFor(base).run(...)` ou `.runStream(...)`. Dê altura ao container (ex.: `h-full`).',
|
|
78
|
+
'Chat da casa (lista de mensagens + composer) que gerencia a conversa por dentro (estado/loading/auto-scroll; Enter envia, Shift+Enter quebra linha). A inteligência vem da prop `send` — resposta inteira (Promise<string>) OU streaming (AsyncIterable<ChatEvent>: texto incremental, indicador vivo do tool, artefato via renderArtifact). No Opus, o backend liga em `runtime.aiFor(base).run(...)` ou `.runStream(...)`. Estado vazio por `greeting` (frase) ou `empty` (nó composto com os slots de Empty). Dê altura ao container (ex.: `h-full`).',
|
|
79
79
|
},
|
|
80
80
|
'checkbox': {
|
|
81
81
|
name: 'checkbox',
|