Design
llms.txt digitalcrew.tech

Unibox

ReplySuggestions

The band between a Unibox thread and its composer: ask for a few replies, watch them arrive, pick one.

MaxUniboxDigitalCrew.ReplySuggestions
ReplySuggestions · live · 1100px wideOpen

Guidelines#

A pick fills the composer and is never sent for you.

Use it for suggesting the next message in a conversation, aimed at the workspace's deal stages.

What you provide

  • State: suggestions, isLoading, error and hasGenerated.
    • Each suggestion is { id, label, intent, deal_target, rationale, text }.
    • intent is qualify, value, meeting, proposal, pricing, objection, close or nurture, and picks the icon.
  • usedProspectCard and dealTypeCount set the source note, such as "From their contact card and your 2 deal types".
  • Callbacks: onGenerate, onDismiss and onSelect(text).
  • Optional: composerValue (marks the card in play), disabled and className.

Anatomy

  • The trigger pill is rounded-full border px-3 py-1.5 text-xs. It is tinted primary before a run ("Suggest replies"), neutral after it ("Suggest again"), and reads "Writing options…" while loading.
  • Cards sit in 1, 2 (sm) or 3 (lg) columns. Each is rounded-xl border-border/60 bg-background/70 p-3 shadow-sm, with an intent icon, a text-xs font-semibold label, the deal target, three clamped lines and an italic rationale.
  • Hover gives border-primary/40. The card in play gets border-primary/60 bg-primary/[0.07] and a check.
  • While loading, three pulsing h-24 skeletons stand in.

Motion: panels swap on FADE_SWAP (0.16s). Cards rise 8px on SPRING_ENTER, staggered 0.05s, except under reduced motion.

Don't auto-send a suggestion or hide its source. The last word stays with the person whose name is on the account.

Facts#

GroupUnibox
ProductMax
Globalwindow.DigitalCrew.ReplySuggestions
Sourcemax-agent: src/features/unibox/ui/components/reply-suggestions.tsx
Import in the appimport { ReplySuggestions } from "@/features/unibox/ui/components/reply-suggestions";

API#

TypeScript declarations emitted from src/features/unibox/ui/components/reply-suggestions.tsx.

interface ReplySuggestionsProps {
    suggestions: ReplySuggestion[];
    isLoading: boolean;
    error: string | null;
    /** A run finished for this conversation (even if it came back empty). */
    hasGenerated: boolean;
    /** The contact card fed the last run, not just the thread. */
    usedProspectCard: boolean;
    /** How many deal types the options could aim at. */
    dealTypeCount: number;
    onGenerate: () => void;
    onDismiss: () => void;
    /** Put this text in the composer, ready to edit and send. */
    onSelect: (text: string) => void;
    /**
     * What the composer holds right now. A card matching it is marked as the
     * one in play — and stops matching the moment the user edits a word, which
     * is exactly when it stops being that suggestion.
     */
    composerValue?: string;
    /** Sending locks the panel: the reply is already on its way. */
    disabled?: boolean;
    className?: string;
}
export declare function ReplySuggestions({ suggestions, isLoading, error, hasGenerated, usedProspectCard, dealTypeCount, onGenerate, onDismiss, onSelect, composerValue, disabled, className, }: ReplySuggestionsProps): JSX.Element;

Example#

The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.

<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  var SUGGESTIONS = [
    { id: "s1", label: "Book the call", intent: "meeting", deal_target: "New business → Meeting booked",
      rationale: "She named a day; lock it before the week fills.",
      text: "Great, Thursday at 10:00 CET it is. I'll send the invite to you and Anouk now, with a 20-minute agenda: your current close process, where the two days go, and what a pilot would look like." },
    { id: "s2", label: "Share the ROI case", intent: "value", deal_target: "New business → Qualified",
      rationale: "A CFO buys on numbers; give her one to bring.",
      text: "Before Thursday, here is how Brightline Logistics cut month-end close from six days to three with us. One page, numbers included. Happy to walk through what the same model would mean for Northwind." },
    { id: "s3", label: "Qualify the buying group", intent: "qualify", deal_target: "New business → Discovery",
      rationale: "A Q1 review means the budget is being set now.",
      text: "Thanks Claire. So I bring the right material: is the Q1 tooling review owned by finance alone, or will IT and procurement join Thursday's call too?" }
  ];
  function App() {
    var sg = React.useState(SUGGESTIONS), list = sg[0], setList = sg[1];
    var ld = React.useState(false), loading = ld[0], setLoading = ld[1];
    var gen = React.useState(true), generated = gen[0], setGenerated = gen[1];
    var cv = React.useState(SUGGESTIONS[0].text), value = cv[0], setValue = cv[1];
    return h("div", { className: "mx-auto flex max-w-[900px] flex-col gap-3" },
      h(DC.ReplySuggestions, {
        suggestions: list, isLoading: loading, error: null, hasGenerated: generated,
        usedProspectCard: true, dealTypeCount: 2, composerValue: value,
        onGenerate: function () {
          setLoading(true); setList([]);
          setTimeout(function () { setLoading(false); setGenerated(true); setList(SUGGESTIONS); }, 1600);
        },
        onDismiss: function () { setList([]); setGenerated(false); },
        onSelect: function (text) { setValue(text); }
      }),
      h(DC.MessageComposer, {
        value: value, onChange: setValue, attachments: [], isSending: false,
        onAddFiles: function () {}, onRemoveAttachment: function () {}, onSubmit: function () {}
      }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>