diff --git a/apps/internal-ui/src/features/chat/ChatPage.tsx b/apps/internal-ui/src/features/chat/ChatPage.tsx index c36efe5..0716358 100644 --- a/apps/internal-ui/src/features/chat/ChatPage.tsx +++ b/apps/internal-ui/src/features/chat/ChatPage.tsx @@ -1,6 +1,7 @@ import { useState, type ReactNode } from "react"; import { ChatMobileHeader } from "../../layout/ChatMobileHeader"; +import { Hint } from "../../shared/Hint"; import { ConversationWorkspace, type DialogScope } from "../conversations/ConversationWorkspace"; import { DialogControls } from "../conversations/DialogControls"; @@ -51,6 +52,9 @@ export function ChatPage({ counters={counters} showScopeSwitcher={showScopeSwitcher} mobileHeader={} + hint={showScopeSwitcher + ? Вы видите все диалоги организации. Сотрудники видят только диалоги своих групп, без группы и те, где они ответственные. + : undefined} renderContextPanel={({ dialog, detail, applyConversation }) => ( void }) => ReactNode; mobileHeader?: ReactNode; + hint?: ReactNode; initialConversationId?: number | null; // Охват (дерево фильтров) живёт в Shell: у сотрудника им управляет сайдбар, // у менеджера — поповер в заголовке списка. @@ -198,6 +199,7 @@ export function ConversationWorkspace({ isOwner = false, listTitle, searchPlaceh setMobileDialogOpen(true); }} mobileHeader={mobileHeader} + hint={hint} />
void callController.start(kind)} onClose={onClose} onSpam={onSpam} onReturnQueue={onReturnQueue} onArchive={onArchive} onToggleContext={() => setCtxOpen((open) => !open)} onMobileBack={() => setMobileDialogOpen(false)} /> diff --git a/apps/internal-ui/src/features/conversations/DialogList.tsx b/apps/internal-ui/src/features/conversations/DialogList.tsx index 0ad4fe9..546ff8f 100644 --- a/apps/internal-ui/src/features/conversations/DialogList.tsx +++ b/apps/internal-ui/src/features/conversations/DialogList.tsx @@ -10,7 +10,7 @@ import type { ConversationCounters } from "./model"; import type { ConversationListItem, ListTab } from "./types"; import { SearchInput } from "../../shared/ui-controls"; -export function DialogList({ title = "Диалоги", searchPlaceholder = "Поиск по клиенту, продукту…", scope, counters, setScope, showScopeSwitcher = true, mobileHeader, dialogs, filtered, listTab, selectedId, search, errorText, setSearch, setListTab, setSelectedId }: { +export function DialogList({ title = "Диалоги", searchPlaceholder = "Поиск по клиенту, продукту…", scope, counters, setScope, showScopeSwitcher = true, mobileHeader, hint, dialogs, filtered, listTab, selectedId, search, errorText, setSearch, setListTab, setSelectedId }: { title?: string; searchPlaceholder?: string; scope: DialogScope; @@ -18,6 +18,7 @@ export function DialogList({ title = "Диалоги", searchPlaceholder = "По setScope: (scope: DialogScope) => void; showScopeSwitcher?: boolean; mobileHeader?: ReactNode; + hint?: ReactNode; dialogs: ConversationListItem[]; filtered: ConversationListItem[]; listTab: ListTab; @@ -46,6 +47,7 @@ export function DialogList({ title = "Диалоги", searchPlaceholder = "По setListTab("mine")}>Мои setListTab("wait")}>Ждут оператора {waitCount > 0 && {waitCount}} + {hint}
{errorText &&
{errorText}
} {/* Кадр S1: пустой список без призыва к действию. */} diff --git a/apps/internal-ui/src/features/sales/dialogs/list.css b/apps/internal-ui/src/features/sales/dialogs/list.css index 8ed05a8..aedb917 100644 --- a/apps/internal-ui/src/features/sales/dialogs/list.css +++ b/apps/internal-ui/src/features/sales/dialogs/list.css @@ -291,3 +291,40 @@ img.sales-dialog-avatar { .scope-dropdown .scope-dot.is-muted { background: var(--n-5); } + +/* Inline-подсказка первого визита (кадр A1). */ +.hub-hint { + display: flex; + align-items: flex-start; + gap: 9px; + margin: 10px 12px 2px; + padding: 9px 10px 9px 12px; + border: 1px solid var(--primary-border); + border-radius: 8px; + background: var(--primary-bg); +} + +.hub-hint p { + flex: 1; + margin: 0; + color: var(--n-2); + font-size: 12.5px; + line-height: 1.45; +} + +.hub-hint button { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + flex: none; + border: 0; + border-radius: 5px; + color: var(--n-4); + background: transparent; +} + +.hub-hint button:hover { + background: rgba(0, 0, 0, 0.06); +} diff --git a/apps/internal-ui/src/shared/Hint.tsx b/apps/internal-ui/src/shared/Hint.tsx new file mode 100644 index 0000000..87ddc83 --- /dev/null +++ b/apps/internal-ui/src/shared/Hint.tsx @@ -0,0 +1,38 @@ +import { useState, type ReactNode } from "react"; + +import { Icon } from "./icons"; + +// Реестр inline-подсказок первого визита (дизайн-базлайн v2, кадр A1; §6 — +// сотрудникам подсказки не показываются, это решает вызывающая сторона). +// Закрытие запоминается на устройстве по ключу `id`. + +const STORAGE_PREFIX = "chatbolls.hint."; + +function isDismissed(id: string): boolean { + try { + return window.localStorage.getItem(STORAGE_PREFIX + id) === "dismissed"; + } catch { + return false; + } +} + +export function Hint({ id, children }: { id: string; children: ReactNode }) { + const [dismissed, setDismissed] = useState(() => isDismissed(id)); + if (dismissed) return null; + + function dismiss() { + setDismissed(true); + try { + window.localStorage.setItem(STORAGE_PREFIX + id, "dismissed"); + } catch { + // Подсказка просто вернётся в следующей сессии. + } + } + + return ( +
+

{children}

+ +
+ ); +}