chore(ai): упразднение тестового чата (frontend + docs)

Удалён фронтенд-only статический прототип тестового чата (features/ai/test-chat/)
и все точки его подключения: роут aiTestChat, таб поднавигации, кнопки в заголовках
AI-агента/release и таблице агентов, CSS-классы. Мёртвая ветка логики статусов релиза
(параметр tested) убрана, тексты про тест-чат заменены на нейтральные.

Baseline-прототип Тестовый чат.dc.html удалён; упразднение AI-04 зафиксировано
в SPEC-HUB-0001, SPEC-HUB-0004, SPEC-HUB-0005.
This commit is contained in:
Andrey committed 2026-07-10 13:46:43 +03:00
1 parent ee764a00e7
commit 1dc7e79612
25 files changed
+14 -1042

No files matched your search

@@ -1,9 +1,8 @@
import { Dropdown } from "antd";
import type { RouteKey } from "../../types";
import { Icon } from "../../shared/icons";
import { StatusPill } from "../../shared/ui";
import { Button, MonoLink, ToneBadge } from "../../shared/ui-controls";
import { MonoLink, ToneBadge } from "../../shared/ui-controls";
import { productAccent } from "../../shared/utils";
import { publishedRelease, releaseName, type AiAgent, type AiRelease } from "./model";
@@ -13,7 +12,6 @@ export function AgentsTable({
menuId,
setMenuId,
toggleActive,
setRoute,
openAgent,
openRelease,
}: {
@@ -22,7 +20,6 @@ export function AgentsTable({
menuId: number | null;
setMenuId: (id: number | null) => void;
toggleActive: (agent: AiAgent) => void;
setRoute: (route: RouteKey) => void;
openAgent: (agentId: number) => void;
openRelease: (releaseId: number) => void;
}) {
@@ -84,7 +81,6 @@ export function AgentsTable({
<td className="numeric"><span className="product-empty-value">—</span></td>
<td className="row-actions">
<div className="ai-row-actions">
<Button variant="secondary" className="ai-test-chat-btn" icon="message" iconSize={14} onClick={() => setRoute("aiTestChat")}>Тест-чат</Button>
<Dropdown
menu={{ items: menuItems }}
open={menuId === agent.id}
@@ -1,14 +1,13 @@
import { useState } from "react";
import { api } from "../../api/client";
import type { RouteKey } from "../../types";
import { PageHeader } from "../../shared/ui";
import { Button } from "../../shared/ui-controls";
import { AgentsKpiStrip } from "./AgentsKpiStrip";
import { AgentsTable } from "./AgentsTable";
import type { AiAgent, AiRelease } from "./model";
export function AiAgentsPage({ agents, releases, reload, setRoute, openAgentCreate, openAgent, openRelease }: { agents: AiAgent[]; releases: AiRelease[]; reload: () => void; setRoute: (route: RouteKey) => void; openAgentCreate: (productCode: string | null) => void; openAgent: (agentId: number) => void; openRelease: (releaseId: number) => void }) {
export function AiAgentsPage({ agents, releases, reload, openAgentCreate, openAgent, openRelease }: { agents: AiAgent[]; releases: AiRelease[]; reload: () => void; openAgentCreate: (productCode: string | null) => void; openAgent: (agentId: number) => void; openRelease: (releaseId: number) => void }) {
const [menuId, setMenuId] = useState<number | null>(null);
async function toggleActive(agent: AiAgent) {
@@ -22,7 +21,7 @@ export function AiAgentsPage({ agents, releases, reload, setRoute, openAgentCrea
<div className="ai-page">
<PageHeader title="AI-агенты" text="Sales-агенты продуктов · один агент на продукт" action={<Button variant="primary" icon="plus" onClick={() => openAgentCreate(null)}>Создать агента</Button>} />
<AgentsKpiStrip agents={agents} />
<AgentsTable agents={agents} releases={releases} menuId={menuId} setMenuId={setMenuId} toggleActive={toggleActive} setRoute={setRoute} openAgent={openAgent} openRelease={openRelease} />
<AgentsTable agents={agents} releases={releases} menuId={menuId} setMenuId={setMenuId} toggleActive={toggleActive} openAgent={openAgent} openRelease={openRelease} />
</div>
);
}
@@ -3,7 +3,6 @@ import { UnderlineTabs } from "../../shared/ui-controls";
const ITEMS: Array<{ key: RouteKey; label: string; disabled?: boolean }> = [
{ key: "aiAgents", label: "AI-агенты" },
{ key: "aiTestChat", label: "Тестовый чат" },
{ key: "aiUsage", label: "Использование AI", disabled: true },
];
@@ -2,20 +2,17 @@ import { Icon } from "../../../shared/icons";
import { StatusPill } from "../../../shared/ui";
import { Button } from "../../../shared/ui-controls";
import { productAccent } from "../../../shared/utils";
import type { RouteKey } from "../../../types";
import { dailyBudget, publishedRelease, releaseDisplayName, type AiAgentDetail, type AiReleaseFull } from "./model";
export function AiAgentDetailHeader({
agent,
releases,
setRoute,
openRelease,
createRelease,
onEditChannel,
}: {
agent: AiAgentDetail;
releases: AiReleaseFull[];
setRoute: (route: RouteKey) => void;
openRelease: (releaseId: number) => void;
createRelease: () => void;
onEditChannel: () => void;
@@ -40,7 +37,6 @@ export function AiAgentDetailHeader({
</div>
</div>
<div className="ai-agent-header-actions">
<Button variant="secondary" icon="message" onClick={() => setRoute("aiTestChat")}>Тест-чат</Button>
<Button variant="primary" icon="plus" onClick={createRelease}>Создать черновик версии</Button>
</div>
</section>
@@ -4,7 +4,6 @@ import { api } from "../../../api/client";
import { Icon } from "../../../shared/icons";
import { EmptyState, LoadingState } from "../../../shared/ui";
import { Button, UnderlineTabs } from "../../../shared/ui-controls";
import type { RouteKey } from "../../../types";
import { AiAgentDetailHeader } from "./AiAgentDetailHeader";
import { AgentEditForm } from "./AgentEditForm";
import { ChannelEditForm } from "./ChannelEditForm";
@@ -16,7 +15,7 @@ import { AiAgentReleasesTab } from "./AiAgentReleasesTab";
import { agentTabs, type AgentTab, type AiReleaseFull } from "./model";
import { useAiAgentDetail } from "./useAiAgentDetail";
export function AiAgentDetailPage({ agentId, setRoute, openRelease, onAgentLoaded }: { agentId: number | null; setRoute: (route: RouteKey) => void; openRelease: (releaseId: number) => void; onAgentLoaded: (name: string | null) => void }) {
export function AiAgentDetailPage({ agentId, openRelease, onAgentLoaded }: { agentId: number | null; openRelease: (releaseId: number) => void; onAgentLoaded: (name: string | null) => void }) {
const { agent, releases, knowledge, prompts, loading, error, reload } = useAiAgentDetail(agentId);
const [tab, setTab] = useState<AgentTab>("overview");
const [editOpen, setEditOpen] = useState(false);
@@ -50,7 +49,7 @@ export function AiAgentDetailPage({ agentId, setRoute, openRelease, onAgentLoade
return (
<div className="ai-agent-page">
<AiAgentDetailHeader agent={agent} releases={releases} setRoute={setRoute} openRelease={openRelease} createRelease={createRelease} onEditChannel={() => setChannelEdit(true)} />
<AiAgentDetailHeader agent={agent} releases={releases} openRelease={openRelease} createRelease={createRelease} onEditChannel={() => setChannelEdit(true)} />
<UnderlineTabs className="ai-agent-tabs" items={agentTabs} value={tab} onChange={setTab} />
{(tab === "instructions" || tab === "knowledge") && (
<div className="ai-notice ai-golive-notice">
@@ -1,6 +1,5 @@
import { Icon } from "../../../shared/icons";
import { Button } from "../../../shared/ui-controls";
import type { RouteKey } from "../../../types";
import type { AiReleaseFull } from "../detail/model";
import { formatDateTime, releaseLabel, releaseState, type ReleaseCheck } from "./model";
@@ -8,13 +7,11 @@ export function ProductAIReleaseHeader({
canPublish,
checks,
release,
setRoute,
startPublish,
}: {
canPublish: boolean;
checks: ReleaseCheck[];
release: AiReleaseFull;
setRoute: (route: RouteKey) => void;
startPublish: () => void;
}) {
const state = releaseState(checks, release.status === "PUBLISHED");
@@ -38,7 +35,6 @@ export function ProductAIReleaseHeader({
</div>
<div className="release-actions">
<Button variant="secondary" icon="expand">Сравнить с опубликованной</Button>
<Button variant="secondary" icon="message" onClick={() => setRoute("aiTestChat")}>Тест-чат</Button>
<Button variant="primary" icon="send" disabled={!canPublish} onClick={startPublish}>
{release.status === "PUBLISHED" ? "Опубликована" : "Опубликовать"}
</Button>
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from "react";
import { api } from "../../../api/client";
import { EmptyState, LoadingState } from "../../../shared/ui";
import type { RouteKey } from "../../../types";
import { ProductAIReleaseHeader } from "./ProductAIReleaseHeader";
import { PublishReleaseModal } from "./PublishReleaseModal";
import { ReleaseComposition } from "./ReleaseComposition";
@@ -13,11 +12,9 @@ import { useProductAIRelease } from "./useProductAIRelease";
export function ProductAIReleasePage({
onReleaseLoaded,
releaseId,
setRoute,
}: {
onReleaseLoaded: (name: string | null) => void;
releaseId: number | null;
setRoute: (route: RouteKey) => void;
}) {
const { release, releases, knowledge, prompts, loading, error, reload } = useProductAIRelease(releaseId);
const [modalOpen, setModalOpen] = useState(false);
@@ -54,7 +51,7 @@ export function ProductAIReleasePage({
return (
<div className="ai-release-page">
<ProductAIReleaseHeader canPublish={canPublish} checks={checks} release={release} setRoute={setRoute} startPublish={() => setModalOpen(true)} />
<ProductAIReleaseHeader canPublish={canPublish} checks={checks} release={release} startPublish={() => setModalOpen(true)} />
<div className="ai-release-body">
<ReleaseValidationBanner checks={checks} runChecks={reload} />
<div className="release-layout">
@@ -79,20 +79,18 @@ export function canPublishRelease(release: AiReleaseFull, checks: ReleaseCheck[]
return release.status === "DRAFT" && checks.every((check) => check.tone === "ok");
}
export function validationBanner(checks: ReleaseCheck[], tested = true) {
export function validationBanner(checks: ReleaseCheck[]) {
const summary = checksSummary(checks);
if (summary.tone === "error") return { title: "Найдены ошибки валидации", note: "Исправьте отмеченные пункты в составе версии, затем перезапустите проверку.", tone: "error" as const };
if (summary.tone === "warn") return { title: "Черновик версии неполный", note: "Заполните обязательные разделы, чтобы перейти к проверке.", tone: "warn" as const };
if (!tested) return { title: "Готово к тестированию", note: "Проверки пройдены. Запустите тест-чат перед публикацией.", tone: "ok" as const };
return { title: "Готово к публикации", note: "Все проверки пройдены, протестировано в тест-чате. Можно публиковать.", tone: "ok" as const };
return { title: "Готово к публикации", note: "Все проверки пройдены. Можно публиковать.", tone: "ok" as const };
}
export function releaseState(checks: ReleaseCheck[], published: boolean, tested = true): { className: string; label: string } {
export function releaseState(checks: ReleaseCheck[], published: boolean): { className: string; label: string } {
const summary = checksSummary(checks);
if (published) return { className: "published", label: "Опубликован" };
if (summary.tone === "error") return { className: "errors", label: "Ошибки валидации" };
if (summary.tone === "warn") return { className: "incomplete", label: "Неполный черновик" };
if (!tested) return { className: "ready-test", label: "Готов к тестированию" };
return { className: "ready-publish", label: "Готов к публикации" };
}
@@ -86,16 +86,6 @@
background: #52c41a;
}
.release-status-badge.ready-test {
color: #0958d9;
background: #e6f4ff;
border-color: #91caff;
}
.release-status-badge.ready-test span {
background: #1677ff;
}
.release-status-badge.incomplete {
color: #8c8c8c;
background: #f5f5f5;
@@ -101,13 +101,6 @@
gap: 7px;
}
.ai-test-chat-btn {
height: 32px;
padding: 0 12px;
font-size: 12.5px;
white-space: nowrap;
}
.ai-table-footer {
display: flex;
align-items: center;
@@ -1,19 +0,0 @@
import { useState } from "react";
import type { TestChannel } from "./model";
import { TestChatDiagnostics } from "./TestChatDiagnostics";
import { TestChatSidebar } from "./TestChatSidebar";
import { TestConversation } from "./TestConversation";
export function AiTestChatPage() {
const [channel, setChannel] = useState<TestChannel>("MAX");
const [scenario, setScenario] = useState("box");
return (
<div className="ai-test-chat-page">
<TestChatSidebar channel={channel} scenario={scenario} setChannel={setChannel} setScenario={setScenario} />
<TestConversation channel={channel} />
<TestChatDiagnostics />
</div>
);
}
@@ -1,52 +0,0 @@
import { Icon } from "../../../shared/icons";
import { diagnosticChunks } from "./model";
export function TestChatDiagnostics() {
return (
<section className="test-diagnostics">
<div className="test-diagnostics-head">
<h3>Диагностика</h3>
<span>Последний ответ AI</span>
</div>
<div className="test-diagnostics-scroll">
<div className="test-cost-grid">
<div><span>Tokens</span><strong>1 284</strong><small>in 940 · out 344</small></div>
<div><span>Cost</span><strong>₽0,42</strong><small>latency 3,1с</small></div>
</div>
<PanelTitle>ИСПОЛЬЗОВАННЫЕ ФРАГМЕНТЫ</PanelTitle>
<div className="test-diagnostic-list">
{diagnosticChunks.map((chunk) => (
<div className="test-chunk" key={chunk.source}>
<div><strong>{chunk.source}</strong><b>{chunk.score}</b></div>
<p>{chunk.text}</p>
</div>
))}
</div>
<PanelTitle>ВЫЗВАННЫЕ ИНСТРУМЕНТЫ</PanelTitle>
<div className="test-diagnostic-list">
<ToolCall name="get_offers" meta="product=firepage → 2 предложения" time="120мс" />
<ToolCall name="create_checkout" meta="offer=OFR-FP-BOX · ₽4 900 (симуляция)" time="88мс" />
</div>
<PanelTitle>ПЕРЕДАЧА ОПЕРАТОРУ</PanelTitle>
<div className="test-handoff"><Icon name="check" size={16} />Передачи оператору не было — сценарий в рамках агента.</div>
</div>
</section>
);
}
function PanelTitle({ children }: { children: string }) {
return <div className="test-panel-title">{children}</div>;
}
function ToolCall({ meta, name, time }: { meta: string; name: string; time: string }) {
return (
<div className="test-tool-call">
<Icon name="check" size={15} />
<div><strong>{name}</strong><span>{meta}</span></div>
<small>{time}</small>
</div>
);
}
@@ -1,56 +0,0 @@
import { Icon } from "../../../shared/icons";
import { testChannels, testScenarios, type TestChannel } from "./model";
export function TestChatSidebar({
channel,
scenario,
setChannel,
setScenario,
}: {
channel: TestChannel;
scenario: string;
setChannel: (channel: TestChannel) => void;
setScenario: (scenario: string) => void;
}) {
return (
<section className="test-chat-sidebar">
<div className="test-chat-sidebar-scroll">
<div className="test-chat-label">ЧЕРНОВИК ВЕРСИИ</div>
<label className="test-release-select">
<select defaultValue="REL-FP-v5">
<option value="REL-FP-v5">REL-FP-v5 · Черновик версии</option>
<option value="REL-FP-v4">REL-FP-v4 · Опубликована</option>
</select>
<Icon name="chevron" size={15} />
</label>
<div className="test-agent-chip">
<Icon name="robot" size={15} />
<span>FirePage Sales · gpt-4o-mini · temp 0.4</span>
</div>
<div className="test-chat-label">КАНАЛ СИМУЛЯЦИИ</div>
<div className="test-channel-row">
{testChannels.map((item) => (
<button className={channel === item.key ? "active" : ""} type="button" onClick={() => setChannel(item.key)} key={item.key}>
<span style={{ background: item.dot }} />
{item.label}
</button>
))}
</div>
<div className="test-chat-label">ГОТОВЫЕ СЦЕНАРИИ</div>
<div className="test-scenarios">
{testScenarios.map((item) => (
<button className={scenario === item.id ? "active" : ""} type="button" onClick={() => setScenario(item.id)} key={item.id}>
<strong>{item.title}</strong>
<span>{item.desc}</span>
</button>
))}
</div>
</div>
<div className="test-chat-sidebar-footer">
<button type="button"><Icon name="trash" size={15} />Очистить тестовую сессию</button>
</div>
</section>
);
}
@@ -1,97 +0,0 @@
import { useLayoutEffect, useRef, useState, type ChangeEvent, type ReactNode } from "react";
import { Icon } from "../../../shared/icons";
import type { TestChannel } from "./model";
const channelLabel: Record<TestChannel, string> = {
MAX: "MAX",
TG: "Telegram",
WEB: "Web Chat",
};
export function TestConversation({ channel }: { channel: TestChannel }) {
const [message, setMessage] = useState("");
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const resizeMessageInput = () => {
const textarea = textareaRef.current;
if (!textarea) return;
const styles = window.getComputedStyle(textarea);
const lineHeight = Number.parseFloat(styles.lineHeight);
const paddingTop = Number.parseFloat(styles.paddingTop);
const paddingBottom = Number.parseFloat(styles.paddingBottom);
const maxHeight = lineHeight * 12 + paddingTop + paddingBottom;
textarea.style.height = "auto";
const nextHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${nextHeight}px`;
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? "auto" : "hidden";
};
useLayoutEffect(() => {
resizeMessageInput();
}, [message]);
const handleMessageChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setMessage(event.target.value);
};
return (
<section className="test-conversation">
<div className="test-conversation-head">
<div className="test-conversation-title">
<span><Icon name="robot" size={18} /></span>
<div>
<strong>Test conversation</strong>
<small>Симуляция клиента · сообщений: 4</small>
</div>
</div>
<em><Icon name="eyeOff" size={13} />Не создаёт Contact, Order или Payment</em>
</div>
<div className="test-messages">
<div className="test-scenario-pill">Сценарий: «Покупка коробки» · канал {channelLabel[channel]}</div>
<ClientMessage text="Здравствуйте, интересует FirePage для сайта-визитки. Сколько стоит?" note="тестовый клиент" />
<AiMessage>
Здравствуйте! FirePage — это готовый нишевой сайт под ключ. Коробка стоит <b>₽4 900</b> единоразово. Дополнительно можно подключить годовую поддержку за ₽1 470. Под визитку отлично подходит — оформить?
</AiMessage>
<ClientMessage text="Да, давайте оформим коробку." />
<AiMessage tool="create_checkout">
Отлично! Подготовил оформление коробки FirePage за ₽4 900. Вызываю оформление покупки — в реальном диалоге клиент получил бы ссылку на оплату.
</AiMessage>
</div>
<div className="test-composer">
<div>
<textarea ref={textareaRef} rows={1} value={message} placeholder="Сообщение от лица тестового клиента…" onChange={handleMessageChange} />
<button type="button">Отправить<Icon name="send" size={15} /></button>
</div>
</div>
</section>
);
}
function ClientMessage({ note, text }: { note?: string; text: string }) {
return (
<div className="test-message client">
<span className="test-client-avatar">Т</span>
<div>
<p>{text}</p>
{note && <small>{note}</small>}
</div>
</div>
);
}
function AiMessage({ children, tool }: { children: ReactNode; tool?: string }) {
return (
<div className="test-message ai">
<span className="test-ai-avatar"><Icon name="robot" size={16} /></span>
<div>
<strong>AI · REL-FP-v5</strong>
<p>{children}</p>
{tool && <em>tool: {tool}</em>}
</div>
</div>
);
}
@@ -1,201 +0,0 @@
.test-conversation-head {
flex: none;
display: flex;
align-items: center;
justify-content: space-between;
padding: 11px 20px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.test-conversation-title {
display: flex;
align-items: center;
gap: 11px;
}
.test-conversation-title > span,
.test-ai-avatar {
display: flex;
align-items: center;
justify-content: center;
background: #f9f0ff;
border: 1px solid #efdbff;
color: #722ed1;
}
.test-conversation-title > span {
width: 34px;
height: 34px;
border-radius: 9px;
}
.test-conversation-title strong {
display: block;
color: #1f1f1f;
font-size: 14px;
}
.test-conversation-title small,
.test-conversation-head em {
color: #8c8c8c;
font-size: 11.5px;
font-style: normal;
}
.test-conversation-head em {
display: inline-flex;
align-items: center;
gap: 5px;
color: #bfbfbf;
}
.test-messages {
flex: 1;
overflow-y: auto;
padding: 18px 24px 8px;
}
.test-messages::before,
.test-messages::after {
display: table;
content: "";
}
.test-scenario-pill {
width: max-content;
margin: 0 auto 16px;
padding: 4px 12px;
color: #8c8c8c;
background: #eef0f2;
border-radius: 20px;
font-size: 11.5px;
}
.test-message {
display: flex;
gap: 10px;
max-width: 680px;
margin: 0 auto 14px;
}
.test-message.ai {
flex-direction: row-reverse;
}
.test-client-avatar,
.test-ai-avatar {
width: 30px;
height: 30px;
flex: none;
border-radius: 50%;
}
.test-client-avatar {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: #8c8c8c;
font-size: 11px;
font-weight: 600;
}
.test-message > div {
max-width: 78%;
}
.test-message p {
margin: 0;
padding: 10px 14px;
color: #262626;
background: #fff;
border: 1px solid #eee;
border-radius: 4px 14px 14px 14px;
font-size: 13.5px;
line-height: 1.5;
}
.test-message.ai p {
background: #f9f0ff;
border-color: #efdbff;
border-radius: 14px 4px 14px 14px;
}
.test-message small {
display: block;
margin: 4px 0 0 4px;
color: #bfbfbf;
font-size: 11px;
}
.test-message.ai strong {
display: block;
margin-bottom: 4px;
color: #722ed1;
font-size: 11px;
text-align: right;
}
.test-message.ai em {
display: inline-flex;
float: right;
margin: 6px 4px 0 0;
padding: 2px 8px;
color: #0958d9;
background: #e6f4ff;
border-radius: 5px;
font-family: "SF Mono", ui-monospace, Menlo, monospace;
font-size: 10.5px;
font-style: normal;
font-weight: 600;
}
.test-composer {
flex: none;
padding: 14px 24px 16px;
background: #fff;
border-top: 1px solid #f0f0f0;
}
.test-composer > div {
max-width: 680px;
display: flex;
align-items: flex-end;
gap: 10px;
margin: 0 auto;
padding: 8px 8px 8px 14px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 12px;
}
.test-composer textarea {
flex: 1;
min-height: 38px;
min-width: 0;
padding: 9px 0;
overflow-y: hidden;
resize: none;
border: 0;
outline: 0;
color: #262626;
font: inherit;
font-size: 13.5px;
line-height: 1.5;
}
.test-composer button {
height: 38px;
display: inline-flex;
align-items: center;
gap: 7px;
flex: none;
padding: 0 16px;
color: #fff;
background: #1677ff;
border: 0;
border-radius: 9px;
font-size: 13px;
font-weight: 600;
}
@@ -1,155 +0,0 @@
.test-diagnostics {
width: 340px;
flex: none;
display: flex;
flex-direction: column;
min-height: 0;
background: #fff;
border-left: 1px solid #f0f0f0;
}
.test-diagnostics-head {
flex: none;
padding: 15px 18px 12px;
border-bottom: 1px solid #f0f0f0;
}
.test-diagnostics-head h3 {
margin: 0;
color: #1f1f1f;
font-size: 14px;
}
.test-diagnostics-head span {
display: block;
margin-top: 2px;
color: #bfbfbf;
font-size: 11.5px;
}
.test-cost-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
margin-bottom: 18px;
overflow: hidden;
background: #f0f0f0;
border: 1px solid #f0f0f0;
border-radius: 9px;
}
.test-cost-grid div {
padding: 12px 14px;
background: #fff;
}
.test-cost-grid span,
.test-cost-grid small {
display: block;
color: #8c8c8c;
font-size: 11px;
}
.test-cost-grid strong {
display: block;
margin-top: 3px;
color: #262626;
font-size: 17px;
}
.test-cost-grid small {
margin-top: 1px;
color: #bfbfbf;
font-size: 10.5px;
}
.test-diagnostic-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.test-chunk,
.test-tool-call,
.test-handoff {
border: 1px solid #f0f0f0;
border-radius: 9px;
}
.test-chunk {
padding: 10px 12px;
}
.test-chunk div,
.test-tool-call {
display: flex;
align-items: center;
gap: 10px;
}
.test-chunk div {
justify-content: space-between;
gap: 8px;
}
.test-chunk strong,
.test-tool-call strong {
color: #262626;
font-size: 12px;
}
.test-chunk b {
padding: 1px 6px;
color: #389e0d;
background: #f6ffed;
border-radius: 5px;
font-size: 10.5px;
}
.test-chunk p,
.test-tool-call span,
.test-handoff {
color: #8c8c8c;
font-size: 11.5px;
line-height: 1.45;
}
.test-chunk p {
margin: 4px 0 0;
}
.test-tool-call {
padding: 10px 12px;
}
.test-tool-call svg,
.test-handoff svg {
flex: none;
color: #389e0d;
}
.test-tool-call div {
flex: 1;
min-width: 0;
}
.test-tool-call strong,
.test-tool-call span {
display: block;
font-family: "SF Mono", ui-monospace, Menlo, monospace;
}
.test-tool-call small {
flex: none;
color: #bfbfbf;
font-size: 10.5px;
}
.test-handoff {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 13px;
background: #fafafa;
}
@@ -1,18 +0,0 @@
export type TestChannel = "MAX" | "TG" | "WEB";
export const testChannels: Array<{ key: TestChannel; label: string; dot: string }> = [
{ key: "MAX", label: "MAX", dot: "#6b5be0" },
{ key: "TG", label: "TG", dot: "#2f8fd0" },
{ key: "WEB", label: "Web", dot: "#0f9b8e" },
];
export const testScenarios = [
{ id: "box", title: "Покупка коробки", desc: "Клиент хочет купить FirePage под визитку." },
{ id: "support", title: "Вопрос про поддержку", desc: "Уточняет условия годовой поддержки." },
{ id: "handoff", title: "Запрос человека", desc: "Просит соединить с менеджером." },
];
export const diagnosticChunks = [
{ source: "Что входит в коробку FirePage", score: "0.91", text: "Коробка FirePage — готовый нишевой сайт под ключ, единоразовая оплата ₽4 900." },
{ source: "Условия годовой поддержки", score: "0.84", text: "Годовая поддержка — 30% от цены коробки, ₽1 470 в год, продление по желанию." },
];
@@ -1,167 +0,0 @@
.ai-test-chat-page {
height: calc(100vh - 56px);
display: flex;
overflow: hidden;
}
.test-chat-sidebar {
width: 300px;
flex: none;
display: flex;
flex-direction: column;
min-height: 0;
background: #fff;
border-right: 1px solid #f0f0f0;
}
.test-chat-sidebar-scroll,
.test-diagnostics-scroll {
flex: 1;
overflow-y: auto;
padding: 18px;
}
.test-chat-label,
.test-panel-title {
margin-bottom: 10px;
color: #bfbfbf;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.05em;
}
.test-release-select {
position: relative;
display: block;
margin-bottom: 8px;
}
.test-release-select select {
width: 100%;
height: 40px;
padding: 0 32px 0 12px;
color: #262626;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 9px;
appearance: none;
font: inherit;
font-size: 13px;
font-weight: 500;
}
.test-release-select svg {
position: absolute;
top: 13px;
right: 11px;
color: #8c8c8c;
pointer-events: none;
}
.test-agent-chip {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 12px;
margin-bottom: 18px;
color: #722ed1;
background: #f9f0ff;
border: 1px solid #efdbff;
border-radius: 8px;
font-size: 11.5px;
}
.test-channel-row {
display: flex;
gap: 6px;
margin-bottom: 18px;
}
.test-channel-row button {
height: 32px;
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
color: #595959;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
}
.test-channel-row button.active {
color: #0958d9;
background: #e6f4ff;
border-color: #1677ff;
font-weight: 600;
}
.test-channel-row span {
width: 6px;
height: 6px;
border-radius: 50%;
}
.test-scenarios {
display: flex;
flex-direction: column;
gap: 8px;
}
.test-scenarios button {
padding: 11px 13px;
text-align: left;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 10px;
}
.test-scenarios button.active {
background: #f7fbff;
border-color: #1677ff;
}
.test-scenarios strong {
display: block;
color: #262626;
font-size: 13px;
}
.test-scenarios span {
display: block;
margin-top: 2px;
color: #8c8c8c;
font-size: 11.5px;
line-height: 1.4;
}
.test-chat-sidebar-footer {
padding: 12px 18px;
border-top: 1px solid #f5f5f5;
}
.test-chat-sidebar-footer button {
width: 100%;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #595959;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 9px;
font-size: 13px;
font-weight: 500;
}
.test-conversation {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
background: #f7f8fa;
}
+1 -1
View File
@@ -93,7 +93,7 @@ export function Shell({ route, setRoute, selectedEmployeeId, selectedProductId,
const isSalesOrders = route === "salesOrderDetail" || route === "salesOrders";
// Подменю AI показываем только там, где оно есть в baseline.
const isAiSection = route === "aiAgents" || route === "aiUsage";
const isAiFullWidth = route === "aiAgentCreate" || route === "aiRelease" || route === "aiTestChat";
const isAiFullWidth = route === "aiAgentCreate" || route === "aiRelease";
// OPERATOR работает в пространстве своего отдела (SPEC-HUB-0004 §9 + §0010 §10):
// sales operator → sales-sidebar, support operator → support-sidebar.
const showSalesSidebar = isSalesWorkspace || (user.role === "OPERATOR" && user.department !== "support" && !isSupportWorkspace);
@@ -2,7 +2,6 @@ import { AiAgentsPage } from "../features/ai/AiAgentsPage";
import { AiAgentCreatePage } from "../features/ai/create/AiAgentCreatePage";
import { AiAgentDetailPage } from "../features/ai/detail/AiAgentDetailPage";
import { ProductAIReleasePage } from "../features/ai/release/ProductAIReleasePage";
import { AiTestChatPage } from "../features/ai/test-chat/AiTestChatPage";
import { CommandCenter } from "../features/command/CommandCenter";
import { IntegrationsPage } from "../features/integrations/IntegrationsPage";
import { DepartmentsPage } from "../features/departments/DepartmentsPage";
@@ -40,11 +39,10 @@ export function ShellRouteContent({ route, data, currentEmployee, currentProduct
{route === "salesDialogs" && <SalesDialogsPage initialConversationId={selectedConversationId} />}
{route === "salesOrderDetail" && <SalesOrderDetailPage orderId={selectedOrderId} setRoute={setRoute} />}
{route === "salesOrders" && <SalesOrdersPage setRoute={setRoute} openOrder={openOrder} />}
{route === "aiAgents" && <AiAgentsPage agents={data.agents} releases={data.releases} reload={reload} setRoute={setRoute} openAgentCreate={openAgentCreate} openAgent={openAgent} openRelease={openRelease} />}
{route === "aiAgents" && <AiAgentsPage agents={data.agents} releases={data.releases} reload={reload} openAgentCreate={openAgentCreate} openAgent={openAgent} openRelease={openRelease} />}
{route === "aiAgentCreate" && <AiAgentCreatePage selectedProductCode={selectedProductCode} reload={reload} setRoute={setRoute} openAgent={openAgent} openRelease={openRelease} />}
{route === "aiAgentDetail" && <AiAgentDetailPage agentId={selectedAgentId} setRoute={setRoute} openRelease={openRelease} onAgentLoaded={onAgentLoaded} />}
{route === "aiRelease" && <ProductAIReleasePage releaseId={selectedReleaseId} setRoute={setRoute} onReleaseLoaded={onAgentLoaded} />}
{route === "aiTestChat" && <AiTestChatPage />}
{route === "aiAgentDetail" && <AiAgentDetailPage agentId={selectedAgentId} openRelease={openRelease} onAgentLoaded={onAgentLoaded} />}
{route === "aiRelease" && <ProductAIReleasePage releaseId={selectedReleaseId} onReleaseLoaded={onAgentLoaded} />}
{route === "integrations" && <IntegrationsPage />}
</>
);
-2
View File
@@ -53,7 +53,6 @@ export function routeFromPath(pathname: string, search = ""): RouteState {
const id = Number(path.split("/")[3]);
return Number.isInteger(id) && id > 0 ? { ...base, route: "aiRelease", releaseId: id } : { route: "aiAgents", ...base };
}
if (path === "/ai/test-chat") return { route: "aiTestChat", ...base };
if (path === "/ai/usage") return { route: "aiUsage", ...base };
if (path === "/integrations") return { route: "integrations", ...base };
if (path === "/profile") return { route: "profile", ...base };
@@ -77,7 +76,6 @@ export function pathFromRoute(route: RouteKey, entityId: number | null = null, p
if (route === "productDetail") return entityId ? `/products/${entityId}` : "/products";
if (route === "aiAgents") return "/ai/agents";
if (route === "aiAgentCreate") return productCode ? `/ai/agents/new?product=${encodeURIComponent(productCode)}` : "/ai/agents/new";
if (route === "aiTestChat") return "/ai/test-chat";
if (route === "aiUsage") return "/ai/usage";
if (route === "aiAgentDetail") return entityId ? `/ai/agents/${entityId}` : "/ai/agents";
if (route === "aiRelease") return entityId ? `/ai/releases/${entityId}` : "/ai/agents";
-1
View File
@@ -20,7 +20,6 @@ export const routes: Record<RouteKey, string> = {
aiAgentCreate: "Создание агента",
aiAgentDetail: "Карточка агента",
aiRelease: "Версия продукта",
aiTestChat: "Тестовый чат",
aiUsage: "Использование AI",
integrations: "Интеграции",
};
-3
View File
@@ -24,9 +24,6 @@
@import "./features/ai/release/composition.css";
@import "./features/ai/release/side.css";
@import "./features/ai/release/modal.css";
@import "./features/ai/test-chat/styles.css";
@import "./features/ai/test-chat/conversation.css";
@import "./features/ai/test-chat/diagnostics.css";
@import "./features/integrations/styles.css";
@import "./features/notifications/styles.css";
@import "./shared/state.css";
+1 -1
View File
@@ -92,7 +92,7 @@ export type ProductOffer = {
prices: ProductPrice[];
};
export type RouteKey = "command" | "departments" | "employeeDetail" | "employees" | "productDetail" | "products" | "profile" | "salesClientDetail" | "salesClients" | "salesDialogs" | "salesOrderDetail" | "salesOrders" | "salesOverview" | "supportOverview" | "supportDialogs" | "aiAgents" | "aiAgentCreate" | "aiAgentDetail" | "aiRelease" | "aiTestChat" | "aiUsage" | "integrations";
export type RouteKey = "command" | "departments" | "employeeDetail" | "employees" | "productDetail" | "products" | "profile" | "salesClientDetail" | "salesClients" | "salesDialogs" | "salesOrderDetail" | "salesOrders" | "salesOverview" | "supportOverview" | "supportDialogs" | "aiAgents" | "aiAgentCreate" | "aiAgentDetail" | "aiRelease" | "aiUsage" | "integrations";
export type AppData = {
employees: Employee[];
@@ -1,218 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<style>
*{box-sizing:border-box;}
html,body{margin:0;padding:0;}
::-webkit-scrollbar{width:8px;height:8px;}
::-webkit-scrollbar-thumb{background:#dcdcdc;border-radius:8px;border:2px solid transparent;background-clip:content-box;}
::-webkit-scrollbar-thumb:hover{background:#bfbfbf;background-clip:content-box;}
</style>
</helmet>
<div style="display:flex;height:100vh;overflow:hidden;background:#f0f2f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;color:#262626;-webkit-font-smoothing:antialiased;min-width:1180px;">
<!-- ============ SIDEBAR (уровень компании) ============ -->
<aside style="width:248px;flex:none;background:#ffffff;border-right:1px solid #f0f0f0;display:flex;flex-direction:column;height:100vh;">
<div style="height:56px;flex:none;display:flex;align-items:center;gap:11px;padding:0 20px;border-bottom:1px solid #f5f5f5;">
<div style="width:30px;height:30px;border-radius:8px;background:#1677ff;display:flex;align-items:center;justify-content:center;flex:none;"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l2 6 4-14 2 8h6"/></svg></div>
<div style="display:flex;flex-direction:column;line-height:1.15;"><span style="font-size:14px;font-weight:600;color:#1f1f1f;letter-spacing:-0.01em;">Edevs Hub</span><span style="font-size:11px;color:#8c8c8c;">Уровень компании</span></div>
</div>
<nav style="flex:1;overflow-y:auto;padding:12px 12px 8px;">
<a href="Командный центр.dc.html" style="display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:8px;color:#595959;font-size:13.5px;font-weight:500;text-decoration:none;margin-bottom:2px;" style-hover="background:#f5f5f5;color:#262626;"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9" rx="1.4"/><rect x="14" y="3" width="7" height="5" rx="1.4"/><rect x="14" y="12" width="7" height="9" rx="1.4"/><rect x="3" y="16" width="7" height="5" rx="1.4"/></svg>Командный центр</a>
<div style="font-size:11px;font-weight:600;letter-spacing:0.06em;color:#bfbfbf;padding:14px 12px 6px;">КОМПАНИЯ</div>
<a href="Отделы.dc.html" style="display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:8px;color:#595959;font-size:13.5px;font-weight:500;text-decoration:none;margin-bottom:2px;" style-hover="background:#f5f5f5;color:#262626;"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16"/><path d="M9 8h1.5M13.5 8H15M9 12h1.5M13.5 12H15M9 16h1.5M13.5 16H15"/></svg>Отделы</a>
<a href="Сотрудники.dc.html" style="display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:8px;color:#595959;font-size:13.5px;font-weight:500;text-decoration:none;margin-bottom:2px;" style-hover="background:#f5f5f5;color:#262626;"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>Сотрудники</a>
<a href="Продукты.dc.html" style="display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:8px;color:#595959;font-size:13.5px;font-weight:500;text-decoration:none;margin-bottom:2px;" style-hover="background:#f5f5f5;color:#262626;"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>Продукты</a>
<div style="font-size:11px;font-weight:600;letter-spacing:0.06em;color:#bfbfbf;padding:14px 12px 6px;">ПЛАТФОРМА</div>
<a href="AI-агенты.dc.html" style="display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:8px;background:#e6f4ff;color:#0958d9;font-size:13.5px;font-weight:600;text-decoration:none;margin-bottom:2px;position:relative;"><span style="position:absolute;left:0;top:8px;bottom:8px;width:3px;border-radius:0 3px 3px 0;background:#1677ff;"></span><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="8" width="16" height="12" rx="2.5"/><path d="M12 8V4.5"/><circle cx="12" cy="3.5" r="1.2"/><circle cx="9" cy="13.5" r="1"/><circle cx="15" cy="13.5" r="1"/><path d="M2 13.5h2M20 13.5h2"/></svg>AI</a>
<a href="#" style="display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:8px;color:#595959;font-size:13.5px;font-weight:500;text-decoration:none;margin-bottom:2px;" style-hover="background:#f5f5f5;color:#262626;"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 8V2.5M15 8V2.5M18 8v5.5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/><path d="M12 17.5V22"/></svg>Интеграции</a>
<div style="height:1px;background:#f5f5f5;margin:14px 12px 10px;"></div>
<a href="#" style="display:flex;align-items:center;gap:11px;padding:9px 12px;border-radius:8px;color:#595959;font-size:13.5px;font-weight:500;text-decoration:none;margin-bottom:2px;" style-hover="background:#f5f5f5;color:#262626;"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><line x1="21" y1="6" x2="9" y2="6"/><line x1="3" y1="6" x2="5" y2="6"/><circle cx="7" cy="6" r="2"/><line x1="21" y1="12" x2="13" y2="12"/><line x1="3" y1="12" x2="9" y2="12"/><circle cx="11" cy="12" r="2"/><line x1="21" y1="18" x2="15" y2="18"/><line x1="3" y1="18" x2="11" y2="18"/><circle cx="13" cy="18" r="2"/></svg>Настройки</a>
</nav>
<div style="flex:none;border-top:1px solid #f5f5f5;padding:10px 12px;">
<a href="#" style="display:flex;align-items:center;gap:11px;padding:8px 10px;border-radius:8px;text-decoration:none;" style-hover="background:#f5f5f5;"><div style="width:32px;height:32px;border-radius:50%;background:#1677ff;color:#fff;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:600;flex:none;">ИП</div><div style="display:flex;flex-direction:column;line-height:1.2;"><span style="font-size:13px;font-weight:600;color:#262626;">Иван Петров</span><span style="font-size:11px;color:#8c8c8c;">OWNER</span></div></a>
</div>
</aside>
<!-- ============ MAIN ============ -->
<div style="flex:1;min-width:0;display:flex;flex-direction:column;height:100vh;">
<header style="height:56px;flex:none;background:#ffffff;border-bottom:1px solid #f0f0f0;display:flex;align-items:center;justify-content:space-between;padding:0 24px;">
<div style="display:flex;align-items:center;gap:8px;font-size:13px;color:#8c8c8c;"><span style="font-weight:500;color:#595959;">Edevs</span><span style="color:#d9d9d9;">/</span><a href="AI-агенты.dc.html" style="font-weight:500;color:#595959;text-decoration:none;" style-hover="color:#1677ff;">AI</a><span style="color:#d9d9d9;">/</span><span style="font-weight:600;color:#262626;">Тестовый чат</span></div>
<div style="display:flex;align-items:center;gap:12px;">
<span style="display:inline-flex;align-items:center;gap:6px;padding:4px 11px;border-radius:6px;background:#fff7e6;border:1px solid #ffe58f;font-size:11.5px;font-weight:600;color:#d48806;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z"/></svg>Тестовая песочница</span>
<div style="width:1px;height:24px;background:#f0f0f0;"></div>
<div style="width:28px;height:28px;border-radius:50%;background:#1677ff;color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;">ИП</div>
</div>
</header>
<!-- 3 panels -->
<div style="flex:1;min-height:0;display:flex;overflow:hidden;">
<!-- ---- LEFT: scenarios / settings ---- -->
<section style="width:300px;flex:none;background:#fff;border-right:1px solid #f0f0f0;display:flex;flex-direction:column;min-height:0;">
<div style="flex:1;overflow-y:auto;padding:18px;">
<div style="font-size:11px;font-weight:600;letter-spacing:0.05em;color:#bfbfbf;margin-bottom:10px;">ЧЕРНОВИК ВЕРСИИ</div>
<div style="position:relative;margin-bottom:8px;">
<select style="width:100%;height:40px;padding:0 32px 0 12px;border:1px solid #e8e8e8;border-radius:9px;background:#fff;font-size:13px;color:#262626;font-family:inherit;cursor:pointer;appearance:none;-webkit-appearance:none;font-weight:500;"><option>REL-FP-v5 · Черновик версии</option><option>REL-FP-v4 · Опубликована</option></select>
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="#8c8c8c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="position:absolute;right:11px;top:13px;pointer-events:none;"><polyline points="6 9 12 15 18 9"/></svg>
</div>
<div style="display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:8px;background:#f9f0ff;border:1px solid #efdbff;margin-bottom:18px;">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="#722ed1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="8" width="16" height="12" rx="2.5"/><path d="M12 8V4.5"/><circle cx="12" cy="3.5" r="1.2"/></svg>
<span style="font-size:11.5px;color:#722ed1;">FirePage Sales · gpt-4o-mini · temp 0.4</span>
</div>
<div style="font-size:11px;font-weight:600;letter-spacing:0.05em;color:#bfbfbf;margin-bottom:10px;">КАНАЛ СИМУЛЯЦИИ</div>
<div style="display:flex;gap:6px;margin-bottom:18px;">
<button onClick="{{ onChMax }}" style="{{ chMax }}"><span style="width:6px;height:6px;border-radius:50%;background:#6b5be0;"></span>MAX</button>
<button onClick="{{ onChTg }}" style="{{ chTg }}"><span style="width:6px;height:6px;border-radius:50%;background:#2f8fd0;"></span>TG</button>
<button onClick="{{ onChWeb }}" style="{{ chWeb }}"><span style="width:6px;height:6px;border-radius:50%;background:#0f9b8e;"></span>Web</button>
</div>
<div style="font-size:11px;font-weight:600;letter-spacing:0.05em;color:#bfbfbf;margin-bottom:10px;">ГОТОВЫЕ СЦЕНАРИИ</div>
<div style="display:flex;flex-direction:column;gap:8px;">
<sc-for list="{{ scenarios }}" as="s" hint-placeholder-count="3">
<button onClick="{{ s.onPick }}" style="{{ s.style }}">
<div style="font-size:13px;font-weight:600;color:#262626;">{{ s.title }}</div>
<div style="font-size:11.5px;color:#8c8c8c;margin-top:2px;line-height:1.4;">{{ s.desc }}</div>
</button>
</sc-for>
</div>
</div>
<div style="flex:none;border-top:1px solid #f5f5f5;padding:12px 18px;">
<button onClick="{{ onClear }}" style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;height:38px;border-radius:9px;border:1px solid #e8e8e8;background:#fff;color:#595959;font-size:13px;font-weight:500;cursor:pointer;" style-hover="border-color:#ff4d4f;color:#cf1322;background:#fff8f7;"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>Очистить тестовую сессию</button>
</div>
</section>
<!-- ---- CENTER: test conversation ---- -->
<section style="flex:1;min-width:0;display:flex;flex-direction:column;background:#f7f8fa;min-height:0;">
<div style="flex:none;background:#fff;border-bottom:1px solid #f0f0f0;padding:11px 20px;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:11px;">
<div style="width:34px;height:34px;border-radius:9px;background:#f9f0ff;display:flex;align-items:center;justify-content:center;flex:none;"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#722ed1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="8" width="16" height="12" rx="2.5"/><path d="M12 8V4.5"/><circle cx="12" cy="3.5" r="1.2"/><circle cx="9" cy="13.5" r="1"/><circle cx="15" cy="13.5" r="1"/></svg></div>
<div><div style="font-size:14px;font-weight:700;color:#1f1f1f;">Test conversation</div><div style="font-size:11.5px;color:#8c8c8c;">Симуляция клиента · сообщений: 4</div></div>
</div>
<span style="font-size:11.5px;color:#bfbfbf;display:inline-flex;align-items:center;gap:5px;"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#bfbfbf" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.5 13.5 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" y1="2" x2="22" y2="22"/></svg>Не создаёт Contact, Order или Payment</span>
</div>
<div style="flex:1;overflow-y:auto;padding:18px 24px 8px;">
<div style="max-width:680px;margin:0 auto;">
<div style="text-align:center;margin-bottom:16px;"><span style="display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:20px;background:#eef0f2;font-size:11.5px;color:#8c8c8c;">Сценарий: «Покупка коробки» · канал {{ channelLabel }}</span></div>
<!-- client -->
<div style="display:flex;gap:10px;margin-bottom:14px;"><div style="width:30px;height:30px;border-radius:50%;background:#8c8c8c;color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;flex:none;">Т</div><div style="max-width:78%;"><div style="background:#fff;border:1px solid #eee;border-radius:4px 14px 14px 14px;padding:10px 14px;font-size:13.5px;line-height:1.5;color:#262626;">Здравствуйте, интересует FirePage для сайта-визитки. Сколько стоит?</div><div style="font-size:11px;color:#bfbfbf;margin:4px 0 0 4px;">тестовый клиент</div></div></div>
<!-- AI -->
<div style="display:flex;flex-direction:row-reverse;gap:10px;margin-bottom:14px;"><div style="width:30px;height:30px;border-radius:50%;background:#f9f0ff;border:1px solid #efdbff;display:flex;align-items:center;justify-content:center;flex:none;"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#722ed1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="8" width="16" height="12" rx="2.5"/><path d="M12 8V4.5"/><circle cx="12" cy="3.5" r="1.2"/></svg></div><div style="max-width:78%;"><div style="display:flex;align-items:center;gap:6px;justify-content:flex-end;margin-bottom:4px;"><span style="font-size:11px;font-weight:600;color:#722ed1;">AI · REL-FP-v5</span></div><div style="background:#f9f0ff;border:1px solid #efdbff;border-radius:14px 4px 14px 14px;padding:10px 14px;font-size:13.5px;line-height:1.5;color:#262626;">Здравствуйте! FirePage — это готовый нишевой сайт под ключ. Коробка стоит <b>₽4 900</b> единоразово. Дополнительно можно подключить годовую поддержку за ₽1 470. Под визитку отлично подходит — оформить?</div><div style="font-size:11px;color:#bfbfbf;margin:4px 4px 0 0;text-align:right;">debug ниже →</div></div></div>
<!-- client -->
<div style="display:flex;gap:10px;margin-bottom:14px;"><div style="width:30px;height:30px;border-radius:50%;background:#8c8c8c;color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;flex:none;">Т</div><div style="max-width:78%;"><div style="background:#fff;border:1px solid #eee;border-radius:4px 14px 14px 14px;padding:10px 14px;font-size:13.5px;line-height:1.5;color:#262626;">Да, давайте оформим коробку.</div></div></div>
<!-- AI with tool -->
<div style="display:flex;flex-direction:row-reverse;gap:10px;margin-bottom:8px;"><div style="width:30px;height:30px;border-radius:50%;background:#f9f0ff;border:1px solid #efdbff;display:flex;align-items:center;justify-content:center;flex:none;"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#722ed1" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="8" width="16" height="12" rx="2.5"/><path d="M12 8V4.5"/><circle cx="12" cy="3.5" r="1.2"/></svg></div><div style="max-width:78%;"><div style="background:#f9f0ff;border:1px solid #efdbff;border-radius:14px 4px 14px 14px;padding:10px 14px;font-size:13.5px;line-height:1.5;color:#262626;">Отлично! Подготовил оформление коробки FirePage за ₽4 900. Вызываю checkout — в реальном диалоге клиент получил бы ссылку на оплату.</div><div style="display:inline-flex;align-items:center;gap:5px;margin:6px 4px 0 0;float:right;clear:both;"><span style="padding:2px 8px;border-radius:5px;background:#e6f4ff;color:#0958d9;font-size:10.5px;font-weight:600;font-family:'SF Mono',ui-monospace,Menlo,monospace;">tool: create_checkout</span></div></div></div>
</div>
</div>
<!-- composer -->
<div style="flex:none;background:#fff;border-top:1px solid #f0f0f0;padding:14px 24px 16px;">
<div style="max-width:680px;margin:0 auto;">
<div style="display:flex;align-items:flex-end;gap:10px;border:1px solid #e8e8e8;border-radius:12px;padding:8px 8px 8px 14px;background:#fff;" style-focus="border-color:#1677ff;">
<textarea rows="1" placeholder="Сообщение от лица тестового клиента…" style="flex:1;border:none;outline:none;resize:none;font-size:13.5px;line-height:1.5;color:#262626;font-family:inherit;padding:7px 0;max-height:120px;"></textarea>
<button style="display:flex;align-items:center;gap:7px;height:38px;padding:0 16px;border-radius:9px;border:none;background:#1677ff;color:#fff;font-size:13px;font-weight:600;cursor:pointer;flex:none;" style-hover="background:#0958d9;">Отправить<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button>
</div>
</div>
</div>
</section>
<!-- ---- RIGHT: diagnostics ---- -->
<section style="width:340px;flex:none;background:#fff;border-left:1px solid #f0f0f0;display:flex;flex-direction:column;min-height:0;">
<div style="flex:none;padding:15px 18px 12px;border-bottom:1px solid #f0f0f0;"><h3 style="margin:0;font-size:14px;font-weight:700;color:#1f1f1f;">Диагностика</h3><div style="font-size:11.5px;color:#bfbfbf;margin-top:2px;">Последний ответ AI</div></div>
<div style="flex:1;overflow-y:auto;padding:16px 18px;">
<!-- tokens / cost -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1px;background:#f0f0f0;border:1px solid #f0f0f0;border-radius:9px;overflow:hidden;margin-bottom:18px;">
<div style="background:#fff;padding:12px 14px;"><div style="font-size:11px;color:#8c8c8c;">Tokens</div><div style="font-size:17px;font-weight:700;color:#262626;margin-top:3px;">1 284</div><div style="font-size:10.5px;color:#bfbfbf;margin-top:1px;">in 940 · out 344</div></div>
<div style="background:#fff;padding:12px 14px;"><div style="font-size:11px;color:#8c8c8c;">Cost</div><div style="font-size:17px;font-weight:700;color:#262626;margin-top:3px;">₽0,42</div><div style="font-size:10.5px;color:#bfbfbf;margin-top:1px;">latency 3,1с</div></div>
</div>
<!-- used chunks -->
<div style="font-size:11px;font-weight:600;letter-spacing:0.05em;color:#bfbfbf;margin-bottom:9px;">ИСПОЛЬЗОВАННЫЕ CHUNKS</div>
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:20px;">
<sc-for list="{{ chunks }}" as="c" hint-placeholder-count="2">
<div style="border:1px solid #f0f0f0;border-radius:9px;padding:10px 12px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;"><span style="font-size:12px;font-weight:600;color:#262626;">{{ c.source }}</span><span style="font-size:10.5px;font-weight:600;color:#389e0d;background:#f6ffed;padding:1px 6px;border-radius:5px;">{{ c.score }}</span></div>
<div style="font-size:11.5px;color:#8c8c8c;line-height:1.45;">{{ c.text }}</div>
</div>
</sc-for>
</div>
<!-- tools called -->
<div style="font-size:11px;font-weight:600;letter-spacing:0.05em;color:#bfbfbf;margin-bottom:9px;">ВЫЗВАННЫЕ TOOLS</div>
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:20px;">
<div style="display:flex;align-items:center;gap:10px;border:1px solid #f0f0f0;border-radius:9px;padding:10px 12px;"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="#389e0d" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="flex:none;"><path d="M20 6 9 17l-5-5"/></svg><div style="flex:1;min-width:0;"><div style="font-size:12px;font-weight:600;color:#262626;font-family:'SF Mono',ui-monospace,Menlo,monospace;">get_offers</div><div style="font-size:11px;color:#8c8c8c;">product=firepage → 2 offers</div></div><span style="font-size:10.5px;color:#bfbfbf;">120мс</span></div>
<div style="display:flex;align-items:center;gap:10px;border:1px solid #f0f0f0;border-radius:9px;padding:10px 12px;"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="#389e0d" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="flex:none;"><path d="M20 6 9 17l-5-5"/></svg><div style="flex:1;min-width:0;"><div style="font-size:12px;font-weight:600;color:#262626;font-family:'SF Mono',ui-monospace,Menlo,monospace;">create_checkout</div><div style="font-size:11px;color:#8c8c8c;">offer=OFR-FP-BOX · ₽4 900 <span style="color:#d48806;">(симуляция)</span></div></div><span style="font-size:10.5px;color:#bfbfbf;">88мс</span></div>
</div>
<!-- handoff -->
<div style="font-size:11px;font-weight:600;letter-spacing:0.05em;color:#bfbfbf;margin-bottom:9px;">HANDOFF</div>
<div style="display:flex;align-items:center;gap:10px;border:1px solid #f0f0f0;border-radius:9px;padding:11px 13px;background:#fafafa;"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#52c41a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex:none;"><path d="M20 6 9 17l-5-5"/></svg><span style="font-size:12px;color:#595959;">Передачи оператору не было — сценарий в рамках агента.</span></div>
</div>
</section>
</div>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{
&quot;$preview&quot;: { &quot;width&quot;: 1440, &quot;height&quot;: 900 }
}">
class Component extends DCLogic {
state = { channel: 'MAX', scenario: 'box' };
renderVals() {
const { channel, scenario } = this.state;
const channelLabel = { MAX: 'MAX', TG: 'Telegram', WEB: 'Web Chat' }[channel];
const chOff = 'display:inline-flex;align-items:center;gap:6px;flex:1;justify-content:center;height:32px;border:1px solid #e8e8e8;border-radius:8px;background:#fff;color:#595959;font-size:12px;font-weight:500;cursor:pointer;';
const chOn = 'display:inline-flex;align-items:center;gap:6px;flex:1;justify-content:center;height:32px;border:1px solid #1677ff;border-radius:8px;background:#e6f4ff;color:#0958d9;font-size:12px;font-weight:600;cursor:pointer;';
const scenData = [
{ id: 'box', title: 'Покупка коробки', desc: 'Клиент хочет купить FirePage под визитку.' },
{ id: 'support', title: 'Вопрос про поддержку', desc: 'Уточняет условия годовой поддержки.' },
{ id: 'handoff', title: 'Запрос человека', desc: 'Просит соединить с менеджером (handoff).' },
];
const scOff = 'text-align:left;border:1px solid #e8e8e8;border-radius:10px;padding:11px 13px;background:#fff;cursor:pointer;';
const scOn = 'text-align:left;border:1px solid #1677ff;border-radius:10px;padding:11px 13px;background:#f7fbff;cursor:pointer;';
const scenarios = scenData.map((s) => ({
title: s.title, desc: s.desc,
style: scenario === s.id ? scOn : scOff,
onPick: () => this.setState({ scenario: s.id }),
}));
const chunks = [
{ source: 'Что входит в коробку FirePage', score: '0.91', text: '«Коробка FirePage — готовый нишевой сайт под ключ, единоразовая оплата ₽4 900…»' },
{ source: 'Условия годовой поддержки', score: '0.84', text: '«Годовая поддержка — 30% от цены коробки, ₽1 470/год, продление по желанию…»' },
];
return {
channelLabel, scenarios, chunks,
chMax: channel === 'MAX' ? chOn : chOff,
chTg: channel === 'TG' ? chOn : chOff,
chWeb: channel === 'WEB' ? chOn : chOff,
onChMax: () => this.setState({ channel: 'MAX' }),
onChTg: () => this.setState({ channel: 'TG' }),
onChWeb: () => this.setState({ channel: 'WEB' }),
onClear: () => {},
};
}
}
</script>
</body>
</html>