@fluencypassdevs/cycle 1.9.7 → 1.13.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/bin/mcp.mjs CHANGED
@@ -411,11 +411,11 @@ const COMPONENTS = [
411
411
  {
412
412
  name: "ChatMessage",
413
413
  import: `import { ChatMessage } from "@fluencypassdevs/cycle"`,
414
- description: "Mensagem de conversa com IA. Persona ai/user/system, suporta texto + audio inline + like/dislike. Loading state com typing indicator. AI usa icone Ai + bubble transparente. User usa bubble pink coral (theme-brand) + avatar.",
414
+ description: "Mensagem de conversa com IA. Persona ai/user/system, suporta texto + audio inline + like/dislike. Loading state com typing indicator. AI usa icone Ai + bubble transparente. User usa bubble pink coral (theme-brand) + avatar. CONVENCAO DO PRODUTO: (1) toda msg da IA com `audioSrc` deve ter `text` (roteiro/legenda do TTS); (2) toda msg do user com `audioSrc` deve ter `text` (transcricao gerada por IA). Garante acessibilidade (leitura + escuta), busca no historico e suporte a screen readers.",
415
415
  props: [
416
416
  { name: "persona", type: '"ai" | "user" | "system"', default: "-" },
417
- { name: "text", type: "string", default: "-" },
418
- { name: "audioSrc", type: "string", default: "-" },
417
+ { name: "text", type: "string", default: "-", note: "Em ai com audioSrc, e o roteiro do TTS. Em user com audioSrc, e a transcricao." },
418
+ { name: "audioSrc", type: "string", default: "-", note: "IA: TTS gerado. User: gravacao do microfone. Em ambos, sempre acompanhe com text." },
419
419
  { name: "avatar", type: "string", default: "-" },
420
420
  { name: "author", type: "string", default: "-" },
421
421
  { name: "showFeedback", type: "boolean", default: "false" },
@@ -423,8 +423,11 @@ const COMPONENTS = [
423
423
  { name: "onFeedbackChange", type: "(val) => void", default: "-" },
424
424
  { name: "loading", type: "boolean", default: "false" },
425
425
  ],
426
- example: `<ChatMessage persona="ai" text="Como posso ajudar?" />
427
- <ChatMessage persona="ai" text="Vou responder..." audioSrc="/audio.mp3" showFeedback />
426
+ example: `{/* IA: sempre com text + audioSrc (convencao) */}
427
+ <ChatMessage persona="ai" text="Como posso ajudar?" audioSrc="/ai-tts.mp3" showFeedback />
428
+ {/* User com audio: SEMPRE inclua text com a transcricao */}
429
+ <ChatMessage persona="user" text="Can you explain present perfect?" audioSrc="/user-rec.mp3" author="Felipe" />
430
+ {/* User so com texto (digitado): audio nao se aplica */}
428
431
  <ChatMessage persona="user" text="Oi" avatar="/me.jpg" author="Felipe" />
429
432
  <ChatMessage persona="ai" loading />
430
433
  <ChatMessage persona="system" text="Conversa iniciada" />`,
@@ -450,7 +453,7 @@ const COMPONENTS = [
450
453
  {
451
454
  name: "MessageBar",
452
455
  import: `import { MessageBar } from "@fluencypassdevs/cycle"`,
453
- description: "Input bar para chat com IA. 6 estados: default, audio-only, active, disabled, recording, paused. UI-only: consumer implementa MediaRecorder + HTMLAudioElement via callbacks. Live waveform real durante recording (Web Audio API, rolling window estilo WhatsApp). RecordedWaveform real durante paused com peaks + barras tocadas/nao-tocadas + dot verde + click/drag-to-seek. Press-to-record no mobile (touch-only): long-press inicia, drag-up trava em hands-free, drag-left cancela, release < 1s cancela, release >= 1s envia. Vibracao haptica em lock/cancel.",
456
+ description: "Input bar para chat com IA. 6 estados: default, audio-only, active, disabled, recording, paused. UI-only: consumer implementa MediaRecorder + HTMLAudioElement via callbacks. Live waveform real durante recording (Web Audio API, rolling window estilo WhatsApp). RecordedWaveform real durante paused com peaks + barras tocadas/nao-tocadas + dot verde + click/drag-to-seek. Press-to-record no mobile (touch-only): long-press inicia, drag-up trava em hands-free, drag-left cancela, release < 1s cancela, release >= 1s envia. Vibracao haptica em lock/cancel. Suporta limite de duracao via maxRecordingDuration (segundos) com warning visual configuravel nos segundos finais (timer destructive + 'Xs restantes' aria-live) e callback onMaxDurationReached ao atingir o limite.",
454
457
  props: [
455
458
  { name: "state", type: '"default" | "audio-only" | "active" | "disabled" | "recording" | "paused"', default: '"default"' },
456
459
  { name: "value", type: "string", default: '""' },
@@ -468,6 +471,10 @@ const COMPONENTS = [
468
471
  { name: "playbackProgress", type: "number (0-1)", default: "0", note: "consumer atualiza via audio.ontimeupdate enquanto toca o preview no estado paused" },
469
472
  { name: "onSeekPlayback", type: "(progress) => void", default: "-", note: "consumer seta audio.currentTime = progress * duration" },
470
473
  { name: "placeholder", type: "string", default: '"Digite sua mensagem..."' },
474
+ { name: "maxRecordingDuration", type: "number", default: "-", note: "limite em segundos. Quando atingido, dispara onMaxDurationReached uma vez. Sem isso, sem limite." },
475
+ { name: "warnAtSecondsLeft", type: "number", default: "10", note: "ativa warning visual nos ultimos N segundos (timer destructive + 'Xs restantes' aria-live polite)" },
476
+ { name: "onMaxDurationReached", type: "() => void", default: "-", note: "disparado uma vez ao atingir o limite. ChatThread usa pra auto-pausar; consumer puro pode usar pra customizar" },
477
+ { name: "secondsLeftLabel", type: "(s) => string", default: "(s) => `${s}s restantes`", note: "label do warning (pra i18n)" },
471
478
  ],
472
479
  example: `// 1) Captura: passa o MESMO stream pro MessageBar (live waveform).
473
480
  // 2) Pause: recorder.stop() gera o Blob; crie um <audio> e atualize playbackProgress via ontimeupdate.
@@ -475,9 +482,75 @@ const COMPONENTS = [
475
482
 
476
483
  <MessageBar value={text} onChange={setText} onSendText={(t) => send(t)} onStartRecording={() => startRec()} />
477
484
  <MessageBar state="recording" recordingDuration={4} recordingStream={stream} onPauseRecording={pause} onCancelRecording={cancel} onSendAudio={send} />
478
- <MessageBar state="paused" recordingDuration={42} isPlaying={isPlaying} playbackProgress={progress} onTogglePlay={togglePlay} onSeekPlayback={seek} onResumeRecording={resume} onCancelRecording={cancel} onSendAudio={send} />`,
485
+ <MessageBar state="paused" recordingDuration={42} isPlaying={isPlaying} playbackProgress={progress} onTogglePlay={togglePlay} onSeekPlayback={seek} onResumeRecording={resume} onCancelRecording={cancel} onSendAudio={send} />
486
+ {/* Com limite de duracao (timer fica vermelho aos ultimos 5s, auto-pausa aos 30s) */}
487
+ <MessageBar state="recording" recordingDuration={duration} recordingStream={stream} maxRecordingDuration={30} warnAtSecondsLeft={5} onMaxDurationReached={() => pause()} onPauseRecording={pause} onSendAudio={send} />`,
479
488
  keywords: ["chat", "input", "message", "bar", "audio", "recording", "microphone", "mensagem", "envio", "press-to-record", "waveform", "playback", "whatsapp"],
480
489
  },
490
+ {
491
+ name: "ChatThread",
492
+ import: `import { ChatThread, type ChatThreadMessage } from "@fluencypassdevs/cycle"`,
493
+ description: "Composite 'tela de chat pronta' que junta ChatMessage + MessageBar + MessageRating. Gerencia state machine interno do MessageBar (default↔recording↔paused) baseado nos callbacks. Auto-scroll inteligente (preserva posicao se user subiu + botao '↓ nova mensagem'). Scroll infinito via onLoadMore. Banners configuraveis: offline, rateLimitedUntil (countdown), quotaExhausted (AlertDialog). Retry inline em msgs failed do user (B2) e regenerar resposta da IA (B3). Botao 'Parar resposta' durante thinking e OPT-IN via presenca de onStopResponse — sem callback, MessageBar some com animacao de saida e volta quando IA termina. requestRating per-message renderiza MessageRating inline embaixo da bubble da IA — escolha some imediato, callback onRate(messageId, value, label) permite consumer enviar como msg do user. initialThinking pra conversas que comecam com IA falando. Input controlled internamente. Animacoes fade+slide (200ms enter / 150ms leave). Suporta limite de duracao de gravacao via maxRecordingDuration (segundos) com warning visual configuravel e auto-pause ao atingir; override via onMaxDurationReached.",
494
+ props: [
495
+ { name: "messages", type: "ChatThreadMessage[]", default: "[]", note: "array de mensagens da conversa" },
496
+ { name: "state", type: '"idle" | "sending" | "thinking" | "error"', default: '"idle"', note: "estado global da conversa (controlled)" },
497
+ { name: "initialThinking", type: "boolean", default: "false", note: "quando true e thread vazio, monta direto em thinking (cenario IA fala primeiro)" },
498
+ { name: "audioOnlyMode", type: "boolean", default: "false", note: "MessageBar interno em modo apenas audio (sem input texto)" },
499
+ { name: "onSendText", type: "(text) => void", default: "-" },
500
+ { name: "onSendAudio", type: "() => void", default: "-", note: "dispara quando user confirma envio (consumer pega Blob do MediaRecorder proprio)" },
501
+ { name: "onStartRecording", type: "() => void", default: "-", note: "consumer chama getUserMedia + cria MediaRecorder" },
502
+ { name: "onPauseRecording", type: "() => void", default: "-", note: "consumer faz recorder.stop() + cria Blob para preview" },
503
+ { name: "onResumeRecording", type: "() => void", default: "-" },
504
+ { name: "onCancelRecording", type: "() => void", default: "-" },
505
+ { name: "onTogglePlay", type: "() => void", default: "-" },
506
+ { name: "onSeekPlayback", type: "(progress) => void", default: "-" },
507
+ { name: "recordingStream", type: "MediaStream | null", default: "null", note: "pra live waveform durante recording" },
508
+ { name: "recordingDuration", type: "number", default: "0" },
509
+ { name: "isPlaying", type: "boolean", default: "false" },
510
+ { name: "playbackProgress", type: "number (0-1)", default: "0" },
511
+ { name: "onRetryMessage", type: "(messageId) => void", default: "-", note: "retry msg do user com status=failed" },
512
+ { name: "onRegenerateResponse", type: "() => void", default: "-", note: "regerar resposta da IA com erro" },
513
+ { name: "onStopResponse", type: "() => void", default: "-", note: "OPT-IN: passar este callback ativa o botao 'Parar resposta' durante thinking. Sem ele, MessageBar some com animacao e volta quando IA termina." },
514
+ { name: "onLoadMore", type: "() => Promise<void>", default: "-", note: "scroll infinito ao chegar no topo (G1)" },
515
+ { name: "offline", type: "boolean", default: "false", note: "banner topo + MessageBar disabled" },
516
+ { name: "rateLimitedUntil", type: "number | null", default: "null", note: "timestamp epoch ate quando bloquear (banner countdown)" },
517
+ { name: "quotaExhausted", type: "boolean", default: "false", note: "abre AlertDialog modal" },
518
+ { name: "quotaExhaustedConfig", type: "{ title, description, ctaLabel, onCta }", default: "-", note: "conteudo do modal configuravel" },
519
+ { name: "onRate", type: "(messageId, value, label) => void", default: "-", note: "rating de uma msg da IA com requestRating=true" },
520
+ { name: "ratingLabels", type: "Partial<MessageRatingLabels>", default: "PT-BR defaults", note: "labels customizadas do rating" },
521
+ { name: "placeholder", type: "string", default: '"Digite sua mensagem..."' },
522
+ { name: "maxLength", type: "number", default: "-" },
523
+ { name: "userAvatar", type: "string", default: "-", note: "URL do avatar do user — propagado pra todas as msgs do user automaticamente" },
524
+ { name: "userName", type: "string", default: "-", note: "Nome do user — usado para iniciais no fallback do avatar e alt da imagem" },
525
+ { name: "maxRecordingDuration", type: "number", default: "-", note: "limite de duracao da gravacao em segundos. Ao atingir, auto-pausa (vai pro preview) a menos que onMaxDurationReached seja passado" },
526
+ { name: "warnAtSecondsLeft", type: "number", default: "10", note: "passthrough pro MessageBar — quando ativar o warning visual nos segundos finais" },
527
+ { name: "onMaxDurationReached", type: "() => void", default: "-", note: "OVERRIDE do auto-pause. Quando passado, consumer assume controle ao atingir o limite" },
528
+ { name: "secondsLeftLabel", type: "(s) => string", default: "(s) => `${s}s restantes`", note: "passthrough pro MessageBar (pra i18n)" },
529
+ ],
530
+ example: `// ChatThreadMessage extra fields:
531
+ // - status?: "sent" | "pending" | "failed"
532
+ // - text?: string
533
+ // - audioSrc?: string (URL ou Blob URL)
534
+ // - audioPeaks?: number[]
535
+ // - errorText?: string (msg de erro custom para AI failed)
536
+ // - requestRating?: boolean (true na msg da IA que pede avaliacao — renderiza MessageRating inline)
537
+
538
+ <ChatThread
539
+ messages={messages}
540
+ state={state}
541
+ initialThinking
542
+ userAvatar="/me.jpg"
543
+ userName="Felipe Pereira"
544
+ onSendText={(text) => sendText(text)}
545
+ onSendAudio={() => sendAudio()}
546
+ onStartRecording={() => startRec()}
547
+ onPauseRecording={() => pauseRec()}
548
+ onRate={(msgId, value, label) => {
549
+ setMessages((m) => [...m, { id: newId(), persona: "user", status: "sent", text: label.subtitle }])
550
+ }}
551
+ />`,
552
+ keywords: ["chat", "thread", "conversation", "conversa", "ai", "ia", "composite", "tela", "screen", "rating", "scroll", "thinking", "offline", "rate-limit", "quota", "feedback"],
553
+ },
481
554
  {
482
555
  name: "LikeDislike",
483
556
  import: `import { LikeDislike } from "@fluencypassdevs/cycle"`,
@@ -3,13 +3,14 @@ import { __objRest, __spreadValues, __spreadProps } from './chunk-YINJ5YZ5.js';
3
3
  import * as React from 'react';
4
4
  import { jsxs, jsx } from 'react/jsx-runtime';
5
5
 
6
- var DEFAULT_LABELS = {
6
+ var DEFAULT_MESSAGE_RATING_LABELS = {
7
7
  1: { emoji: "\u{1F621}", title: "Disappointing", subtitle: "Decepcionante" },
8
8
  2: { emoji: "\u{1F61E}", title: "Frustrating", subtitle: "Frustrante" },
9
9
  3: { emoji: "\u{1F610}", title: "Ok", subtitle: "Tudo bem" },
10
10
  4: { emoji: "\u{1F642}", title: "Helpful", subtitle: "Ajudou" },
11
11
  5: { emoji: "\u{1F60D}", title: "Loved it", subtitle: "Amei" }
12
12
  };
13
+ var DEFAULT_LABELS = DEFAULT_MESSAGE_RATING_LABELS;
13
14
  var VALUES = [1, 2, 3, 4, 5];
14
15
  function MessageRating(_a) {
15
16
  var _b = _a, {
@@ -96,6 +97,6 @@ function MessageRating(_a) {
96
97
  );
97
98
  }
98
99
 
99
- export { MessageRating };
100
- //# sourceMappingURL=chunk-37C2K2NM.js.map
101
- //# sourceMappingURL=chunk-37C2K2NM.js.map
100
+ export { DEFAULT_MESSAGE_RATING_LABELS, MessageRating };
101
+ //# sourceMappingURL=chunk-6OYSTCGP.js.map
102
+ //# sourceMappingURL=chunk-6OYSTCGP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ui/message-rating.tsx"],"names":[],"mappings":"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","file":"chunk-6OYSTCGP.js","sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* ─── Types ───────────────────────────────────────────────────── */\n\nexport type MessageRatingValue = 1 | 2 | 3 | 4 | 5\n\nexport interface RatingLabel {\n emoji: string\n title: string\n subtitle: string\n}\n\nexport type MessageRatingLabels = Record<MessageRatingValue, RatingLabel>\n\nexport interface MessageRatingProps extends Omit<React.ComponentProps<\"div\">, \"onChange\" | \"defaultValue\"> {\n /** Valor selecionado (controlado) */\n value?: MessageRatingValue | null\n /** Valor inicial (nao controlado) */\n defaultValue?: MessageRatingValue | null\n /** Callback ao escolher uma opcao */\n onChange?: (value: MessageRatingValue) => void\n /** Layout. \"auto\" usa vertical (column) abaixo de sm e horizontal acima */\n layout?: \"horizontal\" | \"vertical\" | \"auto\"\n /** Labels customizadas (default: PT-BR) */\n labels?: Partial<MessageRatingLabels>\n /** Desabilita a interacao */\n disabled?: boolean\n /** Titulo opcional acima das opcoes */\n title?: string\n}\n\n/* ─── Default labels (PT-BR) ──────────────────────────────────── */\n\nexport const DEFAULT_MESSAGE_RATING_LABELS: MessageRatingLabels = {\n 1: { emoji: \"😡\", title: \"Disappointing\", subtitle: \"Decepcionante\" },\n 2: { emoji: \"😞\", title: \"Frustrating\", subtitle: \"Frustrante\" },\n 3: { emoji: \"😐\", title: \"Ok\", subtitle: \"Tudo bem\" },\n 4: { emoji: \"🙂\", title: \"Helpful\", subtitle: \"Ajudou\" },\n 5: { emoji: \"😍\", title: \"Loved it\", subtitle: \"Amei\" },\n}\n\n// Backwards-compatible alias usado internamente\nconst DEFAULT_LABELS = DEFAULT_MESSAGE_RATING_LABELS\n\nconst VALUES: MessageRatingValue[] = [1, 2, 3, 4, 5]\n\n/* ─── Component ───────────────────────────────────────────────── */\n\nfunction MessageRating({\n value: controlledValue,\n defaultValue,\n onChange,\n layout = \"auto\",\n labels: labelOverrides,\n disabled = false,\n title,\n className,\n ...props\n}: MessageRatingProps) {\n const [internalValue, setInternalValue] = React.useState<MessageRatingValue | null>(\n defaultValue ?? null\n )\n const isControlled = controlledValue !== undefined\n const value = isControlled ? controlledValue : internalValue\n\n const labels: MessageRatingLabels = React.useMemo(\n () => ({ ...DEFAULT_LABELS, ...labelOverrides }),\n [labelOverrides]\n )\n\n const handleSelect = (v: MessageRatingValue) => {\n if (disabled) return\n if (!isControlled) setInternalValue(v)\n onChange?.(v)\n }\n\n /* Layout: vertical (column) sobrescreve so essa direcao. horizontal/auto\n sao iguais agora (sempre flex-row, pois mobile e desktop diferem no\n conteudo de cada botao, nao na direcao do container). */\n const layoutClass = layout === \"vertical\" ? \"flex-col\" : \"flex-row\"\n\n return (\n <div\n data-slot=\"message-rating\"\n role=\"radiogroup\"\n aria-label={title ?? \"Avalie sua experiencia\"}\n className={cn(\"flex flex-col gap-3\", className)}\n {...props}\n >\n {title && (\n <p className=\"text-sm font-medium text-foreground\">{title}</p>\n )}\n <div className={cn(\"flex gap-2\", layoutClass)}>\n {VALUES.map((v) => {\n const label = labels[v]\n const selected = value === v\n return (\n <button\n key={v}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={`${label.title} — ${label.subtitle}`}\n disabled={disabled}\n onClick={() => handleSelect(v)}\n className={cn(\n /* Base — shared mobile/desktop */\n \"flex items-center transition-[color,background-color,border-color,box-shadow] outline-none\",\n \"hover:opacity-90 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n /* Mobile: compacto, justa o emoji centralizado em pill cinza */\n \"rounded-xl px-3 py-2 justify-center\",\n /* Desktop: card completo com texto, flex-1, border */\n \"sm:flex-1 sm:gap-3 sm:px-3 sm:py-2.5 sm:text-left sm:justify-start sm:border\",\n /* Selected vs unselected (estados diferentes por breakpoint) */\n selected\n ? \"bg-accent theme-brand sm:border-primary sm:shadow-xs\"\n : \"bg-muted sm:bg-transparent sm:border-neutral-border\"\n )}\n >\n <span className=\"text-base sm:text-2xl leading-none shrink-0\" aria-hidden=\"true\">\n {label.emoji}\n </span>\n {/* Labels: visiveis apenas no desktop */}\n <span className=\"hidden sm:flex sm:flex-col min-w-0\">\n <span className=\"text-sm font-medium leading-tight text-foreground truncate\">\n {label.title}\n </span>\n <span className=\"text-xs text-muted-foreground leading-tight truncate\">\n {label.subtitle}\n </span>\n </span>\n </button>\n )\n })}\n </div>\n </div>\n )\n}\n\nexport { MessageRating }\n"]}
@@ -1,11 +1,12 @@
1
1
  import { LikeDislike } from './chunk-F2XA2Z75.js';
2
2
  import { Ai } from './chunk-JPEDYOV7.js';
3
+ import { CycleIcon } from './chunk-V7M2NHUO.js';
3
4
  import { Avatar, AvatarImage, AvatarFallback } from './chunk-MSLQRGSP.js';
4
5
  import { cn } from './chunk-TYCPXAXF.js';
5
6
  import { __objRest, __spreadProps, __spreadValues } from './chunk-YINJ5YZ5.js';
6
7
  import * as React from 'react';
7
8
  import { cva } from 'class-variance-authority';
8
- import { Pause, Play } from 'lucide-react';
9
+ import { Pause, Play, User } from 'lucide-react';
9
10
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
11
 
11
12
  var chatMessageVariants = cva("flex w-full text-sm", {
@@ -143,8 +144,6 @@ var STATIC_FALLBACK_PEAKS = [
143
144
  ].map((h) => h / 32);
144
145
  var MIN_BAR_HEIGHT = 2;
145
146
  var MAX_BAR_HEIGHT = 32;
146
- var BAR_WIDTH = 2;
147
- var BAR_GAP = 2;
148
147
  var SPEED_OPTIONS = [1, 1.25, 1.5, 2, 0.75];
149
148
  var peaksCache = /* @__PURE__ */ new Map();
150
149
  async function calculatePeaks(audioUrl, barCount) {
@@ -273,13 +272,10 @@ function ChatAudio(_a) {
273
272
  if (!audio || !waveform2 || !duration) return;
274
273
  const rect = waveform2.getBoundingClientRect();
275
274
  const x = clientX - rect.left;
276
- const newProgress = Math.max(
277
- 0,
278
- Math.min(x / (peaks.length * (BAR_WIDTH + BAR_GAP) - BAR_GAP), 1)
279
- );
275
+ const newProgress = Math.max(0, Math.min(x / rect.width, 1));
280
276
  audio.currentTime = newProgress * duration;
281
277
  },
282
- [duration, peaks.length]
278
+ [duration]
283
279
  );
284
280
  const handlePointerDown = (e) => {
285
281
  if (!duration) return;
@@ -298,12 +294,11 @@ function ChatAudio(_a) {
298
294
  };
299
295
  const progress = duration > 0 ? currentTime / duration : 0;
300
296
  const displayTime = formatAudioTime(currentTime);
301
- const barsLayoutWidth = peaks.length * (BAR_WIDTH + BAR_GAP) - BAR_GAP;
302
297
  const waveform = /* @__PURE__ */ jsxs(
303
298
  "div",
304
299
  {
305
300
  ref: waveformRef,
306
- className: "flex-1 relative h-8 min-w-0 overflow-hidden touch-none cursor-pointer select-none",
301
+ className: "flex-1 relative h-8 min-w-0 touch-none cursor-pointer select-none",
307
302
  onPointerDown: handlePointerDown,
308
303
  onPointerMove: handlePointerMove,
309
304
  onPointerUp: handlePointerUp,
@@ -314,7 +309,7 @@ function ChatAudio(_a) {
314
309
  "aria-valuemax": Math.round(duration),
315
310
  "aria-valuenow": Math.round(currentTime),
316
311
  children: [
317
- /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 left-0 flex items-center gap-[2px]", children: peaks.map((peak, i) => {
312
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-between", children: peaks.map((peak, i) => {
318
313
  const isPlayed = i / peaks.length <= progress;
319
314
  const height = Math.max(
320
315
  MIN_BAR_HEIGHT,
@@ -336,7 +331,7 @@ function ChatAudio(_a) {
336
331
  "div",
337
332
  {
338
333
  className: "absolute top-1/2 z-10 -translate-y-1/2 -translate-x-1/2 size-3 rounded-full bg-[#098A5E] pointer-events-none shadow-md",
339
- style: { left: `${progress * barsLayoutWidth}px` },
334
+ style: { left: `${progress * 100}%` },
340
335
  "aria-hidden": "true"
341
336
  }
342
337
  )
@@ -453,11 +448,11 @@ function ChatMessage(_a) {
453
448
  children: /* @__PURE__ */ jsxs("div", { className: "flex gap-3 max-w-[85%] sm:max-w-[75%] w-full", children: [
454
449
  /* @__PURE__ */ jsx("div", { className: "hidden sm:flex shrink-0 size-8 rounded-md bg-primary items-center justify-center theme-brand", children: /* @__PURE__ */ jsx("span", { className: "text-primary-foreground", children: /* @__PURE__ */ jsx(Ai, { size: "sm", decorative: true }) }) }),
455
450
  /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 min-w-0 flex-1", children: loading ? (
456
- /* Loading: dots dentro de bubble cinza (speech bubble shape — top-left reto) */
457
- /* @__PURE__ */ jsx("div", { className: "self-start inline-flex items-center rounded-2xl sm:rounded-tl-none bg-muted p-5 text-muted-foreground", children: /* @__PURE__ */ jsx(TypingDots, {}) })
451
+ /* Loading: dots dentro de bubble cinza (speech bubble shape — top-left reto em todos os breakpoints) */
452
+ /* @__PURE__ */ jsx("div", { className: "self-start inline-flex items-center rounded-2xl rounded-tl-none bg-muted p-5 text-muted-foreground", children: /* @__PURE__ */ jsx(TypingDots, {}) })
458
453
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
459
- text && /* Text bubble: bg-muted sempre, speech bubble shape (tl-none) apenas no desktop */
460
- /* @__PURE__ */ jsx("div", { className: "rounded-2xl bg-muted p-5 sm:rounded-tl-none", children: /* @__PURE__ */ jsx("p", { className: "text-base leading-6 text-neutral-foreground break-words", children: text }) }),
454
+ text && /* Text bubble: bg-muted sempre, speech bubble shape (tl-none) em todos os breakpoints */
455
+ /* @__PURE__ */ jsx("div", { className: "rounded-2xl rounded-tl-none bg-muted p-5", children: /* @__PURE__ */ jsx("p", { className: "text-base leading-6 text-neutral-foreground break-words", children: text }) }),
461
456
  audioSrc && /* @__PURE__ */ jsx(ChatAudio, { src: audioSrc, variant: "neutral" }),
462
457
  showFeedback && /* @__PURE__ */ jsx(
463
458
  LikeDislike,
@@ -481,17 +476,17 @@ function ChatMessage(_a) {
481
476
  }, props), {
482
477
  children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 max-w-[85%] sm:max-w-[75%] w-full justify-end", children: [
483
478
  /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 min-w-0 items-end theme-brand flex-1", children: loading ? (
484
- /* Loading: dots dentro de bubble pink (speech bubble shape — top-right reto) */
485
- /* @__PURE__ */ jsx("div", { className: "inline-flex items-center rounded-2xl sm:rounded-tr-none bg-accent p-5 text-neutral-muted-foreground", children: /* @__PURE__ */ jsx(TypingDots, {}) })
479
+ /* Loading: dots dentro de bubble pink (speech bubble shape — top-right reto em todos os breakpoints) */
480
+ /* @__PURE__ */ jsx("div", { className: "inline-flex items-center rounded-2xl rounded-tr-none bg-accent p-5 text-neutral-muted-foreground", children: /* @__PURE__ */ jsx(TypingDots, {}) })
486
481
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
487
- text && /* Text bubble: speech bubble shape (top-right reto) no desktop.
482
+ text && /* Text bubble: speech bubble shape (top-right reto) em todos os breakpoints.
488
483
  text-neutral-foreground = preto neutro fixo, nao muda com theme-brand */
489
- /* @__PURE__ */ jsx("div", { className: "rounded-2xl sm:rounded-tr-none bg-accent p-5", children: /* @__PURE__ */ jsx("p", { className: "text-base leading-6 text-neutral-foreground break-words", children: text }) }),
484
+ /* @__PURE__ */ jsx("div", { className: "rounded-2xl rounded-tr-none bg-accent p-5", children: /* @__PURE__ */ jsx("p", { className: "text-base leading-6 text-neutral-foreground break-words", children: text }) }),
490
485
  audioSrc && /* @__PURE__ */ jsx(ChatAudio, { src: audioSrc, variant: "brand" })
491
486
  ] }) }),
492
487
  /* @__PURE__ */ jsxs(Avatar, { size: "lg", className: "hidden sm:flex shrink-0 rounded-lg", children: [
493
488
  avatar && /* @__PURE__ */ jsx(AvatarImage, { src: avatar, alt: author != null ? author : "User", className: "rounded-lg" }),
494
- /* @__PURE__ */ jsx(AvatarFallback, { className: "rounded-lg bg-muted text-muted-foreground text-sm", children: getInitials(author) })
489
+ /* @__PURE__ */ jsx(AvatarFallback, { className: "rounded-lg bg-muted text-muted-foreground text-sm", children: author ? getInitials(author) : /* @__PURE__ */ jsx(CycleIcon, { icon: User, size: "sm", decorative: true }) })
495
490
  ] })
496
491
  ] })
497
492
  })
@@ -499,5 +494,5 @@ function ChatMessage(_a) {
499
494
  }
500
495
 
501
496
  export { ChatAudio, ChatMessage, chatMessageVariants };
502
- //# sourceMappingURL=chunk-YMWRR7ET.js.map
503
- //# sourceMappingURL=chunk-YMWRR7ET.js.map
497
+ //# sourceMappingURL=chunk-CG7NXMBC.js.map
498
+ //# sourceMappingURL=chunk-CG7NXMBC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/ui/chat-message.tsx"],"names":["_a","waveform"],"mappings":"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","file":"chunk-CG7NXMBC.js","sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Play, Pause, User } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Avatar, AvatarImage, AvatarFallback } from \"@/components/ui/avatar\"\nimport { LikeDislike, type LikeDislikeValue } from \"@/components/ui/like-dislike\"\nimport { Ai, CycleIcon } from \"@/components/icons\"\n\n/* ─── Container variants ──────────────────────────────────────── */\n\nconst chatMessageVariants = cva(\"flex w-full text-sm\", {\n variants: {\n persona: {\n ai: \"justify-start\",\n user: \"justify-end\",\n system: \"justify-center\",\n },\n },\n defaultVariants: {\n persona: \"ai\",\n },\n})\n\n/* ─── Types ───────────────────────────────────────────────────── */\n\nexport interface ChatMessageProps\n extends Omit<React.ComponentProps<\"div\">, \"content\">,\n VariantProps<typeof chatMessageVariants> {\n /** Quem enviou: AI (assistente), user (aluno), system (mensagem do sistema) */\n persona: \"ai\" | \"user\" | \"system\"\n /** Texto da mensagem. Convencao do produto:\n * - Em `persona=\"ai\"` com `audioSrc`, o texto e o roteiro/legenda do audio TTS.\n * - Em `persona=\"user\"` com `audioSrc`, o texto e a transcricao do audio (gerada por IA).\n * Em ambos os casos, aparece em bubble separada acima do audio. */\n text?: string\n /** URL do audio — renderiza AudioPlayer dentro do bubble. Convencao do produto:\n * toda msg da IA tem `text` + `audioSrc` (TTS) e toda msg do user com audio tem\n * `text` (transcricao). Garante acessibilidade (leitura + escuta) e busca no historico. */\n audioSrc?: string\n /** Avatar do user (so usado em persona=\"user\") */\n avatar?: string\n /** Nome do user para fallback do avatar */\n author?: string\n /** Mostrar like/dislike inline (so faz sentido em persona=\"ai\") */\n showFeedback?: boolean\n /** Valor controlado do feedback */\n feedbackValue?: LikeDislikeValue\n /** Callback quando feedback muda */\n onFeedbackChange?: (value: LikeDislikeValue) => void\n /** Estado de loading: mostra typing indicator (\"...\") */\n loading?: boolean\n}\n\n/* ─── Helpers ─────────────────────────────────────────────────── */\n\nfunction getInitials(name?: string): string {\n if (!name) return \"\"\n return name\n .split(\" \")\n .slice(0, 2)\n .map((w) => w[0])\n .join(\"\")\n .toUpperCase()\n}\n\n/* ─── Typing indicator ────────────────────────────────────────── */\n\nfunction TypingDots() {\n return (\n <span\n className=\"inline-flex items-center gap-1\"\n aria-label=\"Digitando...\"\n role=\"status\"\n >\n <span className=\"size-1.5 rounded-full bg-current opacity-40 animate-bounce [animation-delay:-0.3s]\" />\n <span className=\"size-1.5 rounded-full bg-current opacity-40 animate-bounce [animation-delay:-0.15s]\" />\n <span className=\"size-1.5 rounded-full bg-current opacity-40 animate-bounce\" />\n </span>\n )\n}\n\n/* ─── ChatAudio (compact inline audio player com waveform real) ── */\n\n/** Fallback estatico (valores 0-1) — usado enquanto peaks reais carregam ou quando audio nao pode ser analisado (CORS/decode error) */\nconst STATIC_FALLBACK_PEAKS = [\n 2, 2, 2, 2, 8, 24, 18, 24, 8, 18, 24, 24, 18, 24, 18, 18, 24, 24, 32, 24,\n 24, 32, 24, 32, 32, 24, 18, 24, 24, 18, 24, 18, 18, 24, 24, 24, 24, 24, 32, 24,\n 32, 18, 24, 18, 24, 18, 24, 18, 18, 24, 24, 24, 24, 24, 32, 24, 32, 18, 24, 18,\n 24, 8, 18, 24, 8, 4, 2, 2, 2, 2, 4, 8, 24, 24, 18, 24, 18, 18, 24, 24,\n 32, 24, 24, 32, 24, 32, 32, 24, 18, 24, 24, 18, 24, 8, 18, 24, 8, 2, 2, 4,\n].map((h) => h / 32)\n\nconst MIN_BAR_HEIGHT = 2 // px (silencio)\nconst MAX_BAR_HEIGHT = 32 // px (pico maximo)\nconst SPEED_OPTIONS = [1, 1.25, 1.5, 2, 0.75]\n\n/** Cache modular para evitar re-computar peaks do mesmo audio em multiplas instancias */\nconst peaksCache = new Map<string, number[]>()\n\n/**\n * Calcula picos (amplitude maxima) de um audio dividido em N buckets.\n * Usa Web Audio API (decodeAudioData) — sem dependencias externas.\n * Retorna array de valores 0-1 (normalizado pelo pico maximo).\n */\nasync function calculatePeaks(audioUrl: string, barCount: number): Promise<number[]> {\n const response = await fetch(audioUrl)\n const arrayBuffer = await response.arrayBuffer()\n\n const AudioContextClass =\n window.AudioContext ||\n (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext\n const audioContext = new AudioContextClass()\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)\n\n const channelData = audioBuffer.getChannelData(0) // mono ou canal esquerdo\n const samplesPerBar = Math.floor(channelData.length / barCount)\n const peaks: number[] = []\n\n for (let i = 0; i < barCount; i++) {\n const start = i * samplesPerBar\n const end = Math.min(start + samplesPerBar, channelData.length)\n let max = 0\n for (let j = start; j < end; j++) {\n const absSample = Math.abs(channelData[j])\n if (absSample > max) max = absSample\n }\n peaks.push(max)\n }\n\n // Normaliza para 0-1 relativo ao pico maximo do clip\n const maxPeak = Math.max(...peaks, 0.01)\n\n // Fecha o AudioContext (libera recursos)\n void audioContext.close()\n\n return peaks.map((p) => p / maxPeak)\n}\n\n/** Wrapper com cache */\nasync function getPeaks(url: string, barCount: number): Promise<number[]> {\n const cacheKey = `${url}|${barCount}`\n const cached = peaksCache.get(cacheKey)\n if (cached) return cached\n const peaks = await calculatePeaks(url, barCount)\n peaksCache.set(cacheKey, peaks)\n return peaks\n}\n\nfunction formatAudioTime(seconds: number): string {\n if (!Number.isFinite(seconds) || seconds < 0) return \"00:00\"\n const m = Math.floor(seconds / 60)\n const s = Math.floor(seconds % 60)\n return `${m.toString().padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")}`\n}\n\nexport interface ChatAudioProps extends React.ComponentProps<\"div\"> {\n /** URL do audio */\n src: string\n /**\n * Variante visual:\n * - `neutral`: container `bg-muted` (cinza claro) — usado em AI messages\n * - `brand`: container `bg-accent` (rosa pastel) — usado em User messages\n * (requer wrapper `.theme-brand` no componente pai)\n */\n variant?: \"neutral\" | \"brand\"\n /**\n * Array de picos pre-calculados (valores 0-1). Quando fornecido, o componente\n * usa direto sem analisar o audio. Util para SSR, performance ou audios sem CORS.\n * Se nao fornecido, o componente analisa o audio automaticamente via Web Audio API.\n */\n peaks?: number[]\n /**\n * Numero de barras no waveform (default: 60).\n * Ignorado se `peaks` for fornecido (usa o length do array).\n * Reduzido de 100 → 60 para barras nao ficarem finas demais em containers menores.\n */\n barCount?: number\n}\n\nfunction ChatAudio({\n src,\n variant = \"neutral\",\n peaks: providedPeaks,\n barCount = 60,\n className,\n ...props\n}: ChatAudioProps) {\n const audioRef = React.useRef<HTMLAudioElement>(null)\n const [isPlaying, setIsPlaying] = React.useState(false)\n const [currentTime, setCurrentTime] = React.useState(0)\n const [duration, setDuration] = React.useState(0)\n const [speed, setSpeed] = React.useState(1)\n const [computedPeaks, setComputedPeaks] = React.useState<number[] | null>(null)\n\n /** Auto-computa peaks se nao foram fornecidos */\n React.useEffect(() => {\n if (providedPeaks) return // consumer ja forneceu, skip analise\n let cancelled = false\n getPeaks(src, barCount)\n .then((p) => {\n if (!cancelled) setComputedPeaks(p)\n })\n .catch((err) => {\n /* fallback silencioso: mantem null, usa fallback estatico.\n Causa comum: CORS (audio source nao tem Access-Control-Allow-Origin).\n Log apenas em dev para nao poluir prod. */\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(\n `[ChatAudio] Failed to analyze waveform for \"${src}\". ` +\n `Falling back to static waveform. ` +\n `Reason: ${err instanceof Error ? err.message : String(err)}. ` +\n `Audio source likely missing CORS headers (Access-Control-Allow-Origin).`\n )\n }\n })\n return () => {\n cancelled = true\n }\n }, [src, providedPeaks, barCount])\n\n /** Fallback estatico ajustado para o barCount atual (evita troca visual brusca quando peaks reais carregam) */\n const fallbackPeaks = React.useMemo(() => {\n return Array.from(\n { length: barCount },\n (_, i) => STATIC_FALLBACK_PEAKS[i % STATIC_FALLBACK_PEAKS.length]\n )\n }, [barCount])\n\n /** Resolve peaks: provided > computed > fallback (ajustado por barCount) */\n const peaks = providedPeaks ?? computedPeaks ?? fallbackPeaks\n\n /** Seta duracao apenas se valor for finito e positivo (evita Infinity/NaN) */\n const setDurationSafe = React.useCallback((value: number) => {\n if (Number.isFinite(value) && value > 0) {\n setDuration((prev) => (prev !== value ? value : prev))\n }\n }, [])\n\n /** Subscribe a multiplos eventos para garantir captura da duracao\n * (alguns audio sources nao disparam loadedmetadata, so durationchange/canplay) */\n React.useEffect(() => {\n const audio = audioRef.current\n if (!audio) return\n\n const handleDuration = () => setDurationSafe(audio.duration)\n handleDuration() // checa se ja esta carregado\n\n audio.addEventListener(\"loadedmetadata\", handleDuration)\n audio.addEventListener(\"durationchange\", handleDuration)\n audio.addEventListener(\"canplay\", handleDuration)\n return () => {\n audio.removeEventListener(\"loadedmetadata\", handleDuration)\n audio.removeEventListener(\"durationchange\", handleDuration)\n audio.removeEventListener(\"canplay\", handleDuration)\n }\n }, [src, setDurationSafe])\n\n const togglePlay = () => {\n const audio = audioRef.current\n if (!audio) return\n if (audio.paused) {\n audio.play().catch(() => {})\n } else {\n audio.pause()\n }\n }\n\n const cycleSpeed = () => {\n const idx = SPEED_OPTIONS.indexOf(speed)\n const next = SPEED_OPTIONS[(idx + 1) % SPEED_OPTIONS.length] ?? 1\n setSpeed(next)\n if (audioRef.current) audioRef.current.playbackRate = next\n }\n\n /* ─── Seek/drag via pointer no waveform ───────────────────── */\n\n const waveformRef = React.useRef<HTMLDivElement>(null)\n const isDraggingRef = React.useRef(false)\n\n /** Converte clientX (event) em time do audio, baseado na largura do container do waveform */\n const seekToClientX = React.useCallback(\n (clientX: number) => {\n const audio = audioRef.current\n const waveform = waveformRef.current\n if (!audio || !waveform || !duration) return\n const rect = waveform.getBoundingClientRect()\n const x = clientX - rect.left\n const newProgress = Math.max(0, Math.min(x / rect.width, 1))\n audio.currentTime = newProgress * duration\n },\n [duration]\n )\n\n const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n if (!duration) return\n isDraggingRef.current = true\n e.currentTarget.setPointerCapture(e.pointerId)\n seekToClientX(e.clientX)\n }\n\n const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n if (!isDraggingRef.current) return\n seekToClientX(e.clientX)\n }\n\n const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n if (!isDraggingRef.current) return\n isDraggingRef.current = false\n e.currentTarget.releasePointerCapture(e.pointerId)\n }\n\n const progress = duration > 0 ? currentTime / duration : 0\n const displayTime = formatAudioTime(currentTime)\n\n /* Waveform JSX — reusado entre layouts mobile (top) e desktop (inline).\n Barras com largura FIXA de 2px (matches Figma) distribuidas via justify-between\n para preencher toda a largura do container — gap entre barras se torna fluido\n conforme o waveform expande. Garante espacamento simetrico com time (esquerda)\n e speed badge (direita). Pointer events permitem clicar/arrastar para seek. */\n const waveform = (\n <div\n ref={waveformRef}\n className=\"flex-1 relative h-8 min-w-0 touch-none cursor-pointer select-none\"\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerCancel={handlePointerUp}\n role=\"slider\"\n aria-label=\"Posicao do audio\"\n aria-valuemin={0}\n aria-valuemax={Math.round(duration)}\n aria-valuenow={Math.round(currentTime)}\n >\n <div className=\"absolute inset-0 flex items-center justify-between\">\n {peaks.map((peak, i) => {\n const isPlayed = i / peaks.length <= progress\n const height = Math.max(\n MIN_BAR_HEIGHT,\n Math.round(peak * MAX_BAR_HEIGHT)\n )\n return (\n <div\n key={i}\n className={cn(\n \"w-[2px] shrink-0 rounded-full\",\n isPlayed ? \"bg-neutral-foreground\" : \"bg-neutral-ring\"\n )}\n style={{ height: `${height}px` }}\n />\n )\n })}\n </div>\n {/* Bolinha verde — posicionada em PORCENTAGEM da largura do container,\n alinhada com a distribuicao fluida das barras. */}\n {duration > 0 && (\n <div\n className=\"absolute top-1/2 z-10 -translate-y-1/2 -translate-x-1/2 size-3 rounded-full bg-[#098A5E] pointer-events-none shadow-md\"\n style={{ left: `${progress * 100}%` }}\n aria-hidden=\"true\"\n />\n )}\n </div>\n )\n\n /* Time display — reusado entre layouts */\n const timeDisplay = (\n <span className=\"shrink-0 text-sm text-neutral-ring tabular-nums\">\n {displayTime}\n </span>\n )\n\n /* Speed badge — reusado entre layouts */\n const speedBadge = (\n <button\n type=\"button\"\n onClick={cycleSpeed}\n className={cn(\n \"shrink-0 h-[26px] min-w-16 rounded-full text-sm font-medium px-2 transition-opacity hover:opacity-80\",\n variant === \"brand\"\n ? \"bg-muted text-muted-foreground\"\n : \"bg-background text-neutral-muted-foreground\"\n )}\n aria-label={`Velocidade ${speed}x. Clique para alternar.`}\n >\n {speed === 1 ? \"1x\" : `${speed}x`}\n </button>\n )\n\n return (\n <div\n data-slot=\"chat-audio\"\n className={cn(\n \"rounded-2xl w-full max-w-md flex flex-col gap-2 p-3 sm:flex-row sm:items-center sm:gap-2 sm:pl-5 sm:pr-3 sm:py-3\",\n variant === \"brand\" ? \"bg-accent\" : \"bg-muted\",\n className\n )}\n {...props}\n >\n <audio\n ref={audioRef}\n src={src}\n preload=\"metadata\"\n onPlay={() => setIsPlaying(true)}\n onPause={() => setIsPlaying(false)}\n onTimeUpdate={(e) => {\n setCurrentTime(e.currentTarget.currentTime)\n /* Fallback: alguns sources so expoem duration durante o playback */\n setDurationSafe(e.currentTarget.duration)\n }}\n onEnded={() => setIsPlaying(false)}\n />\n\n {/* Mobile top row + Desktop inline row */}\n <div className=\"flex items-center gap-2 flex-1 min-w-0\">\n {/* Play/Pause — icones FILLED */}\n <button\n type=\"button\"\n onClick={togglePlay}\n className=\"shrink-0 flex items-center justify-center text-neutral-foreground hover:opacity-80 transition-opacity\"\n aria-label={isPlaying ? \"Pausar\" : \"Reproduzir\"}\n >\n {isPlaying ? (\n <Pause className=\"size-4\" fill=\"currentColor\" strokeWidth={0} />\n ) : (\n <Play className=\"size-4\" fill=\"currentColor\" strokeWidth={0} />\n )}\n </button>\n\n {/* Time — apenas desktop (no mobile aparece embaixo) */}\n <div className=\"hidden sm:contents\">{timeDisplay}</div>\n\n {waveform}\n\n {/* Speed — apenas desktop (no mobile aparece embaixo) */}\n <div className=\"hidden sm:contents\">{speedBadge}</div>\n </div>\n\n {/* Mobile bottom row: time + speed badge */}\n <div className=\"flex items-center justify-between sm:hidden\">\n {timeDisplay}\n {speedBadge}\n </div>\n </div>\n )\n}\n\nexport { ChatAudio }\n\n/* ─── Component ───────────────────────────────────────────────── */\n\nfunction ChatMessage({\n persona,\n text,\n audioSrc,\n avatar,\n author,\n showFeedback = false,\n feedbackValue,\n onFeedbackChange,\n loading = false,\n className,\n ...props\n}: ChatMessageProps) {\n /* System: centered italic */\n if (persona === \"system\") {\n return (\n <div\n data-slot=\"chat-message\"\n data-persona=\"system\"\n className={cn(chatMessageVariants({ persona }), className)}\n {...props}\n >\n <span className=\"text-xs text-muted-foreground italic\">{text}</span>\n </div>\n )\n }\n\n /* AI: badge quadrado (esquerda, apenas desktop) + conteudo empilhado */\n if (persona === \"ai\") {\n return (\n <div\n data-slot=\"chat-message\"\n data-persona=\"ai\"\n className={cn(chatMessageVariants({ persona }), className)}\n {...props}\n >\n <div className=\"flex gap-3 max-w-[85%] sm:max-w-[75%] w-full\">\n {/* AI badge: oculto no mobile, visivel no desktop */}\n <div className=\"hidden sm:flex shrink-0 size-8 rounded-md bg-primary items-center justify-center theme-brand\">\n <span className=\"text-primary-foreground\">\n <Ai size=\"sm\" decorative />\n </span>\n </div>\n <div className=\"flex flex-col gap-2 min-w-0 flex-1\">\n {loading ? (\n /* Loading: dots dentro de bubble cinza (speech bubble shape — top-left reto em todos os breakpoints) */\n <div className=\"self-start inline-flex items-center rounded-2xl rounded-tl-none bg-muted p-5 text-muted-foreground\">\n <TypingDots />\n </div>\n ) : (\n <>\n {text && (\n /* Text bubble: bg-muted sempre, speech bubble shape (tl-none) em todos os breakpoints */\n <div className=\"rounded-2xl rounded-tl-none bg-muted p-5\">\n <p className=\"text-base leading-6 text-neutral-foreground break-words\">{text}</p>\n </div>\n )}\n {audioSrc && <ChatAudio src={audioSrc} variant=\"neutral\" />}\n {showFeedback && (\n <LikeDislike\n size=\"xs\"\n value={feedbackValue}\n onValueChange={onFeedbackChange}\n />\n )}\n </>\n )}\n </div>\n </div>\n </div>\n )\n }\n\n /* User: bubble (direita) + avatar (apenas desktop, alinhado ao topo) */\n return (\n <div\n data-slot=\"chat-message\"\n data-persona=\"user\"\n className={cn(chatMessageVariants({ persona }), className)}\n {...props}\n >\n <div className=\"flex items-start gap-3 max-w-[85%] sm:max-w-[75%] w-full justify-end\">\n <div className=\"flex flex-col gap-2 min-w-0 items-end theme-brand flex-1\">\n {loading ? (\n /* Loading: dots dentro de bubble pink (speech bubble shape — top-right reto em todos os breakpoints) */\n <div className=\"inline-flex items-center rounded-2xl rounded-tr-none bg-accent p-5 text-neutral-muted-foreground\">\n <TypingDots />\n </div>\n ) : (\n <>\n {text && (\n /* Text bubble: speech bubble shape (top-right reto) em todos os breakpoints.\n text-neutral-foreground = preto neutro fixo, nao muda com theme-brand */\n <div className=\"rounded-2xl rounded-tr-none bg-accent p-5\">\n <p className=\"text-base leading-6 text-neutral-foreground break-words\">{text}</p>\n </div>\n )}\n {audioSrc && <ChatAudio src={audioSrc} variant=\"brand\" />}\n </>\n )}\n </div>\n {/* Avatar: 40x40 rounded-lg, oculto no mobile, alinhado ao topo */}\n <Avatar size=\"lg\" className=\"hidden sm:flex shrink-0 rounded-lg\">\n {avatar && (\n <AvatarImage src={avatar} alt={author ?? \"User\"} className=\"rounded-lg\" />\n )}\n <AvatarFallback className=\"rounded-lg bg-muted text-muted-foreground text-sm\">\n {author ? (\n getInitials(author)\n ) : (\n <CycleIcon icon={User} size=\"sm\" decorative />\n )}\n </AvatarFallback>\n </Avatar>\n </div>\n </div>\n )\n}\n\nexport { ChatMessage, chatMessageVariants }\n"]}
@@ -10,6 +10,13 @@ function formatDuration(seconds) {
10
10
  const s = Math.floor(seconds % 60);
11
11
  return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
12
12
  }
13
+ function getWarningState(elapsed, max, warnAt) {
14
+ if (!max || max <= 0) return { isWarning: false, secondsLeft: Infinity };
15
+ const secondsLeft = Math.max(0, max - elapsed);
16
+ return { isWarning: secondsLeft <= warnAt, secondsLeft };
17
+ }
18
+ var DEFAULT_WARN_AT_SECONDS_LEFT = 10;
19
+ var DEFAULT_SECONDS_LEFT_LABEL = (s) => `${s}s restantes`;
13
20
  function MicButton({
14
21
  onClick,
15
22
  onPointerDown,
@@ -337,7 +344,10 @@ function PressedRecordingOverlay({
337
344
  deltaX,
338
345
  deltaY,
339
346
  lockThreshold,
340
- cancelThreshold
347
+ cancelThreshold,
348
+ isWarning,
349
+ secondsLeft,
350
+ secondsLeftLabel
341
351
  }) {
342
352
  const aboutToLock = deltaY <= -lockThreshold;
343
353
  const aboutToCancel = deltaX <= -cancelThreshold;
@@ -405,9 +415,28 @@ function PressedRecordingOverlay({
405
415
  }
406
416
  )
407
417
  ] }),
408
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-full bg-muted h-7 px-3", children: [
409
- /* @__PURE__ */ jsx(RecordingIndicator, {}),
410
- /* @__PURE__ */ jsx("span", { className: "text-sm tabular-nums text-neutral-foreground", children: formatDuration(duration) })
418
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-0.5", children: [
419
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-full bg-muted h-7 px-3", children: [
420
+ /* @__PURE__ */ jsx(RecordingIndicator, {}),
421
+ /* @__PURE__ */ jsx(
422
+ "span",
423
+ {
424
+ className: cn(
425
+ "text-sm tabular-nums",
426
+ isWarning ? "text-destructive font-medium" : "text-neutral-foreground"
427
+ ),
428
+ children: formatDuration(duration)
429
+ }
430
+ )
431
+ ] }),
432
+ isWarning && /* @__PURE__ */ jsx(
433
+ "span",
434
+ {
435
+ className: "text-xs text-destructive tabular-nums",
436
+ "aria-live": "polite",
437
+ children: secondsLeftLabel(secondsLeft)
438
+ }
439
+ )
411
440
  ] })
412
441
  ] }) : (
413
442
  // Variant default: cancel pill flex-1 + mic
@@ -420,9 +449,28 @@ function PressedRecordingOverlay({
420
449
  aboutToCancel && "bg-destructive/10"
421
450
  ),
422
451
  children: [
423
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
424
- /* @__PURE__ */ jsx(RecordingIndicator, {}),
425
- /* @__PURE__ */ jsx("span", { className: "text-sm tabular-nums text-neutral-foreground", children: formatDuration(duration) })
452
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start gap-0.5", children: [
453
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
454
+ /* @__PURE__ */ jsx(RecordingIndicator, {}),
455
+ /* @__PURE__ */ jsx(
456
+ "span",
457
+ {
458
+ className: cn(
459
+ "text-sm tabular-nums",
460
+ isWarning ? "text-destructive font-medium" : "text-neutral-foreground"
461
+ ),
462
+ children: formatDuration(duration)
463
+ }
464
+ )
465
+ ] }),
466
+ isWarning && /* @__PURE__ */ jsx(
467
+ "span",
468
+ {
469
+ className: "text-xs text-destructive tabular-nums",
470
+ "aria-live": "polite",
471
+ children: secondsLeftLabel(secondsLeft)
472
+ }
473
+ )
426
474
  ] }),
427
475
  /* @__PURE__ */ jsxs(
428
476
  "div",
@@ -489,6 +537,10 @@ function MessageBar(_a) {
489
537
  playbackProgress = 0,
490
538
  onSeekPlayback,
491
539
  placeholder = "Digite sua mensagem...",
540
+ maxRecordingDuration,
541
+ warnAtSecondsLeft = DEFAULT_WARN_AT_SECONDS_LEFT,
542
+ onMaxDurationReached,
543
+ secondsLeftLabel = DEFAULT_SECONDS_LEFT_LABEL,
492
544
  className
493
545
  } = _b, props = __objRest(_b, [
494
546
  "state",
@@ -507,8 +559,24 @@ function MessageBar(_a) {
507
559
  "playbackProgress",
508
560
  "onSeekPlayback",
509
561
  "placeholder",
562
+ "maxRecordingDuration",
563
+ "warnAtSecondsLeft",
564
+ "onMaxDurationReached",
565
+ "secondsLeftLabel",
510
566
  "className"
511
567
  ]);
568
+ const { isWarning: isDurationWarning, secondsLeft: durationSecondsLeft } = getWarningState(recordingDuration, maxRecordingDuration, warnAtSecondsLeft);
569
+ const maxReachedRef = React.useRef(false);
570
+ React.useEffect(() => {
571
+ if (!maxRecordingDuration || maxReachedRef.current) return;
572
+ if (recordingDuration >= maxRecordingDuration) {
573
+ maxReachedRef.current = true;
574
+ onMaxDurationReached == null ? void 0 : onMaxDurationReached();
575
+ }
576
+ }, [recordingDuration, maxRecordingDuration, onMaxDurationReached]);
577
+ React.useEffect(() => {
578
+ if (state !== "recording") maxReachedRef.current = false;
579
+ }, [state]);
512
580
  const samplesRef = React.useRef([]);
513
581
  const prevStateRef = React.useRef(state);
514
582
  const lastBaseStateRef = React.useRef(
@@ -654,7 +722,10 @@ function MessageBar(_a) {
654
722
  deltaX: pressDelta.x,
655
723
  deltaY: pressDelta.y,
656
724
  lockThreshold: LOCK_THRESHOLD,
657
- cancelThreshold: pressCancelThreshold
725
+ cancelThreshold: pressCancelThreshold,
726
+ isWarning: isDurationWarning,
727
+ secondsLeft: durationSecondsLeft,
728
+ secondsLeftLabel
658
729
  }
659
730
  );
660
731
  }
@@ -681,7 +752,26 @@ function MessageBar(_a) {
681
752
  ) }),
682
753
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 flex-1 rounded-2xl bg-muted px-4 py-3", children: [
683
754
  /* @__PURE__ */ jsx(RecordingIndicator, {}),
684
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm text-neutral-foreground tabular-nums", children: formatDuration(recordingDuration) }),
755
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start gap-0.5 shrink-0", children: [
756
+ /* @__PURE__ */ jsx(
757
+ "span",
758
+ {
759
+ className: cn(
760
+ "text-sm tabular-nums",
761
+ isDurationWarning ? "text-destructive font-medium" : "text-neutral-foreground"
762
+ ),
763
+ children: formatDuration(recordingDuration)
764
+ }
765
+ ),
766
+ isDurationWarning && /* @__PURE__ */ jsx(
767
+ "span",
768
+ {
769
+ className: "text-xs text-destructive tabular-nums whitespace-nowrap",
770
+ "aria-live": "polite",
771
+ children: secondsLeftLabel(durationSecondsLeft)
772
+ }
773
+ )
774
+ ] }),
685
775
  /* @__PURE__ */ jsx(LiveWaveform, { stream: recordingStream, samplesRef }),
686
776
  /* @__PURE__ */ jsx("div", { className: "hidden sm:contents", children: /* @__PURE__ */ jsx(
687
777
  "button",
@@ -842,5 +932,5 @@ function MessageBar(_a) {
842
932
  }
843
933
 
844
934
  export { MessageBar };
845
- //# sourceMappingURL=chunk-27PO7X4G.js.map
846
- //# sourceMappingURL=chunk-27PO7X4G.js.map
935
+ //# sourceMappingURL=chunk-CWMXYPWK.js.map
936
+ //# sourceMappingURL=chunk-CWMXYPWK.js.map