@fluencypassdevs/cycle 1.7.0 → 1.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/bin/mcp.mjs +70 -0
- package/dist/chunk-37C2K2NM.js +101 -0
- package/dist/chunk-37C2K2NM.js.map +1 -0
- package/dist/chunk-PW464XEP.js +787 -0
- package/dist/chunk-PW464XEP.js.map +1 -0
- package/dist/chunk-YMWRR7ET.js +503 -0
- package/dist/chunk-YMWRR7ET.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +11 -8
- package/dist/ui/chat-message.d.ts +57 -0
- package/dist/ui/chat-message.js +11 -0
- package/dist/ui/chat-message.js.map +1 -0
- package/dist/ui/message-bar.d.ts +69 -0
- package/dist/ui/message-bar.js +5 -0
- package/dist/ui/message-bar.js.map +1 -0
- package/dist/ui/message-rating.d.ts +29 -0
- package/dist/ui/message-rating.js +5 -0
- package/dist/ui/message-rating.js.map +1 -0
- package/package.json +1 -1
package/bin/mcp.mjs
CHANGED
|
@@ -408,6 +408,76 @@ const COMPONENTS = [
|
|
|
408
408
|
example: `<ChatPanel messages={messages} onSend={handleSend} currentUser="aluno@email.com" />`,
|
|
409
409
|
keywords: ["chat", "painel", "conversa", "mensagens", "live", "tempo real"],
|
|
410
410
|
},
|
|
411
|
+
{
|
|
412
|
+
name: "ChatMessage",
|
|
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.",
|
|
415
|
+
props: [
|
|
416
|
+
{ name: "persona", type: '"ai" | "user" | "system"', default: "-" },
|
|
417
|
+
{ name: "text", type: "string", default: "-" },
|
|
418
|
+
{ name: "audioSrc", type: "string", default: "-" },
|
|
419
|
+
{ name: "avatar", type: "string", default: "-" },
|
|
420
|
+
{ name: "author", type: "string", default: "-" },
|
|
421
|
+
{ name: "showFeedback", type: "boolean", default: "false" },
|
|
422
|
+
{ name: "feedbackValue", type: '"like" | "dislike" | null', default: "-" },
|
|
423
|
+
{ name: "onFeedbackChange", type: "(val) => void", default: "-" },
|
|
424
|
+
{ name: "loading", type: "boolean", default: "false" },
|
|
425
|
+
],
|
|
426
|
+
example: `<ChatMessage persona="ai" text="Como posso ajudar?" />
|
|
427
|
+
<ChatMessage persona="ai" text="Vou responder..." audioSrc="/audio.mp3" showFeedback />
|
|
428
|
+
<ChatMessage persona="user" text="Oi" avatar="/me.jpg" author="Felipe" />
|
|
429
|
+
<ChatMessage persona="ai" loading />
|
|
430
|
+
<ChatMessage persona="system" text="Conversa iniciada" />`,
|
|
431
|
+
keywords: ["chat", "ai", "ia", "mensagem", "conversa", "message", "assistant", "bot"],
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: "MessageRating",
|
|
435
|
+
import: `import { MessageRating } from "@fluencypassdevs/cycle"`,
|
|
436
|
+
description: "Rating de 5 niveis com emoji usado ao final de uma conversa. Niveis: Disappointing/Frustrating/Ok/Helpful/Loved it. Layout horizontal (desktop) ou vertical (mobile). Labels customizaveis para i18n.",
|
|
437
|
+
props: [
|
|
438
|
+
{ name: "value", type: "1 | 2 | 3 | 4 | 5 | null", default: "-" },
|
|
439
|
+
{ name: "defaultValue", type: "1 | 2 | 3 | 4 | 5 | null", default: "-" },
|
|
440
|
+
{ name: "onChange", type: "(val) => void", default: "-" },
|
|
441
|
+
{ name: "layout", type: '"horizontal" | "vertical" | "auto"', default: '"auto"' },
|
|
442
|
+
{ name: "labels", type: "Partial<MessageRatingLabels>", default: "-" },
|
|
443
|
+
{ name: "disabled", type: "boolean", default: "false" },
|
|
444
|
+
{ name: "title", type: "string", default: "-" },
|
|
445
|
+
],
|
|
446
|
+
example: `<MessageRating onChange={(v) => save(v)} title="Como foi sua experiencia?" />
|
|
447
|
+
<MessageRating value={3} disabled />`,
|
|
448
|
+
keywords: ["rating", "emoji", "avaliacao", "feedback", "experiencia", "score", "satisfacao"],
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "MessageBar",
|
|
452
|
+
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.",
|
|
454
|
+
props: [
|
|
455
|
+
{ name: "state", type: '"default" | "audio-only" | "active" | "disabled" | "recording" | "paused"', default: '"default"' },
|
|
456
|
+
{ name: "value", type: "string", default: '""' },
|
|
457
|
+
{ name: "onChange", type: "(val) => void", default: "-" },
|
|
458
|
+
{ name: "onSendText", type: "(text) => void", default: "-" },
|
|
459
|
+
{ name: "onStartRecording", type: "() => void", default: "-" },
|
|
460
|
+
{ name: "onPauseRecording", type: "() => void", default: "-" },
|
|
461
|
+
{ name: "onResumeRecording", type: "() => void", default: "-" },
|
|
462
|
+
{ name: "onCancelRecording", type: "() => void", default: "-" },
|
|
463
|
+
{ name: "onSendAudio", type: "() => void", default: "-" },
|
|
464
|
+
{ name: "onTogglePlay", type: "() => void", default: "-" },
|
|
465
|
+
{ name: "recordingDuration", type: "number", default: "0" },
|
|
466
|
+
{ name: "isPlaying", type: "boolean", default: "false" },
|
|
467
|
+
{ name: "recordingStream", type: "MediaStream | null", default: "null", note: "passe o MESMO stream do MediaRecorder; o componente analisa em tempo real para renderizar o live waveform" },
|
|
468
|
+
{ name: "playbackProgress", type: "number (0-1)", default: "0", note: "consumer atualiza via audio.ontimeupdate enquanto toca o preview no estado paused" },
|
|
469
|
+
{ name: "onSeekPlayback", type: "(progress) => void", default: "-", note: "consumer seta audio.currentTime = progress * duration" },
|
|
470
|
+
{ name: "placeholder", type: "string", default: '"Digite sua mensagem..."' },
|
|
471
|
+
],
|
|
472
|
+
example: `// 1) Captura: passa o MESMO stream pro MessageBar (live waveform).
|
|
473
|
+
// 2) Pause: recorder.stop() gera o Blob; crie um <audio> e atualize playbackProgress via ontimeupdate.
|
|
474
|
+
// 3) Cleanup: revokeObjectURL ao cancelar/enviar.
|
|
475
|
+
|
|
476
|
+
<MessageBar value={text} onChange={setText} onSendText={(t) => send(t)} onStartRecording={() => startRec()} />
|
|
477
|
+
<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} />`,
|
|
479
|
+
keywords: ["chat", "input", "message", "bar", "audio", "recording", "microphone", "mensagem", "envio", "press-to-record", "waveform", "playback", "whatsapp"],
|
|
480
|
+
},
|
|
411
481
|
{
|
|
412
482
|
name: "LikeDislike",
|
|
413
483
|
import: `import { LikeDislike } from "@fluencypassdevs/cycle"`,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { cn } from './chunk-TYCPXAXF.js';
|
|
2
|
+
import { __objRest, __spreadValues, __spreadProps } from './chunk-YINJ5YZ5.js';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var DEFAULT_LABELS = {
|
|
7
|
+
1: { emoji: "\u{1F621}", title: "Disappointing", subtitle: "Decepcionante" },
|
|
8
|
+
2: { emoji: "\u{1F61E}", title: "Frustrating", subtitle: "Frustrante" },
|
|
9
|
+
3: { emoji: "\u{1F610}", title: "Ok", subtitle: "Tudo bem" },
|
|
10
|
+
4: { emoji: "\u{1F642}", title: "Helpful", subtitle: "Ajudou" },
|
|
11
|
+
5: { emoji: "\u{1F60D}", title: "Loved it", subtitle: "Amei" }
|
|
12
|
+
};
|
|
13
|
+
var VALUES = [1, 2, 3, 4, 5];
|
|
14
|
+
function MessageRating(_a) {
|
|
15
|
+
var _b = _a, {
|
|
16
|
+
value: controlledValue,
|
|
17
|
+
defaultValue,
|
|
18
|
+
onChange,
|
|
19
|
+
layout = "auto",
|
|
20
|
+
labels: labelOverrides,
|
|
21
|
+
disabled = false,
|
|
22
|
+
title,
|
|
23
|
+
className
|
|
24
|
+
} = _b, props = __objRest(_b, [
|
|
25
|
+
"value",
|
|
26
|
+
"defaultValue",
|
|
27
|
+
"onChange",
|
|
28
|
+
"layout",
|
|
29
|
+
"labels",
|
|
30
|
+
"disabled",
|
|
31
|
+
"title",
|
|
32
|
+
"className"
|
|
33
|
+
]);
|
|
34
|
+
const [internalValue, setInternalValue] = React.useState(
|
|
35
|
+
defaultValue != null ? defaultValue : null
|
|
36
|
+
);
|
|
37
|
+
const isControlled = controlledValue !== void 0;
|
|
38
|
+
const value = isControlled ? controlledValue : internalValue;
|
|
39
|
+
const labels = React.useMemo(
|
|
40
|
+
() => __spreadValues(__spreadValues({}, DEFAULT_LABELS), labelOverrides),
|
|
41
|
+
[labelOverrides]
|
|
42
|
+
);
|
|
43
|
+
const handleSelect = (v) => {
|
|
44
|
+
if (disabled) return;
|
|
45
|
+
if (!isControlled) setInternalValue(v);
|
|
46
|
+
onChange == null ? void 0 : onChange(v);
|
|
47
|
+
};
|
|
48
|
+
const layoutClass = layout === "vertical" ? "flex-col" : "flex-row";
|
|
49
|
+
return /* @__PURE__ */ jsxs(
|
|
50
|
+
"div",
|
|
51
|
+
__spreadProps(__spreadValues({
|
|
52
|
+
"data-slot": "message-rating",
|
|
53
|
+
role: "radiogroup",
|
|
54
|
+
"aria-label": title != null ? title : "Avalie sua experiencia",
|
|
55
|
+
className: cn("flex flex-col gap-3", className)
|
|
56
|
+
}, props), {
|
|
57
|
+
children: [
|
|
58
|
+
title && /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: title }),
|
|
59
|
+
/* @__PURE__ */ jsx("div", { className: cn("flex gap-2", layoutClass), children: VALUES.map((v) => {
|
|
60
|
+
const label = labels[v];
|
|
61
|
+
const selected = value === v;
|
|
62
|
+
return /* @__PURE__ */ jsxs(
|
|
63
|
+
"button",
|
|
64
|
+
{
|
|
65
|
+
type: "button",
|
|
66
|
+
role: "radio",
|
|
67
|
+
"aria-checked": selected,
|
|
68
|
+
"aria-label": `${label.title} \u2014 ${label.subtitle}`,
|
|
69
|
+
disabled,
|
|
70
|
+
onClick: () => handleSelect(v),
|
|
71
|
+
className: cn(
|
|
72
|
+
/* Base — shared mobile/desktop */
|
|
73
|
+
"flex items-center transition-[color,background-color,border-color,box-shadow] outline-none",
|
|
74
|
+
"hover:opacity-90 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
75
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
76
|
+
/* Mobile: compacto, justa o emoji centralizado em pill cinza */
|
|
77
|
+
"rounded-xl px-3 py-2 justify-center",
|
|
78
|
+
/* Desktop: card completo com texto, flex-1, border */
|
|
79
|
+
"sm:flex-1 sm:gap-3 sm:px-3 sm:py-2.5 sm:text-left sm:justify-start sm:border",
|
|
80
|
+
/* Selected vs unselected (estados diferentes por breakpoint) */
|
|
81
|
+
selected ? "bg-accent theme-brand sm:border-primary sm:shadow-xs" : "bg-muted sm:bg-transparent sm:border-neutral-border"
|
|
82
|
+
),
|
|
83
|
+
children: [
|
|
84
|
+
/* @__PURE__ */ jsx("span", { className: "text-base sm:text-2xl leading-none shrink-0", "aria-hidden": "true", children: label.emoji }),
|
|
85
|
+
/* @__PURE__ */ jsxs("span", { className: "hidden sm:flex sm:flex-col min-w-0", children: [
|
|
86
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium leading-tight text-foreground truncate", children: label.title }),
|
|
87
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground leading-tight truncate", children: label.subtitle })
|
|
88
|
+
] })
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
v
|
|
92
|
+
);
|
|
93
|
+
}) })
|
|
94
|
+
]
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { MessageRating };
|
|
100
|
+
//# sourceMappingURL=chunk-37C2K2NM.js.map
|
|
101
|
+
//# sourceMappingURL=chunk-37C2K2NM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/ui/message-rating.tsx"],"names":[],"mappings":"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","file":"chunk-37C2K2NM.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\nconst DEFAULT_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\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"]}
|