import { useState } from "react"; import { Avatar, AvatarFallback, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@godxjp/ui/data-display"; import { ChatComposer, ChatSuggestion, type ChatSuggestionItemProp, } from "@godxjp/ui/data-entry"; import { Text } from "@godxjp/ui/general"; import { AppShell, Flex, PageContainer, Sidebar, type SidebarSectionProp, Topbar, } from "@godxjp/ui/layout"; import { AtSign, Bot, Hash, MessageSquare, Settings, Slash, Users } from "lucide-react"; /** * ChatSuggestion — the trigger-character list that completes a draft in place. * * It owns the part `Command` does not: watching a TEXTAREA for a trigger character at the caret, * tracking the query as the caret moves, and closing on Escape, on blur and on a word break — * while the list itself stays the real `Command` inside a real `Popover`, so the listbox ARIA is * cmdk's and not hand-rolled. * * Every card here is one axis at rest, so the whole API is visible without clicking: * `triggerCharacter`, one level of `children`, a disabled row, an empty result, and the * open/defaultOpen/onOpenChange triad. `ChatComposer`'s own page shows it wired into a live * transcript; this page is about the completion behaviour alone. * * Composed only from real @godxjp/ui components. */ const sections: SidebarSectionProp[] = [ { label: "アシスタント", items: [ { id: "chat", label: "チャット", icon: MessageSquare }, { id: "agents", label: "エージェント", icon: Bot }, { id: "members", label: "メンバー", icon: Users }, ], }, { label: "管理", items: [{ id: "settings", label: "設定", icon: Settings }] }, ]; /** Slash commands — the `template` row carries one level of `children`. */ const COMMANDS: ChatSuggestionItemProp[] = [ { value: "summarize", label: "要約する", description: "このスレッドを3行で要約します" }, { value: "translate", label: "翻訳する", description: "英語 ⇄ 日本語" }, { value: "explain", label: "詳しく説明する" }, { value: "archive", label: "アーカイブする", description: "権限がないため選択できません", disabled: true, }, { value: "template", label: "テンプレート", description: "定型文を挿入します", children: [ { value: "minutes", label: "議事録" }, { value: "weekly", label: "週報" }, ], }, ]; const MEMBERS: ChatSuggestionItemProp[] = [ { value: "sato", label: "佐藤 花子", description: "経理" }, { value: "tanaka", label: "田中 太郎", description: "開発" }, { value: "nguyen", label: "Nguyễn Minh", description: "Design" }, ]; const CHANNELS: ChatSuggestionItemProp[] = [ { value: "general", label: "general", description: "全社アナウンス" }, { value: "accounting", label: "accounting", description: "経理チーム" }, ]; export default function Demo() { const [slashDraft, setSlashDraft] = useState(""); const [mentionDraft, setMentionDraft] = useState(""); const [channelDraft, setChannelDraft] = useState(""); const [emptyDraft, setEmptyDraft] = useState(""); const [controlledDraft, setControlledDraft] = useState(""); const [controlledOpen, setControlledOpen] = useState(false); return ( {}} product={{ name: "CoreDesk", role: "アシスタント", color: "hsl(var(--primary))" }} /> } topbar={ C } /> } > {/* ── 1. The default trigger, and one level of children ────────────────────────── */} 「/」を入力すると開きます。「テンプレート」は子を持つので、Enter はまだ確定せず一段深く入ります。Escape は閉じるだけで、入力済みの文字はそのまま残ります。 setSlashDraft(`/${value} `)} > {({ onTrigger, onKeyDown }) => ( { setSlashDraft(next); onTrigger(next); }} onKeyDown={onKeyDown} onSubmit={() => setSlashDraft("")} placeholder="「/」でコマンド一覧" /> )} {/* ── 2. triggerCharacter="@" ──────────────────────────────────────────────────── */} トリガー文字は差し替えられます。1つの入力欄に対して1つの ChatSuggestion。2種類を同時に見張るのではなく、用途ごとに別の欄にします。 setMentionDraft(`@${value} `)} > {({ onTrigger, onKeyDown }) => ( { setMentionDraft(next); onTrigger(next); }} onKeyDown={onKeyDown} onSubmit={() => setMentionDraft("")} placeholder="「@」でメンバー一覧" /> )} {/* ── 3. triggerCharacter="#" ──────────────────────────────────────────────────── */} チャンネル参照。項目が少なくても扱いは同じです。 setChannelDraft(`#${value} `)} > {({ onTrigger, onKeyDown }) => ( { setChannelDraft(next); onTrigger(next); }} onKeyDown={onKeyDown} onSubmit={() => setChannelDraft("")} placeholder="「#」でチャンネル一覧" size="sm" /> )} {/* ── 4. No items — the empty state is the component's, not the caller's ───────── */} 該当なし 候補が0件でもパネルは開き、件数は読み上げ用の status で通知されます。空の list を黙って閉じると、利用者には「効かない」ように見えます。 {}}> {({ onTrigger, onKeyDown }) => ( { setEmptyDraft(next); onTrigger(next); }} onKeyDown={onKeyDown} onSubmit={() => setEmptyDraft("")} placeholder="「/」を押しても候補はありません" /> )} {/* ── 5. open / onOpenChange — the controlled triad ────────────────────────────── */} open · onOpenChange 開閉も制御できます。下の行は現在の状態をそのまま映したもので、 トリガー文字・Escape・語の区切りのいずれでも同じ値が動きます。 setControlledDraft(`/${value} `)} > {({ onTrigger, onKeyDown }) => ( { setControlledDraft(next); onTrigger(next); }} onKeyDown={onKeyDown} onSubmit={() => setControlledDraft("")} placeholder="「/」で開き、Escape で閉じます" /> )} open = {controlledOpen ? "true" : "false"} ); }