💄 fix(ui): порталы и знания в формате нового чата

Порталы: заголовок «Порталы» и подпись без «отдела поддержки», счётчики со
склонением («1 портал», «7 материалов»), статусы статей в таблице и в
фильтре — одни подписи (Черновик · Опубликована · В архиве), поле «Адрес
портала» вместо «Адрес Chatballs». Знания: колонки «Агенты»/«Статус» больше
не налезают друг на друга, счётчик со склонением, агенты в карточке знания
окрашены как в чате (agentColorOf). StatusPill принимает подпись.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
AndreyandClaude Fable 5.1 committed 2026-09-06 04:02:06 +03:00
1 parent 04bf264176
commit 8a8486b676
10 files changed
+31 -17

No files matched your search

@@ -1,4 +1,5 @@
import type { AgentCard } from "../../agents/model";
import { agentColorOf } from "../../conversations/model";
import { Icon } from "../../../shared/icons";
import { formatDate } from "../../../shared/utils";
import type { KnowledgeItem } from "./types";
@@ -22,7 +23,7 @@ export function KnowledgeDetailSidebar({
<div className="knowledge-agent-list">
{assignedAgents.map((agent) => (
<button type="button" onClick={() => openAgent(agent.id)} key={agent.id}>
<span><Icon name="robot" size={16} /></span>
<span style={{ color: agentColorOf(agent.code), background: `color-mix(in srgb, ${agentColorOf(agent.code)} 14%, var(--surface-card))` }}><Icon name="robot" size={16} /></span>
<div><strong>{agent.name}</strong><small>{agent.groupName ?? "Без группы"}</small></div>
</button>
))}
@@ -2,6 +2,7 @@ import { ContentLibraryTable } from "../../../shared/content-library/ContentLibr
import { formatDate } from "../../../shared/utils";
import { knowledgeCategoryPath } from "./knowledgeTree";
import type { KnowledgeCategory, KnowledgeItem } from "./model";
import { pluralRu } from "../../../shared/utils";
type KnowledgeTableProps = {
bulkMode: boolean;
@@ -54,7 +55,7 @@ export function KnowledgeTable({
onCreate={onCreate}
onRetry={onRetry}
footer={!bulkMode && <div className="ai-table-footer">
<span>{items.length} знаний</span>
<span>{pluralRu(items.length, ["знание", "знания", "знаний"])}</span>
<span>{selectedIds.size > 0 ? `Выбрано: ${selectedIds.size}` : "Изменения знаний применяются к агентам сразу"}</span>
</div>}
>
@@ -68,11 +68,11 @@
.knowledge-table { table-layout: fixed; }
.knowledge-col-select { width: 38px; }
.knowledge-col-title { width: 32%; }
.knowledge-col-category { width: 27%; }
.knowledge-col-attachments { width: 6%; }
.knowledge-col-agents { width: 6%; }
.knowledge-col-status { width: 9%; }
.knowledge-col-title { width: 30%; }
.knowledge-col-category { width: 24%; }
.knowledge-col-attachments { width: 8%; }
.knowledge-col-agents { width: 9%; }
.knowledge-col-status { width: 13%; }
.knowledge-col-updated { width: 14%; }
.knowledge-table th,
@@ -4,6 +4,7 @@ import { Icon } from "../../shared/icons";
import { StatusPill } from "../../shared/ui";
import { formatDate } from "../../shared/utils";
import type { PortalArticle } from "./model";
import { ARTICLE_STATUS_LABEL } from "./model";
export function PortalArticleTable({
articles,
@@ -88,7 +89,7 @@ export function PortalArticleTable({
<td className="knowledge-category-path">{article.category.name}</td>
<td>{article.locale.toLocaleUpperCase()}</td>
<td>{article.publishedRevision ? article.publishedRevision.revision : "—"}</td>
<td><StatusPill status={article.status === "PUBLISHED" ? "published" : article.status === "ARCHIVED" ? "archived" : "draft"} /></td>
<td><StatusPill status={article.status === "PUBLISHED" ? "published" : article.status === "ARCHIVED" ? "archived" : "draft"} label={ARTICLE_STATUS_LABEL[article.status]} /></td>
<td>{formatDate(article.updatedAt)}</td>
<td className="row-actions" onClick={(event) => event.stopPropagation()}>
{canManage && (
@@ -12,6 +12,8 @@ import { ContentLibraryToolbar } from "../../shared/content-library/ContentLibra
import { DecisionDialog } from "../../shared/DecisionDialog";
import { Icon } from "../../shared/icons";
import { Button } from "../../shared/ui-controls";
import { pluralRu } from "../../shared/utils";
import { ARTICLE_STATUS_LABEL } from "./model";
import { agentLinkOptions } from "../ai/agentOptions";
import { linkPortalArticlesToAgent } from "../ai/knowledge/model";
import { useAiAgents } from "../ai/useAiAgents";
@@ -198,7 +200,7 @@ export function PortalContent({
)}
>
<label className="knowledge-filter-select"><span>Язык:</span><select value={language} onChange={(event) => setLanguage(event.target.value)}><option value="">Все</option><option value="ru">Русский</option><option value="en">English</option></select></label>
<label className="knowledge-filter-select"><span>Статус:</span><select value={status} onChange={(event) => setStatus(event.target.value)}><option value="">Все</option><option value="DRAFT">Черновик</option><option value="PUBLISHED">Опубликована</option><option value="ARCHIVED">Архив</option></select></label>
<label className="knowledge-filter-select"><span>Статус:</span><select value={status} onChange={(event) => setStatus(event.target.value)}><option value="">Все</option><option value="DRAFT">{ARTICLE_STATUS_LABEL.DRAFT}</option><option value="PUBLISHED">{ARTICLE_STATUS_LABEL.PUBLISHED}</option><option value="ARCHIVED">{ARTICLE_STATUS_LABEL.ARCHIVED}</option></select></label>
</ContentLibraryToolbar>
)}
{error && <div className="portal-form-error">{error}</div>}
@@ -213,7 +215,7 @@ export function PortalContent({
loading={false}
onCreate={() => setEditing(null)}
onRetry={() => void reload()}
footer={<div className="ai-table-footer"><span>{filtered.length} материалов</span></div>}
footer={<div className="ai-table-footer"><span>{pluralRu(filtered.length, ["материал", "материала", "материалов"])}</span></div>}
>
<PortalArticleTable
articles={filtered}
@@ -73,7 +73,7 @@ export function PortalCreateDialog({
<p>Портал — это публичный центр помощи с инструкциями и ответами для клиентов.</p>
<FormField error={fieldErrors.name} label="Название портала" value={name} onChange={setName} placeholder="Например, Центр помощи" wide />
<label className={`portal-address-field${fieldErrors.slug ? " is-invalid" : ""}`}>
<span>Адрес Chatballs</span>
<span>Адрес портала</span>
<div>
<input
type="text"
@@ -77,7 +77,7 @@ export function PortalSettings({
</div>
<div className="portal-settings-fields">
<FormField disabled={!canManage} label="Название портала" value={name} onChange={canManage ? setName : undefined} wide />
<FormField disabled={!canManage} label="Адрес Chatballs" value={slug} onChange={canManage ? setSlug : undefined} mono wide />
<FormField disabled={!canManage} label="Адрес портала" value={slug} onChange={canManage ? setSlug : undefined} mono wide />
<p className="portal-address-preview">{portal.publicUrl}</p>
<SelectField
disabled={!canManage}
@@ -6,6 +6,7 @@ import { ContentLibraryToolbar } from "../../shared/content-library/ContentLibra
import { Icon } from "../../shared/icons";
import { ContentState, ErrorScreen, LoadingState, PageHeader, StatusPill } from "../../shared/ui";
import { Button } from "../../shared/ui-controls";
import { pluralRu } from "../../shared/utils";
import type { SessionUser } from "../../types";
import {
listSupportPortals,
@@ -65,8 +66,8 @@ export function SupportPortalsPage({
return (
<div className="support-portals-page">
<PageHeader
title="Порталы поддержки"
text="Публичные базы знаний отдела поддержки"
title="Порталы"
text="Публичные базы знаний для клиентов"
action={createAction}
/>
{portals.length === 0 ? (
@@ -90,7 +91,7 @@ export function SupportPortalsPage({
hasItems={filtered.length > 0}
loading={false}
onRetry={() => void load()}
footer={<div className="ai-table-footer"><span>{filtered.length} порталов</span></div>}
footer={<div className="ai-table-footer"><span>{pluralRu(filtered.length, ["портал", "портала", "порталов"])}</span></div>}
>
<table className="baseline-table knowledge-table">
<thead><tr><th>ПОРТАЛ</th><th>СТАТУС</th><th>ПРОДУКТЫ</th><th /></tr></thead>
+2 -2
View File
@@ -25,7 +25,7 @@ export function PageHeader({ title, text, action }: { title: string; text?: Reac
export type StatusPillKey = "normal" | "active" | "published" | "blocked" | "disabled" | "invited" | "archived" | "draft" | "healthy" | "error" | "pending" | "unchecked";
export function StatusPill({ status }: { status: StatusPillKey }) {
export function StatusPill({ status, label: labelOverride }: { status: StatusPillKey; label?: string }) {
const map = {
normal: ["var(--success-bg)", "var(--success-border)", "var(--success-text)", "Работает"],
active: ["transparent", "transparent", "var(--success-text)", "Активен"],
@@ -44,7 +44,7 @@ export function StatusPill({ status }: { status: StatusPillKey }) {
const dotStyle = status === "archived"
? { background: "transparent", border: `1.5px solid ${color}` }
: { background: color };
return <span className={`status-pill ${status}`} style={{ background: bg, borderColor: border, color }}><span style={dotStyle} />{label}</span>;
return <span className={`status-pill ${status}`} style={{ background: bg, borderColor: border, color }}><span style={dotStyle} />{labelOverride ?? label}</span>;
}
const ROLE_LABELS: Record<Role, string> = {
+8
View File
@@ -18,3 +18,11 @@ export function productAccent(code: string): { bg: string; color: string } {
export function formatDate(value: string): string {
return new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "long", year: "numeric" }).format(new Date(value));
}
/** Склонение по числу: pluralRu(3, ["портал", "портала", "порталов"]) → «3 портала». */
export function pluralRu(count: number, forms: [string, string, string]): string {
const n = Math.abs(count) % 100;
const n1 = n % 10;
const form = n > 10 && n < 20 ? forms[2] : n1 > 1 && n1 < 5 ? forms[1] : n1 === 1 ? forms[0] : forms[2];
return `${count} ${form}`;
}