mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
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:
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;
|
||||
}
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -20,7 +20,6 @@ export const routes: Record<RouteKey, string> = {
|
||||
aiAgentCreate: "Создание агента",
|
||||
aiAgentDetail: "Карточка агента",
|
||||
aiRelease: "Версия продукта",
|
||||
aiTestChat: "Тестовый чат",
|
||||
aiUsage: "Использование AI",
|
||||
integrations: "Интеграции",
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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="{
|
||||
"$preview": { "width": 1440, "height": 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>
|
||||
Reference in new issue
Block a user