mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 17:14:59 +03:00
💄 fix(ui): порталы и знания в формате нового чата
Порталы: заголовок «Порталы» и подпись без «отдела поддержки», счётчики со склонением («1 портал», «7 материалов»), статусы статей в таблице и в фильтре — одни подписи (Черновик · Опубликована · В архиве), поле «Адрес портала» вместо «Адрес Chatballs». Знания: колонки «Агенты»/«Статус» больше не налезают друг на друга, счётчик со склонением, агенты в карточке знания окрашены как в чате (agentColorOf). StatusPill принимает подпись. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
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>
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
Reference in new issue
Block a user