diff --git a/apps/backend/hub_platform/ai/limits.py b/apps/backend/hub_platform/ai/limits.py
index e9ae220..1d1a23e 100644
--- a/apps/backend/hub_platform/ai/limits.py
+++ b/apps/backend/hub_platform/ai/limits.py
@@ -25,6 +25,8 @@ def assert_within_limits(channel, agent) -> None:
global_limit = settings.HUB_AI_GLOBAL_DAILY_COST_LIMIT_MICROS
if global_limit and daily_cost_micros() >= global_limit:
raise LimitExceeded("Global daily AI cost limit reached")
- channel_limit = (agent.limits or {}).get("dailyCostMicros")
- if channel_limit and daily_cost_micros(channel) >= channel_limit:
+ # Канальный лимит хранится в целых центах USD (dailyCostUsd); расход учитывается
+ # в micro-USD. 1 цент = 10 000 micro-USD.
+ channel_limit = (agent.limits or {}).get("dailyCostUsd")
+ if channel_limit and daily_cost_micros(channel) >= int(channel_limit) * 10_000:
raise LimitExceeded("Channel daily AI cost limit reached")
diff --git a/apps/backend/hub_platform/ai/services.py b/apps/backend/hub_platform/ai/services.py
index c358aed..c486856 100644
--- a/apps/backend/hub_platform/ai/services.py
+++ b/apps/backend/hub_platform/ai/services.py
@@ -34,6 +34,20 @@ class AgentCreateInput:
knowledge_document_ids: list[int]
+# Единственный поддерживаемый лимит агента — дневной бюджет в целых центах USD
+# (dailyCostUsd). Прочие исторические ключи (dailyCostMicros, dailyBudgetRub,
+# dailyDialogs, maxMessagesPerDialog) бэкендом не используются и отбрасываются.
+def _normalize_limits(raw: dict | None) -> dict:
+ if not isinstance(raw, dict):
+ return {}
+ value = raw.get("dailyCostUsd")
+ try:
+ cents = int(value)
+ except (TypeError, ValueError):
+ return {}
+ return {"dailyCostUsd": cents} if cents > 0 else {}
+
+
START_PROMPTS: tuple[tuple[str, str, str], ...] = (
("system", "Системный prompt", PromptCategory.SYSTEM),
("qualification", "Квалификация", PromptCategory.QUALIFICATION),
@@ -122,7 +136,7 @@ def update_agent(*, agent: AIAgent, data: AgentInput) -> AIAgent:
agent.model = data.model
agent.model_params = data.model_params
agent.allowed_tools = data.allowed_tools
- agent.limits = data.limits
+ agent.limits = _normalize_limits(data.limits)
agent.save(update_fields=["name", "model", "model_params", "allowed_tools", "limits", "updated_at"])
return agent
diff --git a/apps/backend/hub_platform/ai/tests.py b/apps/backend/hub_platform/ai/tests.py
index d25bda3..6b5fe80 100644
--- a/apps/backend/hub_platform/ai/tests.py
+++ b/apps/backend/hub_platform/ai/tests.py
@@ -450,10 +450,11 @@ class ChatInvocationTests(TestCase):
from hub_platform.ai.models import LlmInvocation, LlmInvocationStatus
from hub_platform.ai.provider.base import ChatMessage
- self.agent.limits = {"dailyCostMicros": 1}
+ self.agent.limits = {"dailyCostUsd": 1}
self.agent.save(update_fields=["limits"])
+ # 1 цент = 10 000 micro-USD; лимит превышен расходом в 10_001 micros.
LlmInvocation.objects.create(
- channel=self.channel, purpose="seed", operation="chat", model="x", cost_micros=10,
+ channel=self.channel, purpose="seed", operation="chat", model="x", cost_micros=10_001,
status=LlmInvocationStatus.SUCCESS,
)
diff --git a/apps/backend/hub_platform/conversations/ingest.py b/apps/backend/hub_platform/conversations/ingest.py
index f9ec309..5c9f791 100644
--- a/apps/backend/hub_platform/conversations/ingest.py
+++ b/apps/backend/hub_platform/conversations/ingest.py
@@ -12,6 +12,7 @@ import logging
from django.db import IntegrityError, transaction
from django.utils import timezone
+from hub_platform.ai.limits import LimitExceeded
from hub_platform.ai.provider.base import ProviderError
from hub_platform.ai.runtime import HANDOFF_TOKEN
from hub_platform.channels.runtime import run_channel_turn
@@ -142,9 +143,10 @@ def ingest_inbound(integration, inbound: InboundMessage) -> None:
try:
result = run_channel_turn(channel=channel, message=inbound.text, history=_history(conversation))
- except ProviderError as error:
- # Сбой AI не должен «терять» сообщение: переводим диалог в очередь к
- # оператору, уведомляем и отвечаем клиенту понятным fallback.
+ except (ProviderError, LimitExceeded) as error:
+ # Сбой AI (провайдер недоступен) или срабатывание лимита стоимости не должны
+ # «терять» сообщение: переводим диалог в очередь к оператору, уведомляем и
+ # отвечаем клиенту понятным fallback.
logger.warning("AI turn failed for conversation %s: %s", conversation.id, error)
conversation.control_mode = ControlMode.PAUSED
conversation.expected_responder = ExpectedResponder.OPERATOR
diff --git a/apps/backend/hub_platform/conversations/tests.py b/apps/backend/hub_platform/conversations/tests.py
new file mode 100644
index 0000000..0042ff0
--- /dev/null
+++ b/apps/backend/hub_platform/conversations/tests.py
@@ -0,0 +1,66 @@
+from unittest import mock
+
+from django.test import TestCase
+
+from hub_platform.ai.limits import LimitExceeded
+from hub_platform.ai.models import AIAgent
+from hub_platform.channels.models import Channel
+from hub_platform.conversations.models import ControlMode, ExpectedResponder, Message, MessageAuthor
+from hub_platform.conversations.transports.base import InboundMessage
+from hub_platform.identity.bootstrap import bootstrap_edevs_owner
+from hub_platform.identity.models import Organization
+from hub_platform.integrations.models import Integration, IntegrationKind, IntegrationProvider
+
+
+def _messenger_connection(channel):
+ return Integration.objects.create(
+ organization=channel.organization,
+ kind=IntegrationKind.MESSENGER,
+ provider=IntegrationProvider.TELEGRAM,
+ name="test-bot",
+ channel=channel,
+ )
+
+
+class IngestLimitHandlingTests(TestCase):
+ """При срабатывании дневного лимита стоимости (LimitExceeded) диалог не должен
+ «зависать»: его передают оператору с fallback-ответом клиенту (как при сбое
+ провайдера). См. ingest.ingest_inbound.
+ """
+
+ def setUp(self) -> None:
+ bootstrap_edevs_owner(email="owner@edevs.tech", password="temporary-password")
+ self.organization = Organization.objects.get(slug="edevs")
+ self.channel = Channel.objects.create(organization=self.organization, code="foxray-sales", name="FoxRay — продажи")
+ AIAgent.objects.create(channel=self.channel, name="FoxRay Agent", model="openai/gpt-4o-mini")
+ self.integration = _messenger_connection(self.channel)
+ self.inbound = InboundMessage(
+ external_id="ext-1", user_id="user-1", chat_id="chat-1", text="Здравствуйте", display_name="Гость"
+ )
+
+ def test_limit_exceeded_hands_off_to_operator(self) -> None:
+ from hub_platform.conversations.ingest import ingest_inbound
+
+ with (
+ mock.patch(
+ "hub_platform.conversations.ingest.run_channel_turn",
+ side_effect=LimitExceeded("Channel daily AI cost limit reached"),
+ ),
+ mock.patch("hub_platform.conversations.ingest.transports.send_reply", return_value=True) as send,
+ ):
+ ingest_inbound(self.integration, self.inbound)
+
+ conversation = self.channel.conversations.get()
+ # Диалог передан оператору, ответчик — оператор.
+ self.assertEqual(conversation.control_mode, ControlMode.PAUSED)
+ self.assertEqual(conversation.expected_responder, ExpectedResponder.OPERATOR)
+
+ # Сохранены: входящее сообщение, системная отметка и fallback ИИ.
+ authors = list(self.channel.conversations.get().messages.values_list("author_type", flat=True))
+ self.assertIn(MessageAuthor.CONTACT, authors)
+ self.assertIn(MessageAuthor.SYSTEM, authors)
+ self.assertIn(MessageAuthor.AI, authors)
+
+ # Fallback отправлен в канал.
+ send.assert_called_once()
+ self.assertIn("специалисту", send.call_args.kwargs["text"])
diff --git a/apps/internal-ui/src/features/ai/AgentsTable.tsx b/apps/internal-ui/src/features/ai/AgentsTable.tsx
index 8738e71..f3fde19 100644
--- a/apps/internal-ui/src/features/ai/AgentsTable.tsx
+++ b/apps/internal-ui/src/features/ai/AgentsTable.tsx
@@ -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({
—
-
setRoute("aiTestChat")}>Тест-чат
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(null);
async function toggleActive(agent: AiAgent) {
@@ -22,7 +21,7 @@ export function AiAgentsPage({ agents, releases, reload, setRoute, openAgentCrea
openAgentCreate(null)}>Создать агента} />
-
+
);
}
diff --git a/apps/internal-ui/src/features/ai/AiSubnav.tsx b/apps/internal-ui/src/features/ai/AiSubnav.tsx
index 73059bf..70f46d4 100644
--- a/apps/internal-ui/src/features/ai/AiSubnav.tsx
+++ b/apps/internal-ui/src/features/ai/AiSubnav.tsx
@@ -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 },
];
diff --git a/apps/internal-ui/src/features/ai/detail/AgentEditForm.tsx b/apps/internal-ui/src/features/ai/detail/AgentEditForm.tsx
index e13fe34..e54ec21 100644
--- a/apps/internal-ui/src/features/ai/detail/AgentEditForm.tsx
+++ b/apps/internal-ui/src/features/ai/detail/AgentEditForm.tsx
@@ -10,23 +10,25 @@ import type { AiAgentDetail } from "./model";
// Известные инструменты системы (services.py: operator-handoff). Хранятся как string[].
const TOOLS: Array<[string, string]> = [["operator-handoff", "Передача оператору"]];
-// Фиксированный набор лимитов (release/model.ts limitLabel). Числовые поля.
-const LIMIT_FIELDS: Array<[string, string]> = [
- ["dailyDialogs", "Диалогов в день"],
- ["maxMessagesPerDialog", "Макс. сообщений / диалог"],
- ["dailyBudgetRub", "Бюджет в день, ₽"],
- ["dailyCostMicros", "Бюджет в день, micros"],
-];
+// Дневной бюджет хранится в целых центах USD (dailyCostUsd); в поле вводим доллары.
+function budgetFromCents(limits: Record): string {
+ const cents = limits?.dailyCostUsd;
+ if (typeof cents !== "number" || cents <= 0) return "";
+ return (cents / 100).toString();
+}
-function isNumber(value: string): boolean {
- return value === "" || /^\d+$/.test(value.trim());
+// Допускаются неотрицательные доллары: до 2 знаков после запятой, разделитель «.» или «,».
+function isBudget(value: string): boolean {
+ const trimmed = value.trim();
+ if (trimmed === "") return true;
+ return /^\d+([.,]\d{1,2})?$/.test(trimmed);
}
export function AgentEditForm({ agent, onClose, onSaved }: { agent: AiAgentDetail; onClose: () => void; onSaved: () => void }) {
const [name, setName] = useState(agent.name);
const [model, setModel] = useState(agent.model);
const [tools, setTools] = useState(Array.isArray(agent.allowedTools) ? (agent.allowedTools as string[]) : []);
- const [limits, setLimits] = useState>({ ...(agent.limits as Record) });
+ const [budget, setBudget] = useState(budgetFromCents(agent.limits as Record));
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
@@ -34,30 +36,18 @@ export function AgentEditForm({ agent, onClose, onSaved }: { agent: AiAgentDetai
setTools((current) => (current.includes(code) ? current.filter((item) => item !== code) : [...current, code]));
}
- function setLimit(key: string, value: string) {
- setLimits((current) => {
- const next = { ...current };
- const trimmed = value.trim();
- next[key] = trimmed === "" ? Number.NaN : Number(trimmed);
- return next;
- });
- }
-
- const limitsValid = Object.values(limits).every((value) => Number.isFinite(value));
- const ready = name.trim().length > 0 && limitsValid;
+ const ready = name.trim().length > 0 && isBudget(budget);
async function submit() {
if (!ready) return;
setSubmitting(true);
setError(null);
- const cleanLimits: Record = {};
- for (const [key, value] of Object.entries(limits)) {
- if (Number.isFinite(value)) cleanLimits[key] = value;
- }
+ const dollars = parseFloat(budget.trim().replace(",", "."));
+ const limits: Record = Number.isFinite(dollars) && dollars > 0 ? { dailyCostUsd: Math.round(dollars * 100) } : {};
try {
await api(`/api/v1/ai/agents/${agent.id}/update/`, {
method: "PATCH",
- body: JSON.stringify({ name: name.trim(), model, allowedTools: tools, limits: cleanLimits }),
+ body: JSON.stringify({ name: name.trim(), model, allowedTools: tools, limits }),
});
onSaved();
} catch (caught) {
@@ -86,18 +76,17 @@ export function AgentEditForm({ agent, onClose, onSaved }: { agent: AiAgentDetai
{error && {error}
}
diff --git a/apps/internal-ui/src/features/ai/detail/AiAgentDetailHeader.tsx b/apps/internal-ui/src/features/ai/detail/AiAgentDetailHeader.tsx
index 9dd8dcb..3e39bff 100644
--- a/apps/internal-ui/src/features/ai/detail/AiAgentDetailHeader.tsx
+++ b/apps/internal-ui/src/features/ai/detail/AiAgentDetailHeader.tsx
@@ -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({
- setRoute("aiTestChat")}>Тест-чат
Создать черновик версии
diff --git a/apps/internal-ui/src/features/ai/detail/AiAgentDetailPage.tsx b/apps/internal-ui/src/features/ai/detail/AiAgentDetailPage.tsx
index 459744c..11aca0d 100644
--- a/apps/internal-ui/src/features/ai/detail/AiAgentDetailPage.tsx
+++ b/apps/internal-ui/src/features/ai/detail/AiAgentDetailPage.tsx
@@ -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("overview");
const [editOpen, setEditOpen] = useState(false);
@@ -50,7 +49,7 @@ export function AiAgentDetailPage({ agentId, setRoute, openRelease, onAgentLoade
return (
-
setChannelEdit(true)} />
+ setChannelEdit(true)} />
{(tab === "instructions" || tab === "knowledge") && (
diff --git a/apps/internal-ui/src/features/ai/detail/model.ts b/apps/internal-ui/src/features/ai/detail/model.ts
index eceede4..0d66c4b 100644
--- a/apps/internal-ui/src/features/ai/detail/model.ts
+++ b/apps/internal-ui/src/features/ai/detail/model.ts
@@ -128,9 +128,9 @@ export function releaseDisplayName(release: AiReleaseFull): string {
return `REL-${prefix.toUpperCase()}-v${release.version}`;
}
-// agent.limits — свободный JSON; пока бэкенд хранит только дневной лимит стоимости (micro-USD).
+// agent.limits — единый поддерживаемый лимит: дневной бюджет в целых центах USD (dailyCostUsd).
export function dailyBudget(limits: Record): string {
- const micros = limits?.dailyCostMicros;
- if (typeof micros !== "number" || micros <= 0) return "—";
- return `$${(micros / 1_000_000).toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} / день`;
+ const cents = limits?.dailyCostUsd;
+ if (typeof cents !== "number" || cents <= 0) return "—";
+ return `$${(cents / 100).toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} / день`;
}
diff --git a/apps/internal-ui/src/features/ai/release/ProductAIReleaseHeader.tsx b/apps/internal-ui/src/features/ai/release/ProductAIReleaseHeader.tsx
index 48a3fc4..aadd185 100644
--- a/apps/internal-ui/src/features/ai/release/ProductAIReleaseHeader.tsx
+++ b/apps/internal-ui/src/features/ai/release/ProductAIReleaseHeader.tsx
@@ -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({
Сравнить с опубликованной
-
setRoute("aiTestChat")}>Тест-чат
{release.status === "PUBLISHED" ? "Опубликована" : "Опубликовать"}
diff --git a/apps/internal-ui/src/features/ai/release/ProductAIReleasePage.tsx b/apps/internal-ui/src/features/ai/release/ProductAIReleasePage.tsx
index 0bf55d8..875deb6 100644
--- a/apps/internal-ui/src/features/ai/release/ProductAIReleasePage.tsx
+++ b/apps/internal-ui/src/features/ai/release/ProductAIReleasePage.tsx
@@ -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 (
-
setModalOpen(true)} />
+ setModalOpen(true)} />
diff --git a/apps/internal-ui/src/features/ai/release/ReleaseComposition.tsx b/apps/internal-ui/src/features/ai/release/ReleaseComposition.tsx
index c3f9b69..ea0f1c9 100644
--- a/apps/internal-ui/src/features/ai/release/ReleaseComposition.tsx
+++ b/apps/internal-ui/src/features/ai/release/ReleaseComposition.tsx
@@ -1,8 +1,8 @@
import type { ReactNode } from "react";
import { Icon } from "../../../shared/icons";
-import type { AiReleaseFull, KnowledgeDoc, PromptDoc } from "../detail/model";
-import { formatParam, isPromptChanged, knowledgeTitle, limitLabel, promptTitle, promptVersionDate, toolLabel } from "./model";
+import { dailyBudget, type AiReleaseFull, type KnowledgeDoc, type PromptDoc } from "../detail/model";
+import { formatParam, isPromptChanged, knowledgeTitle, promptTitle, promptVersionDate, toolLabel } from "./model";
function ReleaseCard({ children, className = "", title, icon }: { children: ReactNode; className?: string; icon?: ReactNode; title: string }) {
return (
@@ -30,7 +30,7 @@ function VersionRow({ changed, date, title, version }: { changed?: boolean; date
export function ReleaseComposition({ knowledge, prompts, published, release }: { knowledge: KnowledgeDoc[]; prompts: PromptDoc[]; published?: AiReleaseFull; release: AiReleaseFull }) {
const modelParams = release.modelParams ?? {};
const tools = release.allowedTools.map(toolLabel).filter((tool) => tool !== "—");
- const limitRows = Object.entries(release.limits ?? {});
+ const budget = dailyBudget(release.limits ?? {});
return (
@@ -86,15 +86,13 @@ export function ReleaseComposition({ knowledge, prompts, published, release }: {
)}
- {limitRows.length === 0 ? (
+ {budget === "—" ? (
) : (
- limitRows.map(([key, value]) => (
-
- {limitLabel(key)}
- {formatParam(value)}
-
- ))
+
+ Бюджет в день
+ {budget}
+
)}
diff --git a/apps/internal-ui/src/features/ai/release/model.test.ts b/apps/internal-ui/src/features/ai/release/model.test.ts
index 4cad5e0..00bddaa 100644
--- a/apps/internal-ui/src/features/ai/release/model.test.ts
+++ b/apps/internal-ui/src/features/ai/release/model.test.ts
@@ -14,7 +14,7 @@ function buildRelease(overrides: Partial
= {}): AiReleaseFull {
model: "anthropic/claude-sonnet-4.6",
modelParams: {},
allowedTools: [{ name: "handoff" }],
- limits: { maxMessagesPerDialog: 12 },
+ limits: { dailyCostUsd: 500 },
notes: "",
retrievalIndexVersion: "",
knowledgeVersions: [{ document: "overview", version: 1 }],
diff --git a/apps/internal-ui/src/features/ai/release/model.ts b/apps/internal-ui/src/features/ai/release/model.ts
index 1ef383e..f94e012 100644
--- a/apps/internal-ui/src/features/ai/release/model.ts
+++ b/apps/internal-ui/src/features/ai/release/model.ts
@@ -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: "Готов к публикации" };
}
@@ -133,13 +131,3 @@ export function isPromptChanged(published: AiReleaseFull | undefined, code: stri
export function isKnowledgeChanged(published: AiReleaseFull | undefined, code: string, version: number): boolean {
return published ? !published.knowledgeVersions.some((item) => item.document === code && item.version === version) : true;
}
-
-export function limitLabel(key: string): string {
- const labels: Record = {
- dailyBudgetRub: "Бюджет в день",
- dailyCostMicros: "Бюджет в день",
- dailyDialogs: "Диалогов в день",
- maxMessagesPerDialog: "Макс. сообщений / диалог",
- };
- return labels[key] ?? key;
-}
diff --git a/apps/internal-ui/src/features/ai/release/styles.css b/apps/internal-ui/src/features/ai/release/styles.css
index 5194c6c..d39d36c 100644
--- a/apps/internal-ui/src/features/ai/release/styles.css
+++ b/apps/internal-ui/src/features/ai/release/styles.css
@@ -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;
diff --git a/apps/internal-ui/src/features/ai/styles.css b/apps/internal-ui/src/features/ai/styles.css
index 4a2e3c7..0ed486a 100644
--- a/apps/internal-ui/src/features/ai/styles.css
+++ b/apps/internal-ui/src/features/ai/styles.css
@@ -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;
diff --git a/apps/internal-ui/src/features/ai/test-chat/AiTestChatPage.tsx b/apps/internal-ui/src/features/ai/test-chat/AiTestChatPage.tsx
deleted file mode 100644
index 71440b7..0000000
--- a/apps/internal-ui/src/features/ai/test-chat/AiTestChatPage.tsx
+++ /dev/null
@@ -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("MAX");
- const [scenario, setScenario] = useState("box");
-
- return (
-
-
-
-
-
- );
-}
diff --git a/apps/internal-ui/src/features/ai/test-chat/TestChatDiagnostics.tsx b/apps/internal-ui/src/features/ai/test-chat/TestChatDiagnostics.tsx
deleted file mode 100644
index 99eaa18..0000000
--- a/apps/internal-ui/src/features/ai/test-chat/TestChatDiagnostics.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Icon } from "../../../shared/icons";
-import { diagnosticChunks } from "./model";
-
-export function TestChatDiagnostics() {
- return (
-
-
-
Диагностика
- Последний ответ AI
-
-
-
-
Tokens 1 284 in 940 · out 344
-
Cost ₽0,42 latency 3,1с
-
-
-
ИСПОЛЬЗОВАННЫЕ ФРАГМЕНТЫ
-
- {diagnosticChunks.map((chunk) => (
-
-
{chunk.source} {chunk.score}
-
{chunk.text}
-
- ))}
-
-
-
ВЫЗВАННЫЕ ИНСТРУМЕНТЫ
-
-
-
-
-
-
ПЕРЕДАЧА ОПЕРАТОРУ
-
Передачи оператору не было — сценарий в рамках агента.
-
-
- );
-}
-
-function PanelTitle({ children }: { children: string }) {
- return {children}
;
-}
-
-function ToolCall({ meta, name, time }: { meta: string; name: string; time: string }) {
- return (
-
-
-
{name} {meta}
-
{time}
-
- );
-}
diff --git a/apps/internal-ui/src/features/ai/test-chat/TestChatSidebar.tsx b/apps/internal-ui/src/features/ai/test-chat/TestChatSidebar.tsx
deleted file mode 100644
index e0f698f..0000000
--- a/apps/internal-ui/src/features/ai/test-chat/TestChatSidebar.tsx
+++ /dev/null
@@ -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 (
-
-
-
ЧЕРНОВИК ВЕРСИИ
-
-
- REL-FP-v5 · Черновик версии
- REL-FP-v4 · Опубликована
-
-
-
-
-
- FirePage Sales · gpt-4o-mini · temp 0.4
-
-
-
КАНАЛ СИМУЛЯЦИИ
-
- {testChannels.map((item) => (
- setChannel(item.key)} key={item.key}>
-
- {item.label}
-
- ))}
-
-
-
ГОТОВЫЕ СЦЕНАРИИ
-
- {testScenarios.map((item) => (
- setScenario(item.id)} key={item.id}>
- {item.title}
- {item.desc}
-
- ))}
-
-
-
- Очистить тестовую сессию
-
-
- );
-}
diff --git a/apps/internal-ui/src/features/ai/test-chat/TestConversation.tsx b/apps/internal-ui/src/features/ai/test-chat/TestConversation.tsx
deleted file mode 100644
index d4cac3a..0000000
--- a/apps/internal-ui/src/features/ai/test-chat/TestConversation.tsx
+++ /dev/null
@@ -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 = {
- MAX: "MAX",
- TG: "Telegram",
- WEB: "Web Chat",
-};
-
-export function TestConversation({ channel }: { channel: TestChannel }) {
- const [message, setMessage] = useState("");
- const textareaRef = useRef(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) => {
- setMessage(event.target.value);
- };
-
- return (
-
-
-
-
-
- Test conversation
- Симуляция клиента · сообщений: 4
-
-
-
Не создаёт Contact, Order или Payment
-
-
-
-
Сценарий: «Покупка коробки» · канал {channelLabel[channel]}
-
-
- Здравствуйте! FirePage — это готовый нишевой сайт под ключ. Коробка стоит ₽4 900 единоразово. Дополнительно можно подключить годовую поддержку за ₽1 470. Под визитку отлично подходит — оформить?
-
-
-
- Отлично! Подготовил оформление коробки FirePage за ₽4 900. Вызываю оформление покупки — в реальном диалоге клиент получил бы ссылку на оплату.
-
-
-
-
-
- );
-}
-
-function ClientMessage({ note, text }: { note?: string; text: string }) {
- return (
-
-
Т
-
-
{text}
- {note &&
{note} }
-
-
- );
-}
-
-function AiMessage({ children, tool }: { children: ReactNode; tool?: string }) {
- return (
-
-
-
-
AI · REL-FP-v5
-
{children}
- {tool &&
tool: {tool} }
-
-
- );
-}
diff --git a/apps/internal-ui/src/features/ai/test-chat/conversation.css b/apps/internal-ui/src/features/ai/test-chat/conversation.css
deleted file mode 100644
index 8b57d26..0000000
--- a/apps/internal-ui/src/features/ai/test-chat/conversation.css
+++ /dev/null
@@ -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;
-}
diff --git a/apps/internal-ui/src/features/ai/test-chat/diagnostics.css b/apps/internal-ui/src/features/ai/test-chat/diagnostics.css
deleted file mode 100644
index 8f97254..0000000
--- a/apps/internal-ui/src/features/ai/test-chat/diagnostics.css
+++ /dev/null
@@ -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;
-}
diff --git a/apps/internal-ui/src/features/ai/test-chat/model.ts b/apps/internal-ui/src/features/ai/test-chat/model.ts
deleted file mode 100644
index e13fa4e..0000000
--- a/apps/internal-ui/src/features/ai/test-chat/model.ts
+++ /dev/null
@@ -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 в год, продление по желанию." },
-];
diff --git a/apps/internal-ui/src/features/ai/test-chat/styles.css b/apps/internal-ui/src/features/ai/test-chat/styles.css
deleted file mode 100644
index 5985159..0000000
--- a/apps/internal-ui/src/features/ai/test-chat/styles.css
+++ /dev/null
@@ -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;
-}
diff --git a/apps/internal-ui/src/layout/Shell.tsx b/apps/internal-ui/src/layout/Shell.tsx
index 587439b..fbcf5a2 100644
--- a/apps/internal-ui/src/layout/Shell.tsx
+++ b/apps/internal-ui/src/layout/Shell.tsx
@@ -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);
diff --git a/apps/internal-ui/src/layout/ShellRouteContent.tsx b/apps/internal-ui/src/layout/ShellRouteContent.tsx
index f5ee45c..9c58339 100644
--- a/apps/internal-ui/src/layout/ShellRouteContent.tsx
+++ b/apps/internal-ui/src/layout/ShellRouteContent.tsx
@@ -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" && }
{route === "salesOrderDetail" && }
{route === "salesOrders" && }
- {route === "aiAgents" && }
+ {route === "aiAgents" && }
{route === "aiAgentCreate" && }
- {route === "aiAgentDetail" && }
- {route === "aiRelease" && }
- {route === "aiTestChat" && }
+ {route === "aiAgentDetail" && }
+ {route === "aiRelease" && }
{route === "integrations" && }
>
);
diff --git a/apps/internal-ui/src/router.ts b/apps/internal-ui/src/router.ts
index 487dd88..2a08b93 100644
--- a/apps/internal-ui/src/router.ts
+++ b/apps/internal-ui/src/router.ts
@@ -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";
diff --git a/apps/internal-ui/src/routes.ts b/apps/internal-ui/src/routes.ts
index 7742e86..3be86ce 100644
--- a/apps/internal-ui/src/routes.ts
+++ b/apps/internal-ui/src/routes.ts
@@ -20,7 +20,6 @@ export const routes: Record = {
aiAgentCreate: "Создание агента",
aiAgentDetail: "Карточка агента",
aiRelease: "Версия продукта",
- aiTestChat: "Тестовый чат",
aiUsage: "Использование AI",
integrations: "Интеграции",
};
diff --git a/apps/internal-ui/src/styles.css b/apps/internal-ui/src/styles.css
index 922eb76..52bfce2 100644
--- a/apps/internal-ui/src/styles.css
+++ b/apps/internal-ui/src/styles.css
@@ -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";
diff --git a/apps/internal-ui/src/types.ts b/apps/internal-ui/src/types.ts
index b060a2a..bd2d14b 100644
--- a/apps/internal-ui/src/types.ts
+++ b/apps/internal-ui/src/types.ts
@@ -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[];
diff --git a/design/baseline/Тестовый чат.dc.html b/design/baseline/Тестовый чат.dc.html
deleted file mode 100644
index 968ccc9..0000000
--- a/design/baseline/Тестовый чат.dc.html
+++ /dev/null
@@ -1,218 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Тестовая песочница
-
-
ИП
-
-
-
-
-
-
-
-
-
-
ЧЕРНОВИК ВЕРСИИ
-
-
REL-FP-v5 · Черновик версии REL-FP-v4 · Опубликована
-
-
-
-
-
FirePage Sales · gpt-4o-mini · temp 0.4
-
-
-
КАНАЛ СИМУЛЯЦИИ
-
- MAX
- TG
- Web
-
-
-
ГОТОВЫЕ СЦЕНАРИИ
-
-
-
- {{ s.title }}
- {{ s.desc }}
-
-
-
-
-
-
Очистить тестовую сессию
-
-
-
-
-
-
-
-
-
Test conversation
Симуляция клиента · сообщений: 4
-
-
Не создаёт Contact, Order или Payment
-
-
-
-
-
Сценарий: «Покупка коробки» · канал {{ channelLabel }}
-
-
-
Т
Здравствуйте, интересует FirePage для сайта-визитки. Сколько стоит?
тестовый клиент
-
-
-
AI · REL-FP-v5
Здравствуйте! FirePage — это готовый нишевой сайт под ключ. Коробка стоит ₽4 900 единоразово. Дополнительно можно подключить годовую поддержку за ₽1 470. Под визитку отлично подходит — оформить?
debug ниже →
-
-
-
Т
Да, давайте оформим коробку.
-
-
-
Отлично! Подготовил оформление коробки FirePage за ₽4 900. Вызываю checkout — в реальном диалоге клиент получил бы ссылку на оплату.
tool: create_checkout
-
-
-
-
-
-
-
-
-
- Диагностика Последний ответ AI
-
-
-
-
-
Tokens
1 284
in 940 · out 344
-
-
-
-
-
ИСПОЛЬЗОВАННЫЕ CHUNKS
-
-
-
-
{{ c.source }} {{ c.score }}
-
{{ c.text }}
-
-
-
-
-
-
ВЫЗВАННЫЕ TOOLS
-
-
get_offers
product=firepage → 2 offers
120мс
-
create_checkout
offer=OFR-FP-BOX · ₽4 900 (симуляция)
88мс
-
-
-
-
HANDOFF
-
Передачи оператору не было — сценарий в рамках агента.
-
-
-
-
-
-
-
-
-
-
-